Files
vllm-omni/tests/edge/test_step_stats.py
T
tzhouamandClaude Fable 5.1 37c0ea9a6b [Edge][WP8] Per-step overhead attribution, edge scheduling helpers, CPU thread sweep
- vllm_omni/edge/step_stats.py: process-local counters enabled by
  VLLM_OMNI_STEP_STATS_DIR (scheduler schedule/update_from_output, engine-core
  step, chunk hop enqueue->put / put->get / get, orchestrator dispatch), dumped
  per process and merged into an attribution table (share of the 80 ms frame).
  Hooks: stage_engine_core_proc (instrument after construction, dump at exit),
  chunk_transfer_adapter (put/get timing, put->get via the shm lock-file mtime),
  orchestrator dispatch loop. Bench: stream_latency_bench.py --step-stats.
- vllm_omni/edge/scheduling.py: playback slack, prefill-throttle decision and
  the no-preemption KV admission rule (exact bytes/token from the talker KV
  geometry); adapt.derive_overrides now sizes max_num_seqs with it and
  auto_profile passes the geometry read from the model config.
- Engine re-reads parallel_stage_init after apply_deploy_profile so the edge
  profile's orchestrator default takes effect (was read before the profile).
- benchmarks/tts/cpu_thread_sweep.py: CPU-only 8/16/32-thread sweep (+inductor)
  with per-step attribution.
- Tests: tests/core/test_edge_scheduling.py, tests/edge/test_step_stats.py,
  tests/benchmarks/test_cpu_thread_sweep.py, profile default test.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-09 14:55:55 +08:00

90 lines
3.0 KiB
Python

"""WP8 per-step overhead attribution counters (CPU)."""
from __future__ import annotations
import json
import pytest
from vllm_omni.edge import step_stats as ss
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
def test_disabled_stats_are_noops(monkeypatch):
monkeypatch.delenv(ss.ENV_DIR, raising=False)
st = ss.StepStats.reset(None, role="x")
assert not st.enabled
st.add("a", 1.0)
with st.timed("b"):
pass
fn = st.wrap("c", lambda: 3)
assert fn() == 3 and not hasattr(fn, "__wrapped__")
assert st.to_dict()["counters"] == {} and st.dump() is None
def test_series_summary_and_merge(tmp_path):
st = ss.StepStats.reset(str(tmp_path), role="stage0")
for v in (1.0, 2.0, 3.0, 4.0, 100.0):
st.add("sched.schedule_ms", v)
with st.timed("core.step_ms"):
pass
path = st.dump()
assert path and path.endswith(f"stage0_{st.pid}.json")
d = json.loads(open(path).read())
s = d["counters"]["sched.schedule_ms"]
assert s["count"] == 5 and s["max_ms"] == 100.0 and s["mean_ms"] == pytest.approx(22.0) and s["p50_ms"] == 3.0
# a second process with the same role merges by count/sum and worst p95
(tmp_path / "stage0_999.json").write_text(
json.dumps(
{
"role": "stage0",
"counters": {
"sched.schedule_ms": {
"count": 5,
"sum_ms": 5.0,
"mean_ms": 1.0,
"p50_ms": 1.0,
"p95_ms": 1.0,
"max_ms": 1.0,
}
},
}
)
)
merged = ss.merge_dir(tmp_path)
m = merged["roles"]["stage0"]["sched.schedule_ms"]
assert m["count"] == 10 and m["sum_ms"] == pytest.approx(115.0) and m["p95_ms"] == 100.0
rows = merged["attribution"]
assert rows[0]["counter"] == "sched.schedule_ms" and rows[0]["share_of_frame"] == pytest.approx(11.5 / 80)
assert "share of 80 ms" in ss.format_table(rows)
def test_instrument_engine_core_wraps_scheduler_and_step(tmp_path):
ss.StepStats.reset(str(tmp_path), role="stage1")
class Sched:
def schedule(self, throttle_prefills=False):
return "out"
def update_from_output(self, a, b):
return {}
class Core:
def __init__(self):
self.scheduler = Sched()
self.step_fn = lambda: ({}, True)
core = Core()
ss.instrument_engine_core(core)
ss.instrument_engine_core(core) # idempotent
assert core.scheduler.schedule(True) == "out" and core.scheduler.update_from_output(1, 2) == {}
assert core.step_fn() == ({}, True)
counters = ss.StepStats.get().to_dict()["counters"]
assert {"sched.schedule_ms", "sched.update_from_output_ms", "core.step_ms"} <= set(counters)
assert counters["core.step_ms"]["count"] == 1
def test_shm_lockfile_age_missing_is_none():
assert ss.shm_lockfile_age_ms("definitely_missing_key_xyz") is None