Summarize vLLM scheduler step profiles

This commit is contained in:
2026-07-16 23:20:04 +08:00
parent 630de9f573
commit 008324e70c

View File

@@ -0,0 +1,144 @@
#!/usr/bin/env python3
"""Summarize prompt-free vLLM scheduler/operator-profiler step records."""
from __future__ import annotations
import argparse
import json
import math
import statistics
from collections import Counter, defaultdict
from pathlib import Path
from typing import Any
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser()
parser.add_argument("--cell-root", type=Path, required=True)
parser.add_argument("--output", type=Path, required=True)
return parser.parse_args()
def percentile(values: list[float], fraction: float) -> float:
ordered = sorted(values)
index = min(len(ordered) - 1, max(0, math.ceil(fraction * len(ordered)) - 1))
return ordered[index]
def stats(values: list[float]) -> dict[str, float]:
return {
"min": min(values),
"mean": statistics.fmean(values),
"p50": percentile(values, 0.50),
"p95": percentile(values, 0.95),
"p99": percentile(values, 0.99),
"max": max(values),
}
def summarize_cell(path: Path) -> dict[str, Any]:
streams = sorted(path.glob("opprof/*.jsonl"))
if len(streams) != 1:
raise ValueError(f"expected one opprof stream in {path}, got {len(streams)}")
groups: dict[tuple[str, str], dict[str, list[float]]] = defaultdict(
lambda: defaultdict(list)
)
phase_counts: Counter[str] = Counter()
graph_counts: Counter[str] = Counter()
prefix_queries = 0
prefix_hits = 0
total_records = 0
dropped_records_max = 0
with streams[0].open() as handle:
for line in handle:
record = json.loads(line)
if not record.get("model_executed", False):
continue
prefill_tokens = int(record.get("prefill_tokens", 0))
decode_tokens = int(record.get("decode_tokens", 0))
if prefill_tokens and decode_tokens:
phase = "true_mixed"
elif prefill_tokens:
phase = "pure_prefill"
elif decode_tokens:
phase = "pure_decode"
else:
phase = "empty"
graph = str((record.get("cudagraph") or {}).get("runtime_mode", "UNKNOWN"))
duration_ms = (
int(record["complete_mono_ns"]) - int(record["submit_mono_ns"])
) / 1e6
if duration_ms < 0:
raise ValueError(f"negative duration in {streams[0]}")
group = groups[(phase, graph)]
group["submit_to_complete_ms"].append(duration_ms)
group["scheduled_requests"].append(int(record["scheduled_requests"]))
group["prefill_tokens"].append(prefill_tokens)
group["decode_tokens"].append(decode_tokens)
group["total_tokens"].append(prefill_tokens + decode_tokens)
group["queue_waiting"].append(int(record["queues"]["waiting"]))
group["kv_usage"].append(float(record["kv"]["usage"]))
local_prefix = (record.get("prefix") or {}).get("local") or {}
prefix_queries += int(local_prefix.get("queries", 0))
prefix_hits += int(local_prefix.get("hits", 0))
dropped_records_max = max(
dropped_records_max, int(record.get("dropped_records_before", 0))
)
phase_counts[phase] += 1
graph_counts[graph] += 1
total_records += 1
if total_records == 0:
raise ValueError(f"empty opprof stream: {streams[0]}")
summarized_groups = []
for (phase, graph), metrics in sorted(groups.items()):
summarized_groups.append(
{
"phase": phase,
"cudagraph_runtime_mode": graph,
"steps": len(metrics["submit_to_complete_ms"]),
**{name: stats(values) for name, values in metrics.items()},
}
)
cell = path.name
tp_text, mns_text = cell.split("_")
return {
"cell": cell,
"tensor_parallel_size": int(tp_text.removeprefix("tp")),
"max_num_seqs": int(mns_text.removeprefix("mns")),
"stream": str(streams[0]),
"records": total_records,
"phase_counts": dict(phase_counts),
"cudagraph_runtime_mode_counts": dict(graph_counts),
"prefix": {
"queries": prefix_queries,
"hits": prefix_hits,
"hit_rate": prefix_hits / prefix_queries if prefix_queries else 0.0,
},
"dropped_records_before_max": dropped_records_max,
"groups": summarized_groups,
}
def main() -> None:
args = parse_args()
cells = [summarize_cell(path) for path in sorted(args.cell_root.glob("tp*_mns*"))]
if not cells:
raise SystemExit(f"no cells found in {args.cell_root}")
payload = {
"schema_version": "qwen30_vllm020_opprof_summary.v1",
"measurement_semantics": (
"complete_mono_ns - submit_mono_ns from the existing vLLM opprof record; "
"this includes the instrumented submit-to-completion interval and is not "
"claimed to be a single-kernel CUDA-event duration"
),
"contains_prompt_text": False,
"cell_root": str(args.cell_root),
"cells": cells,
}
args.output.parent.mkdir(parents=True, exist_ok=True)
args.output.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n")
print(json.dumps({"cells": len(cells), "records": sum(c["records"] for c in cells)}))
if __name__ == "__main__":
main()