# SPDX-License-Identifier: Apache-2.0 # SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project """Steady-state per-step overhead attribution (edge scheduling, WP8). Process-local counters that answer "where does a codec frame's 80 ms budget go outside the model forward?": scheduler ``schedule()`` / ``update_from_output()`` time, the whole engine-core step, the talker -> code2wav chunk hop (enqueue -> connector put, put -> consumer get, get duration) and the orchestrator dispatch loop. Enabled by ``VLLM_OMNI_STEP_STATS_DIR=``; every process dumps ``/_.json`` periodically and at shutdown. ``merge_dir`` folds the files into one attribution table (``benchmarks/tts/stream_latency_bench.py --step-stats``). Zero cost when disabled: ``StepStats.get().enabled`` is False and every hook returns immediately. """ from __future__ import annotations import atexit import json import os import threading import time from collections.abc import Callable, Iterator from contextlib import contextmanager from pathlib import Path from typing import Any ENV_DIR = "VLLM_OMNI_STEP_STATS_DIR" ENV_SYNC = "VLLM_OMNI_STEP_STATS_SYNC" # "1": synchronize the accelerator around model-level timers ENV_STAGE_ID = "VLLM_OMNI_STAGE_ID" FRAME_BUDGET_MS = 80.0 # one Qwen3-TTS codec frame at 12.5 Hz _MAX_SAMPLES = 20000 _FLUSH_EVERY = 500 class _Series: __slots__ = ("count", "total", "max", "samples") def __init__(self) -> None: self.count = 0 self.total = 0.0 self.max = 0.0 self.samples: list[float] = [] def add(self, v: float) -> None: self.count += 1 self.total += v if v > self.max: self.max = v if len(self.samples) < _MAX_SAMPLES: self.samples.append(v) else: # keep a uniform-ish reservoir: overwrite a pseudo-random slot self.samples[(self.count * 7919) % _MAX_SAMPLES] = v def summary(self) -> dict[str, float | int]: s = sorted(self.samples) n = len(s) def pct(p: float) -> float: if not n: return 0.0 return s[min(n - 1, int(round((n - 1) * p)))] return { "count": self.count, "sum_ms": self.total, "mean_ms": (self.total / self.count) if self.count else 0.0, "p50_ms": pct(0.5), "p95_ms": pct(0.95), "max_ms": self.max, } class StepStats: """Process-local counter registry (thread-safe adds).""" _instance: StepStats | None = None _instance_lock = threading.Lock() def __init__(self, out_dir: str | None, role: str | None = None) -> None: self.out_dir = out_dir self.enabled = bool(out_dir) self.sync = os.environ.get(ENV_SYNC, "0").strip().lower() in ("1", "true", "yes", "on") self.role = role or _default_role() self.pid = os.getpid() self._series: dict[str, _Series] = {} self._lock = threading.Lock() self._n_since_flush = 0 self.started_at = time.time() if self.enabled: Path(out_dir).mkdir(parents=True, exist_ok=True) atexit.register(self.dump) # ------------------------------------------------------------ singleton @classmethod def get(cls) -> StepStats: inst = cls._instance if inst is None: with cls._instance_lock: inst = cls._instance if inst is None: inst = cls(os.environ.get(ENV_DIR) or None) cls._instance = inst return inst @classmethod def reset(cls, out_dir: str | None = None, role: str | None = None) -> StepStats: """Replace the singleton (tests / explicit enablement in-process).""" with cls._instance_lock: cls._instance = cls(out_dir, role) return cls._instance # ------------------------------------------------------------ recording def add(self, name: str, value_ms: float) -> None: if not self.enabled: return with self._lock: s = self._series.get(name) if s is None: s = self._series[name] = _Series() s.add(float(value_ms)) self._n_since_flush += 1 flush = self._n_since_flush >= _FLUSH_EVERY if flush: self._n_since_flush = 0 if flush: self.dump() @contextmanager def timed(self, name: str, sync_device: Any = None) -> Iterator[None]: """Time a block; with ``sync_device`` (a torch device) and ``VLLM_OMNI_STEP_STATS_SYNC=1`` the accelerator is synchronized before and after so async kernels are included.""" if not self.enabled: yield return sync = sync_device is not None and self.sync and getattr(sync_device, "type", "cpu") == "cuda" if sync: import torch torch.accelerator.synchronize(sync_device) t0 = time.perf_counter() try: yield finally: if sync: torch.accelerator.synchronize(sync_device) self.add(name, (time.perf_counter() - t0) * 1000.0) def wrap(self, name: str, fn: Callable[..., Any]) -> Callable[..., Any]: """Return ``fn`` timed under ``name`` (identity when disabled).""" if not self.enabled: return fn def timed_fn(*args: Any, **kwargs: Any) -> Any: t0 = time.perf_counter() try: return fn(*args, **kwargs) finally: self.add(name, (time.perf_counter() - t0) * 1000.0) timed_fn.__name__ = getattr(fn, "__name__", name) timed_fn.__wrapped__ = fn # type: ignore[attr-defined] return timed_fn # ------------------------------------------------------------ output def to_dict(self) -> dict[str, Any]: with self._lock: counters = {k: v.summary() for k, v in self._series.items()} return { "role": self.role, "pid": self.pid, "started_at": self.started_at, "dumped_at": time.time(), "counters": counters, } def dump(self) -> str | None: if not self.enabled: return None path = Path(self.out_dir) / f"{self.role}_{self.pid}.json" tmp = path.with_suffix(".json.tmp") try: with open(tmp, "w") as f: json.dump(self.to_dict(), f, indent=1) os.replace(tmp, path) except OSError: return None return str(path) def _default_role() -> str: sid = os.environ.get(ENV_STAGE_ID) return f"stage{sid}" if sid not in (None, "") else "orchestrator" # ---------------------------------------------------------------- hooks def instrument_engine_core(engine_core: Any) -> None: """Time the scheduler and the whole step of a (stage) EngineCore instance.""" stats = StepStats.get() if not stats.enabled: return sched = getattr(engine_core, "scheduler", None) if sched is not None: for name in ("schedule", "update_from_output"): fn = getattr(sched, name, None) if callable(fn) and not hasattr(fn, "__wrapped__"): setattr(sched, name, stats.wrap(f"sched.{name}_ms", fn)) step_fn = getattr(engine_core, "step_fn", None) if callable(step_fn) and not hasattr(step_fn, "__wrapped__"): engine_core.step_fn = stats.wrap("core.step_ms", step_fn) def instrument_sampler(sampler: Any) -> None: """Time the phases of vLLM's v1 ``Sampler`` in place (idempotent, no-op when disabled).""" stats = StepStats.get() if not stats.enabled or sampler is None or getattr(sampler, "_omni_step_stats_instrumented", False): return for name in ( "apply_logits_processors", "apply_penalties", "apply_temperature", "sample", "compute_logprobs", "gather_logprobs", ): fn = getattr(sampler, name, None) if callable(fn): setattr(sampler, name, stats.wrap(f"sampler.{name}_ms", fn)) tk = getattr(sampler, "topk_topp_sampler", None) if tk is not None and callable(getattr(tk, "forward", None)): tk.forward = stats.wrap("sampler.topk_topp_ms", tk.forward) sampler._omni_step_stats_instrumented = True def shm_lockfile_age_ms(key: str, now: float | None = None) -> float | None: """Age of the SharedMemoryConnector lock file for ``key`` (written at put time).""" try: st = os.stat(f"/dev/shm/shm_{key}_lockfile.lock") except OSError: return None return ((now if now is not None else time.time()) - st.st_mtime) * 1000.0 # ---------------------------------------------------------------- merge def merge_dir(out_dir: str | Path) -> dict[str, Any]: """Merge per-process dumps into ``{role: {counter: summary}}`` plus a share table.""" roles: dict[str, dict[str, Any]] = {} for f in sorted(Path(out_dir).glob("*.json")): try: d = json.loads(f.read_text()) except (OSError, ValueError): continue role = d.get("role", f.stem) bucket = roles.setdefault(role, {}) for name, summ in d.get("counters", {}).items(): cur = bucket.get(name) if cur is None: bucket[name] = dict(summ) else: # several processes with the same role: combine count/sum/max, keep worst p95 cnt = cur["count"] + summ["count"] cur.update( count=cnt, sum_ms=cur["sum_ms"] + summ["sum_ms"], mean_ms=((cur["sum_ms"] + summ["sum_ms"]) / cnt) if cnt else 0.0, p50_ms=max(cur["p50_ms"], summ["p50_ms"]), p95_ms=max(cur["p95_ms"], summ["p95_ms"]), max_ms=max(cur["max_ms"], summ["max_ms"]), ) return {"roles": roles, "attribution": attribution(roles)} def attribution(roles: dict[str, dict[str, Any]], frame_budget_ms: float = FRAME_BUDGET_MS) -> list[dict[str, Any]]: """Rows of (role, counter, mean, p95, share of the frame budget) sorted by share.""" rows = [] for role, counters in roles.items(): for name, s in counters.items(): rows.append( { "role": role, "counter": name, "count": s["count"], "mean_ms": s["mean_ms"], "p95_ms": s["p95_ms"], "share_of_frame": (s["mean_ms"] / frame_budget_ms) if frame_budget_ms else 0.0, } ) rows.sort(key=lambda r: -r["share_of_frame"]) return rows def format_table(rows: list[dict[str, Any]]) -> str: lines = ["| role | counter | n | mean ms | p95 ms | share of 80 ms |", "|---|---|---|---|---|---|"] for r in rows: lines.append( f"| {r['role']} | {r['counter']} | {r['count']} | {r['mean_ms']:.2f} | {r['p95_ms']:.2f} " f"| {100 * r['share_of_frame']:.1f}% |" ) return "\n".join(lines)