litellm/tests/e2e/perf/setup/ramp.py
Kerry Lu b99d2cc06e
test(perf): ramp the load cases until the pod stops keeping up and floor sustained_rps (#47758)
* 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>
2026-10-20 22:31:08 -07:00

116 lines
5.2 KiB
Python

"""A ramped run step by step: the send schedule its plan carries (`send_offsets`), one `StepReading` per step
and `sustained_rps`, the last step the pod held before the first it did not. `Ramp` itself sits in
`performance_test_case.py` with the rest of the case schema."""
from __future__ import annotations
import random
from collections.abc import Iterable, Iterator
from dataclasses import dataclass
from itertools import chain, takewhile
from typing import Final
from setup.performance_test_case import Ramp
HELD_SHARE: Final = 0.9
"""A step that completed fewer than this share of its rate is a queue that stopped draining."""
HELD_UNEXPECTED_SHARE: Final = 0.01
"""A step with more than this share of unexpected statuses broke, whatever its latency."""
_JITTER: Final = 0.25
def _step_offsets(ramp: Ramp, index: int, count: int, rng: random.Random) -> Iterator[float]:
for slot in range(count):
yield (index + (slot + 0.5 + rng.uniform(-_JITTER, _JITTER)) / count) * ramp.step_seconds
def send_offsets(ramp: Ramp, seed: int) -> tuple[float, ...]:
"""Seconds after the run starts to send each request of the plan, in plan order: a step's requests spread
evenly over its window, each moved by up to a quarter of a slot either way, drawn from `seed` so a rerun
sends at the same seconds."""
rng: Final = random.Random(f"ramp-{seed}")
return tuple(chain.from_iterable(_step_offsets(ramp, index, count, rng) for index, count in enumerate(ramp.counts)))
@dataclass(frozen=True, slots=True)
class StepReading:
rps: float
planned: int
completed_rps: float
gateway_added_p99: float | None
unexpected: int
held_p99_seconds: float
gateway_added_p50: float | None = None
time_to_first_token_p99: float | None = None
cpu_seconds_per_request: float | None = None
redis_latency_mean_seconds: float | None = None
postgres_peak_connections: int | None = None
"""The hold verdict reads the first six. The rest say what the pod was doing on that step: CPU per request
from the pod sampler, Redis latency from the gateway's `/metrics`, connections from `pg_stat_activity`,
all cut to the step's window, so a latency jump can be told apart as CPU, waiting or neither."""
@property
def held(self) -> bool:
return (
self.gateway_added_p99 is not None
and self.gateway_added_p99 <= self.held_p99_seconds
and self.completed_rps >= HELD_SHARE * self.rps
and self.unexpected <= HELD_UNEXPECTED_SHARE * self.planned
)
def sustained_rps(steps: Iterable[StepReading]) -> float:
"""The rate of the last step that held before the first that did not, 0 when the first step did not hold."""
held: Final = tuple(takewhile(lambda step: step.held, steps))
return held[-1].rps if held else 0.0
def step_properties(steps: Iterable[StepReading]) -> tuple[tuple[str, float], ...]:
"""The per-step readings as JUnit properties (`perf_ramp_step_<rps>_<reading>`, every reading the step has)
plus the hold bound, so the dashboard can draw the run's curves next to the sustained rate."""
rows: Final = tuple(steps)
hold: Final = (("perf_ramp_hold_p99_seconds", rows[0].held_p99_seconds),) if rows else ()
return (*hold, *chain.from_iterable(_step_properties(step) for step in rows))
def _step_properties(step: StepReading) -> Iterator[tuple[str, float]]:
prefix: Final = f"perf_ramp_step_{step.rps:g}"
readings: Final[tuple[tuple[str, float | None], ...]] = (
("gateway_added_p99", step.gateway_added_p99),
("completed_rps", step.completed_rps),
("unexpected", float(step.unexpected)),
("gateway_added_p50", step.gateway_added_p50),
("time_to_first_token_p99", step.time_to_first_token_p99),
("cpu_seconds_per_request", step.cpu_seconds_per_request),
("redis_latency_mean_seconds", step.redis_latency_mean_seconds),
(
"postgres_peak_connections",
None if step.postgres_peak_connections is None else float(step.postgres_peak_connections),
),
)
return ((f"{prefix}_{name}", value) for name, value in readings if value is not None)
def render_steps(steps: Iterable[StepReading]) -> tuple[str, ...]:
rows: Final = tuple(steps)
if not rows:
return ()
header: Final = (
f" steps: {'rps':>6} {'planned':>8} {'completed rps':>14} {'gateway added p50':>18} {'p99':>8}"
f" {'ttft p99':>9} {'unexpected':>11} {'cpu s/req':>10} {'redis mean':>11} {'pg conns':>9}"
)
return (header, *(_step_line(step) for step in rows))
def _seconds(value: float | None) -> str:
return "n/a" if value is None else f"{value:.4f}"
def _step_line(step: StepReading) -> str:
conns: Final = "n/a" if step.postgres_peak_connections is None else str(step.postgres_peak_connections)
return (
f" {step.rps:6.1f} {step.planned:8d} {step.completed_rps:14.2f} {_seconds(step.gateway_added_p50):>18}"
f" {_seconds(step.gateway_added_p99):>8} {_seconds(step.time_to_first_token_p99):>9} {step.unexpected:11d}"
f" {_seconds(step.cpu_seconds_per_request):>10} {_seconds(step.redis_latency_mean_seconds):>11} {conns:>9}"
f" {'held' if step.held else 'broke'}"
)