Files
2026-07-13 13:17:40 +08:00

145 lines
3.9 KiB
Python

import time
from enum import Enum
from typing import (
AsyncGenerator,
Callable,
List,
Set,
TypeVar,
)
from ray.util import metrics
# Histogram buckets for short-range latencies measurements:
# Min=1ms, Max=30s
#
# NOTE: Number of buckets have to be bounded (and not exceed 30)
# to avoid overloading metrics sub-system
SHORT_RANGE_LATENCY_HISTOGRAM_BUCKETS_MS: List[float] = [
1,
5,
10,
25,
50,
100,
150,
250,
500,
1000,
1500,
2500,
5000,
7500,
10000,
20000,
30000,
]
# Histogram buckets for long-range latencies measurements:
# Min=10ms, Max=300s
LONG_RANGE_LATENCY_HISTOGRAM_BUCKETS_MS = [
x * 10 for x in SHORT_RANGE_LATENCY_HISTOGRAM_BUCKETS_MS
]
class ClockUnit(int, Enum):
ms = 1000
s = 1
class MsClock:
"""A clock that tracks intervals in milliseconds"""
def __init__(self, unit: ClockUnit = ClockUnit.ms):
self.reset()
self.unit = unit.value
self.start_time = time.perf_counter()
def reset(self):
self.start_time = time.perf_counter()
def interval(self):
return (time.perf_counter() - self.start_time) * self.unit
def reset_interval(self):
interval = self.interval()
self.reset()
return interval
T = TypeVar("T")
class InstrumentTokenAsyncGenerator:
"""This class instruments an asynchronous generator.
It gathers 3 metrics:
1. Time to first time
2. Time between tokens
3. Total completion time
Usage:
@InstrumentTokenAsyncGenerator("my_special_fn")
async def to_instrument():
yield ...
"""
all_instrument_names: Set[str] = set()
def __init__(
self, generator_name: str, latency_histogram_buckets: List[float] = None
):
self.generator_name = f"rayllm_{generator_name}"
target_latency_histogram_buckets = (
latency_histogram_buckets or SHORT_RANGE_LATENCY_HISTOGRAM_BUCKETS_MS
)
assert (
self.generator_name not in self.all_instrument_names
), "This generator name was already used elsewhere. Please specify another one."
self.all_instrument_names.add(self.generator_name)
self.token_latency_histogram = metrics.Histogram(
f"{self.generator_name}_per_token_latency_ms",
f"Generator metrics for {self.generator_name}",
boundaries=target_latency_histogram_buckets,
)
self.first_token_latency_histogram = metrics.Histogram(
f"{self.generator_name}_first_token_latency_ms",
f"Generator metrics for {self.generator_name}",
boundaries=target_latency_histogram_buckets,
)
self.total_latency_histogram = metrics.Histogram(
f"{self.generator_name}_total_latency_ms",
f"Generator metrics for {self.generator_name}",
boundaries=target_latency_histogram_buckets,
)
def __call__(
self, async_generator_fn: Callable[..., AsyncGenerator[T, None]]
) -> Callable[..., AsyncGenerator[T, None]]:
async def new_gen(*args, **kwargs):
interval_clock = MsClock()
total_clock = MsClock()
is_first_token = True
try:
async for x in async_generator_fn(*args, **kwargs):
if is_first_token:
self.first_token_latency_histogram.observe(
total_clock.interval()
)
interval_clock.reset()
is_first_token = False
else:
self.token_latency_histogram.observe(
interval_clock.reset_interval()
)
yield x
finally:
self.total_latency_histogram.observe(total_clock.interval())
return new_gen