-
Notifications
You must be signed in to change notification settings - Fork 448
[metrics 3/5] Publish weighted fresh-run vLLM summaries #2367
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -22,6 +22,7 @@ | |
| from loguru import logger | ||
|
|
||
| from skyrl.backends.skyrl_train.inference_servers.common import format_http_url | ||
| from skyrl.train.utils.vllm_run_statistics import RunStatistics | ||
| from skyrl.train.utils.vllm_window_statistics import latency_metrics | ||
|
|
||
| # vLLM metric base names after RayPrometheusStatLogger sanitization (`:` -> `_`) | ||
|
|
@@ -39,6 +40,7 @@ | |
| _COUNTER_PREEMPTIONS = "ray_vllm_num_preemptions_total" | ||
| _COUNTER_EXTERNAL_PREFIX_QUERIES = "ray_vllm_external_prefix_cache_queries_total" | ||
| _COUNTER_EXTERNAL_PREFIX_HITS = "ray_vllm_external_prefix_cache_hits_total" | ||
| _COUNTER_KV_OFFLOAD_STORE_BYTES = "ray_vllm_kv_offload_store_bytes_total" | ||
| _COUNTER_KV_OFFLOAD_LOAD_BYTES = "ray_vllm_kv_offload_load_bytes_total" | ||
| _HIST_TTFT_SUM = "ray_vllm_time_to_first_token_seconds_sum" | ||
| _HIST_TTFT_COUNT = "ray_vllm_time_to_first_token_seconds_count" | ||
|
|
@@ -69,6 +71,7 @@ | |
| _COUNTER_PREEMPTIONS, | ||
| _COUNTER_EXTERNAL_PREFIX_QUERIES, | ||
| _COUNTER_EXTERNAL_PREFIX_HITS, | ||
| _COUNTER_KV_OFFLOAD_STORE_BYTES, | ||
| _COUNTER_KV_OFFLOAD_LOAD_BYTES, | ||
| _HIST_REQUEST_TPOT_SUM, | ||
| _HIST_REQUEST_TPOT_COUNT, | ||
|
|
@@ -211,6 +214,7 @@ def __init__( | |
| self._urls = urls if urls is not None else discover_ray_metrics_urls() | ||
| self._timeout = request_timeout_s | ||
| self._worker_ids = None if worker_ids is None else frozenset(worker_ids) | ||
| self.run_statistics = RunStatistics() | ||
| self._prev_aggregated: Optional[Dict[str, float]] = None | ||
| self._prev_timestamp: Optional[float] = None | ||
| self._client: Optional[httpx.AsyncClient] = None | ||
|
|
@@ -306,6 +310,8 @@ async def _read_snapshot(self) -> Optional[Dict[str, float]]: | |
| counters = engine_counters.setdefault(engine, {}) | ||
| counters[name] = counters.get(name, 0.0) + value | ||
| self._engine_snapshot = engine_counters | ||
| if self._worker_ids is not None and {engine[0] for engine in engine_counters} != self._worker_ids: | ||
| return None | ||
| buckets = {} | ||
| schemas = {} | ||
| for (name, labels), value in parsed.items(): | ||
|
|
@@ -328,14 +334,29 @@ async def _read_snapshot(self) -> Optional[Dict[str, float]]: | |
| # Ray counters skip zero increments. A live engine gauge confirms the exporter exists. | ||
| if any(name == _GAUGE_NUM_RUNNING for name, _ in parsed): | ||
| sums.setdefault(_COUNTER_PREEMPTIONS, 0.0) | ||
| sums.setdefault(_COUNTER_PROMPT_TOKENS, 0.0) | ||
| sums.setdefault(_COUNTER_GENERATION_TOKENS, 0.0) | ||
| for hits, queries in ( | ||
| (_COUNTER_PREFIX_HITS, _COUNTER_PREFIX_QUERIES), | ||
| (_COUNTER_EXTERNAL_PREFIX_HITS, _COUNTER_EXTERNAL_PREFIX_QUERIES), | ||
| ): | ||
| if queries in sums: | ||
| sums.setdefault(hits, 0.0) | ||
| if _COUNTER_KV_OFFLOAD_STORE_BYTES in sums: | ||
| sums.setdefault(_COUNTER_KV_OFFLOAD_LOAD_BYTES, 0.0) | ||
| return {**sums, **means, **per_pos, **buckets} | ||
|
|
||
| async def finalize(self) -> Dict[str, float]: | ||
| """Attempt one final collection and close the HTTP client.""" | ||
| try: | ||
| if self._label is not None: | ||
| await self.stop() | ||
| else: | ||
| await self.sample() | ||
| return self.run_statistics.summary() | ||
| finally: | ||
| await self.aclose() | ||
|
|
||
| async def sample(self, generation_time_s: Optional[float] = None) -> Dict[str, float]: | ||
| """Return ``vllm/...`` scalars for the current step (empty if unavailable). | ||
|
|
||
|
|
@@ -345,6 +366,7 @@ async def sample(self, generation_time_s: Optional[float] = None) -> Dict[str, f | |
| """ | ||
| snapshot = await self._read_snapshot() | ||
| if snapshot is None: | ||
| self.run_statistics.incomplete.add("combined") | ||
| self._prev_aggregated = None | ||
| self._prev_timestamp = None | ||
| self._previous_engines = {} | ||
|
|
@@ -355,6 +377,7 @@ async def sample(self, generation_time_s: Optional[float] = None) -> Dict[str, f | |
| dt = max(now - self._prev_timestamp, 1e-9) | ||
| window = generation_time_s if (generation_time_s is not None and generation_time_s > 0) else dt | ||
| out = self._window_metrics(self._prev_aggregated, snapshot, window, "vllm/") | ||
| self.run_statistics.add("combined", self._prev_aggregated, snapshot, window) | ||
| else: | ||
| out = self._window_metrics(None, snapshot, None, "vllm/") # gauges only | ||
|
|
||
|
|
@@ -418,6 +441,7 @@ async def stop(self) -> Dict[str, float]: | |
| self._window_prev = None | ||
| self._active_since = None | ||
| self._paused = False | ||
| self.run_statistics.add(label.removeprefix("vllm/"), prev, new_snapshot, window) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| if new_snapshot is None: | ||
| return {} | ||
| result = self._window_metrics(prev, new_snapshot, window, f"{label}/") | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| """Weighted summaries of observed, non-overlapping vLLM windows.""" | ||
|
|
||
| from skyrl.train.utils.vllm_window_statistics import counter_deltas | ||
|
|
||
|
|
||
| class RunStatistics: | ||
| """Sum raw counters and durations, keeping sync train and eval separate.""" | ||
|
|
||
| def __init__(self): | ||
| self.windows = {} | ||
| self.seconds = {} | ||
| self.incomplete = set() | ||
|
|
||
| def add(self, scope, previous, current, duration): | ||
| """Omit a scope with a missing/reset window; never average step rates.""" | ||
| deltas = counter_deltas(previous, current) | ||
| if deltas is None: | ||
| self.incomplete.add(scope) | ||
| return | ||
| if scope not in self.windows: | ||
| self.windows[scope] = deltas | ||
| else: | ||
| total = self.windows[scope] | ||
| for name in deltas: | ||
| if "_bucket::" in name and total.get(name.split("_bucket::", 1)[0] + "_count") == 0: | ||
| total.setdefault(name, 0.0) | ||
| # Only counters observed over every window have full-run denominators. | ||
| self.windows[scope] = {name: value + deltas[name] for name, value in total.items() if name in deltas} | ||
|
cursor[bot] marked this conversation as resolved.
|
||
| self.seconds[scope] = self.seconds.get(scope, 0.0) + duration | ||
|
cursor[bot] marked this conversation as resolved.
|
||
|
|
||
| def summary(self): | ||
| """Derive rates, request-weighted means and merged histogram P90 values.""" | ||
| from skyrl.train.utils.vllm_metrics_scraper import VLLMMetricsScraper | ||
|
|
||
| result = {} | ||
| for scope, deltas in self.windows.items(): | ||
| if scope in self.incomplete: | ||
| continue | ||
| prefix = f"vllm_correct_aggregate/{scope}/" | ||
| metrics = VLLMMetricsScraper._derive(deltas, dict.fromkeys(deltas, 0), self.seconds[scope], prefix) | ||
| result.update( | ||
| {key: value for key, value in metrics.items() if "draft_num_" not in key and "_pos_" not in key} | ||
| ) | ||
| result[prefix + "measurement_seconds"] = self.seconds[scope] | ||
| for counter, public in ( | ||
| ("generation_tokens", "output_tokens_total"), | ||
| ("prompt_tokens", "prompt_tokens_total"), | ||
| ("num_preemptions", "preemptions_total"), | ||
| ("kv_offload_store_bytes", "kv_offload_store_bytes_total"), | ||
| ("kv_offload_load_bytes", "kv_offload_load_bytes_total"), | ||
| ): | ||
| value = deltas.get(f"ray_vllm_{counter}_total") | ||
| if value is not None: | ||
| result[prefix + public] = value | ||
| return result | ||
Uh oh!
There was an error while loading. Please reload this page.