mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-30 06:43:27 +00:00
* test(perf): add small_bodies_30rps and mixed_size_requests_4rps load cases Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(perf): run the load cases at 50 rps small and 5 rps mixed, the pod's knee Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(perf): ramp the load cases until the pod stops keeping up and report sustained_rps Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(perf): hand the ramp to the observation and widen the mixed ramp to 14 rps Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(perf): floor the small ramp at 20 rps and emit the per-step readings Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(perf): emit the ramp's hold bound and each step's unexpected statuses Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(perf): read each ramp step's cpu, redis latency and connections from the samplers' windows Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(perf): floor mixed_size_requests_ramp at 8 rps from the lane's 10 rps sustained step Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(perf): compare step readings with math.isclose so the e2e basedpyright gate stays clean Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --------- Co-authored-by: kerry <kerry@berri.ai> Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
110 lines
4.3 KiB
Python
110 lines
4.3 KiB
Python
"""The gateway's own `/metrics`, read before and after a run (and every few seconds during a ramped run, see
|
|
`telemetry_sampler`) for the numbers nothing outside the gateway can see. Today that is `litellm_redis_latency`, the histogram customers' Grafana panels show when Redis
|
|
slows the gateway down. The gateway's latency and request counts are read per request from its response
|
|
headers instead (`setup/wire.py`), which gives percentiles where a histogram delta only gives a mean.
|
|
|
|
RSS and CPU are not here: a multi-worker gateway serves `/metrics` through the prometheus multiprocess
|
|
collector, which drops `process_resident_memory_bytes` and `process_cpu_seconds_total`, so those stay
|
|
on psutil (`proxy_usage.py`)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import math
|
|
import threading
|
|
from collections.abc import Generator, Iterable
|
|
from contextlib import contextmanager
|
|
from dataclasses import dataclass
|
|
from itertools import chain
|
|
from typing import Final
|
|
|
|
from e2e_http import NoBody
|
|
from prometheus_client.parser import text_string_to_metric_families
|
|
from prometheus_client.samples import Sample
|
|
from proxy_client import ProxyClient
|
|
|
|
from setup.series import Recorder, Series
|
|
|
|
_SAMPLE_SECONDS: Final = 10.0
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class HistogramTotals:
|
|
"""`_sum` and `_count` of one histogram across every label set, enough for a mean over a window."""
|
|
|
|
total: float
|
|
count: float
|
|
|
|
def minus(self, earlier: HistogramTotals) -> HistogramTotals:
|
|
return HistogramTotals(total=self.total - earlier.total, count=self.count - earlier.count)
|
|
|
|
@property
|
|
def mean(self) -> float | None:
|
|
return self.total / self.count if self.count > 0 else None
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class GatewayTelemetry:
|
|
redis_latency: HistogramTotals
|
|
"""`litellm_redis_latency`, seconds per Redis call the gateway made."""
|
|
|
|
def minus(self, earlier: GatewayTelemetry) -> GatewayTelemetry:
|
|
return GatewayTelemetry(redis_latency=self.redis_latency.minus(earlier.redis_latency))
|
|
|
|
|
|
def _value(samples: Iterable[Sample], name: str) -> float:
|
|
return sum(sample.value for sample in samples if sample.name == name and math.isfinite(sample.value))
|
|
|
|
|
|
def _histogram(samples: tuple[Sample, ...], family: str) -> HistogramTotals:
|
|
return HistogramTotals(total=_value(samples, f"{family}_sum"), count=_value(samples, f"{family}_count"))
|
|
|
|
|
|
def parse_gateway_telemetry(text: str) -> GatewayTelemetry:
|
|
samples: Final = tuple(chain.from_iterable(family.samples for family in text_string_to_metric_families(text)))
|
|
return GatewayTelemetry(redis_latency=_histogram(samples, "litellm_redis_latency"))
|
|
|
|
|
|
def read_gateway_telemetry(proxy: ProxyClient) -> GatewayTelemetry | None:
|
|
"""One `/metrics` scrape, or None when the gateway does not serve it (no `prometheus` callback), in which
|
|
case every limit on a gateway-reported metric fails as missing rather than reading a silent zero."""
|
|
scrape: Final = proxy.probe("/metrics", params=NoBody())
|
|
return parse_gateway_telemetry(scrape.body) if scrape.status_code == 200 else None
|
|
|
|
|
|
class TelemetrySampler:
|
|
"""`/metrics` every `_SAMPLE_SECONDS` on a background thread while traffic runs, so a ramped run can read
|
|
the Redis latency of each step's window as the difference between the scrapes around it."""
|
|
|
|
def __init__(self, proxy: ProxyClient) -> None:
|
|
self._proxy: Final = proxy
|
|
self._stop: Final = threading.Event()
|
|
self._thread: Final = threading.Thread(target=self._run, name="gateway-telemetry", daemon=True)
|
|
self._scrapes: Final[Recorder[GatewayTelemetry]] = Recorder()
|
|
|
|
@property
|
|
def series(self) -> Series[GatewayTelemetry]:
|
|
return self._scrapes.series
|
|
|
|
def _run(self) -> None:
|
|
while not self._stop.is_set():
|
|
scrape: GatewayTelemetry | None = read_gateway_telemetry(self._proxy)
|
|
if scrape is not None:
|
|
self._scrapes.record(scrape)
|
|
_ = self._stop.wait(_SAMPLE_SECONDS)
|
|
|
|
def start(self) -> None:
|
|
self._thread.start()
|
|
|
|
def stop(self) -> None:
|
|
self._stop.set()
|
|
self._thread.join(timeout=30)
|
|
|
|
|
|
@contextmanager
|
|
def telemetry_sampler(proxy: ProxyClient) -> Generator[TelemetrySampler]:
|
|
sampler: Final = TelemetrySampler(proxy)
|
|
sampler.start()
|
|
try:
|
|
yield sampler
|
|
finally:
|
|
sampler.stop()
|