Files
vllm-omni/benchmarks/tts/cpu_thread_sweep.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

200 lines
7.5 KiB
Python

#!/usr/bin/env python3
"""CPU-only Qwen3-TTS thread sweep (WP3): 8 / 16 / 32 threads (+ inductor variant).
For each thread count N the two stages get disjoint OpenMP thread ranges
(stage 0 = talker gets ``--stage0-share`` of N, stage 1 = code2wav the rest)
written into a variant of ``vllm_omni/deploy/qwen3_tts.yaml``'s ``platforms:
cpu:`` overlay, and ``stream_latency_bench.py`` runs pinned to those cores with
``--step-stats`` so the per-step attribution (scheduler / step / chunk hop) is
captured next to TTFA, playback-start latency, RTF and RSS.
VLLM_TARGET_DEVICE=cpu python benchmarks/tts/cpu_thread_sweep.py \
--python /path/to/omni-cpu/bin/python --threads 8,16,32 --inductor-at 16 --cpu-base 56
No GPU is used. Runs one configuration at a time.
"""
from __future__ import annotations
import argparse
import copy
import datetime as _dt
import json
import os
import shutil
import subprocess
import sys
from pathlib import Path
from typing import Any
import yaml
BENCH_DIR = Path(__file__).resolve().parent
REPO_ROOT = BENCH_DIR.parents[1]
sys.path.insert(0, str(BENCH_DIR))
from init_profile import _flatten_deploy_yaml # noqa: E402
GiB = 1 << 30
def plan_ranges(n_threads: int, cpu_base: int, stage0_share: float = 0.75) -> tuple[list[int], list[int]]:
"""Disjoint core lists for stage 0 and stage 1 (stage 0 gets the larger share, >= 1 core each)."""
if n_threads < 2:
raise ValueError("need at least 2 threads (one per stage)")
n0 = max(1, min(n_threads - 1, round(n_threads * stage0_share)))
cpus = list(range(cpu_base, cpu_base + n_threads))
return cpus[:n0], cpus[n0:]
def _range_str(cpus: list[int]) -> str:
return f"{cpus[0]}-{cpus[-1]}" if len(cpus) > 1 else str(cpus[0])
def write_cpu_variant(base_yaml: Path, out_path: Path, stage0: list[int], stage1: list[int]) -> Path:
"""Copy ``base_yaml`` (flattened) with the CPU overlay's thread binding replaced."""
cfg = copy.deepcopy(_flatten_deploy_yaml(base_yaml))
cpu = cfg.setdefault("platforms", {}).setdefault("cpu", {})
stages = {int(s.get("stage_id", i)): s for i, s in enumerate(cpu.setdefault("stages", []))}
for sid, cpus in ((0, stage0), (1, stage1)):
st = stages.get(sid)
if st is None:
st = {"stage_id": sid}
cpu["stages"].append(st)
st.setdefault("runtime", {}).setdefault("env", {})["VLLM_CPU_OMP_THREADS_BIND"] = _range_str(cpus)
out_path.parent.mkdir(parents=True, exist_ok=True)
with open(out_path, "w", encoding="utf-8") as f:
yaml.safe_dump(cfg, f, sort_keys=False)
return out_path
def run_one(name: str, n: int, inductor: bool, args: argparse.Namespace, out_dir: Path) -> dict[str, Any]:
case_dir = out_dir / name
case_dir.mkdir(parents=True, exist_ok=True)
s0, s1 = plan_ranges(n, args.cpu_base, args.stage0_share)
yaml_path = write_cpu_variant(Path(args.base_yaml), case_dir / f"{name}.yaml", s0, s1)
env = dict(os.environ)
env["VLLM_TARGET_DEVICE"] = "cpu"
env["VLLM_OMNI_INIT_TIMELINE"] = str(case_dir / "timeline.json")
env["VLLM_OMNI_STEP_STATS_DIR"] = str(case_dir / "step_stats")
if inductor:
env["VLLM_OMNI_CPU_INDUCTOR"] = "1"
cmd: list[str] = []
if shutil.which("taskset"):
cmd += ["taskset", "-c", f"{s0[0]}-{s1[-1]}"]
cmd += [
args.python,
str(BENCH_DIR / "stream_latency_bench.py"),
"--model",
args.model,
"--deploy-config",
str(yaml_path),
"--query-type",
"CustomVoice",
"--txt-prompts",
args.prompts,
"--warmup",
"1",
"--repeat",
str(args.repeat),
"--output-dir",
str(case_dir / "bench"),
"--tag",
name,
"--step-stats",
"--init-timeout",
"1500",
"--stage-init-timeout",
"1200",
]
print(
f"[cpu_sweep] {name}: threads={n} stage0={_range_str(s0)} stage1={_range_str(s1)} inductor={inductor}",
flush=True,
)
with open(case_dir / "stdout.log", "w") as log:
try:
rc = subprocess.run(
cmd, env=env, stdout=log, stderr=subprocess.STDOUT, timeout=args.timeout_s, cwd=str(REPO_ROOT)
).returncode
except subprocess.TimeoutExpired:
rc = -1
row: dict[str, Any] = {
"case": name,
"threads": n,
"stage0_cpus": _range_str(s0),
"stage1_cpus": _range_str(s1),
"inductor": inductor,
"returncode": rc,
"cmd": cmd,
}
files = sorted((case_dir / "bench").rglob("*.json"))
if files:
doc = json.loads(files[-1].read_text())
s = doc["summary"]
row.update(
init_s=doc["init"]["init_s"],
ttfa_p50=s["ttfa_ms"]["p50"],
playback_start_p50=s["playback_start_ms"]["p50"],
rtf_total_p50=s["rtf_total"]["p50"],
peak_rss_gib=doc["resources"]["peak_rss_bytes"] / GiB,
step_stats=(doc.get("step_stats") or {}).get("attribution", [])[:8],
)
print(f"[cpu_sweep] {name}: rc={rc} ttfa_p50={row.get('ttfa_p50')} rtf={row.get('rtf_total_p50')}", flush=True)
return row
def render_table(rows: list[dict[str, Any]]) -> str:
cols = [
"case",
"threads",
"inductor",
"rc",
"init_s",
"ttfa_p50",
"playback_start_p50",
"rtf_total_p50",
"peak_rss_gib",
]
out = ["| " + " | ".join(cols) + " |", "|" + "---|" * len(cols)]
for r in rows:
vals = []
for c in cols:
v = r.get("returncode") if c == "rc" else r.get(c)
vals.append(f"{v:.2f}" if isinstance(v, float) else ("" if v is None else str(v)))
out.append("| " + " | ".join(vals) + " |")
return "\n".join(out)
def main(argv: list[str] | None = None) -> int:
p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
p.add_argument("--threads", default="8,16,32")
p.add_argument("--inductor-at", type=int, default=16, help="thread count for the inductor variant (0 = skip)")
p.add_argument("--cpu-base", type=int, default=0, help="first core of the contiguous range to pin to")
p.add_argument("--stage0-share", type=float, default=0.75)
p.add_argument("--model", default="Qwen/Qwen3-TTS-12Hz-0.6B-CustomVoice")
p.add_argument("--base-yaml", default=str(REPO_ROOT / "vllm_omni" / "deploy" / "qwen3_tts.yaml"))
p.add_argument("--prompts", default=str(BENCH_DIR / "prompts_12.txt"))
p.add_argument("--repeat", type=int, default=1)
p.add_argument("--timeout-s", type=int, default=2400)
p.add_argument("--output-dir", default=str(BENCH_DIR / "edge_results" / "cpu_sweep"))
p.add_argument("--python", default=sys.executable)
args = p.parse_args(argv)
out_dir = Path(args.output_dir) / _dt.datetime.now().strftime("%Y%m%d-%H%M%S")
out_dir.mkdir(parents=True, exist_ok=True)
rows = []
for n in [int(x) for x in args.threads.split(",") if x.strip()]:
rows.append(run_one(f"cpu{n}", n, False, args, out_dir))
if args.inductor_at and n == args.inductor_at:
rows.append(run_one(f"cpu{n}_inductor", n, True, args, out_dir))
summary = {"model": args.model, "timestamp": out_dir.name, "host_cpu_base": args.cpu_base, "rows": rows}
(out_dir / "summary.json").write_text(json.dumps(summary, indent=1))
table = render_table(rows)
(out_dir / "summary.md").write_text(table + "\n")
print(table)
print(f"[cpu_sweep] wrote {out_dir / 'summary.json'}")
return 0
if __name__ == "__main__":
raise SystemExit(main())