#!/usr/bin/env python3 """Extract per-DP-window speculative metrics from a vLLM engine log. vLLM emits an ``Engine NNN`` throughput line followed by the corresponding ``SpecDecoding metrics`` line. This script keeps that adjacency explicit and does not infer request-level outcomes from the aggregate metrics. """ from __future__ import annotations import argparse import json import math import re import statistics from collections import defaultdict from pathlib import Path from typing import Any ENGINE = re.compile( r"INFO (?P\d\d-\d\d \d\d:\d\d:\d\d\.\d+) .*?" r"Engine (?P\d+): .*?" r"Avg decode step duration: (?P[0-9.]+) ms .*?" r"Running: (?P\d+) reqs, Waiting: (?P\d+) reqs" ) SPEC = re.compile( r"INFO (?P\d\d-\d\d \d\d:\d\d:\d\d\.\d+) .*?" r"SpecDecoding metrics: Mean acceptance length: (?P[0-9.]+), .*?" r"Avg Draft acceptance rate: (?P[0-9.]+)%" ) def percentile(values: list[float], q: float) -> float | None: if not values: return None values = sorted(values) index = (len(values) - 1) * q lower, upper = math.floor(index), math.ceil(index) if lower == upper: return values[lower] return values[lower] + (values[upper] - values[lower]) * (index - lower) def describe(values: list[float]) -> dict[str, Any]: return { "n": len(values), "mean": statistics.fmean(values) if values else None, "min": min(values) if values else None, "p50": percentile(values, 0.5), "p95": percentile(values, 0.95), "max": max(values) if values else None, "distinct_value_count": len(set(values)), } def extract(path: Path) -> dict[str, Any]: pending: tuple[str, dict[str, Any]] | None = None rows: list[dict[str, Any]] = [] for line in path.read_text(errors="replace").splitlines(): engine = ENGINE.search(line) if engine: pending = ( engine.group("clock")[:17], { "clock": engine.group("clock"), "engine": int(engine.group("engine")), "decode_ms": float(engine.group("decode_ms")), "running": int(engine.group("running")), "waiting": int(engine.group("waiting")), }, ) continue spec = SPEC.search(line) if not spec or pending is None: continue clock, row = pending if spec.group("clock")[:17] != clock: pending = None continue row["mean_accept_length"] = float(spec.group("mean_accept")) row["accept_pct"] = float(spec.group("accept_pct")) rows.append(row) pending = None by_engine: dict[int, list[dict[str, Any]]] = defaultdict(list) for row in rows: by_engine[row["engine"]].append(row) summaries: list[dict[str, Any]] = [] for engine, items in sorted(by_engine.items()): summaries.append( { "engine": engine, "window_count": len(items), "accept_pct": describe([item["accept_pct"] for item in items]), "mean_accept_length": describe([item["mean_accept_length"] for item in items]), "decode_ms": describe([item["decode_ms"] for item in items]), "running_requests": describe([float(item["running"]) for item in items]), } ) acceptance = [row["accept_pct"] for row in rows] return { "source": str(path), "records": rows, "per_engine": summaries, "data_sanity": { "accept_pct": { "n": len(acceptance), "min": min(acceptance) if acceptance else None, "max": max(acceptance) if acceptance else None, "distinct_value_count": len(set(acceptance)), "within_0_100": all(0 <= item <= 100 for item in acceptance), }, "invariants": { "non_negative_decode_ms": all(row["decode_ms"] >= 0 for row in rows), "non_negative_running": all(row["running"] >= 0 for row in rows), "all_records_have_engine": all("engine" in row for row in rows), }, }, } def main() -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--engine-log", type=Path, required=True) parser.add_argument("--output", type=Path, required=True) args = parser.parse_args() result = extract(args.engine_log) args.output.write_text(json.dumps(result, indent=2, sort_keys=True) + "\n") print(json.dumps({"per_engine": result["per_engine"], "data_sanity": result["data_sanity"]}, indent=2)) return 0 if __name__ == "__main__": raise SystemExit(main())