Summarize vLLM scheduler step profiles
This commit is contained in:
144
runs/frontier-qwen30-vllm020-profile-v1/summarize_opprof.py
Normal file
144
runs/frontier-qwen30-vllm020-profile-v1/summarize_opprof.py
Normal 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()
|
||||
Reference in New Issue
Block a user