Coverage for orchestrant/benchmark/report.py: 74%
115 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-18 00:48 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-18 00:48 +0000
1"""Summaries, tables and the viewer manifest over speed-run result files.
3Usage:
4 python3 bench_report.py summary <result.json>
5 python3 bench_report.py manifest <dir> <out.json> --title T --model M --generated TS
6 python3 bench_report.py table <dir>
7"""
9import argparse
10import glob
11import json
12import os
13import sys
15from orchestrant.benchmark import correctness, speed_summary
16from orchestrant.benchmark.answers import answered_count, row_answer_s
19def _rows(doc):
20 """Every per-prompt result, errors included, whichever envelope the file uses."""
21 if "results" in doc:
22 return list(doc["results"])
23 return [row for rep in doc.get("reports", []) for row in rep.get("results", [])]
26def _mean(values):
27 return sum(values) / len(values) if values else None
30def summarise(doc):
31 """One line's worth of numbers for a single result file, taken from speed_summary."""
32 rows = _rows(doc)
33 ok = speed_summary.completed(rows)
34 if not ok:
35 return None
36 return {
37 **speed_summary.summarise(rows),
38 # Cut rows have no time to an answer, so the mean travels with its row count.
39 "answer_s": _mean([s for s in map(row_answer_s, ok) if s is not None]),
40 "answered": answered_count(ok),
41 "cpu_percent": _mean([r["cpu_percent"] for r in ok if "cpu_percent" in r]),
42 "ram_used_gb": _mean([r["ram_used_gb"] for r in ok if "ram_used_gb" in r]),
43 "gpu_utilization_percent": _mean(
44 [
45 r["gpu_utilization_percent"]
46 for r in ok
47 if r.get("gpu_utilization_percent") is not None
48 ]
49 ),
50 }
53def result_files(directory):
54 """Result files in `directory`, skipping our `_`-prefixed ones, which have no `results`."""
55 return sorted(
56 f
57 for f in glob.glob(os.path.join(directory, "*.json"))
58 if not os.path.basename(f).startswith("_")
59 )
62def build_manifest(directory, title, model, generated):
63 """Index every result file for the viewer, with a `kind` so each renders as itself."""
64 manifest = {
65 "title": title,
66 "generated": generated,
67 "model": model,
68 "host_hardware": {},
69 "configs": [],
70 }
71 records = []
72 for path in result_files(directory):
73 with open(path) as f:
74 doc = json.load(f)
75 records.append((doc.get("hardware"), doc.get("provenance")))
76 entry = {
77 "label": os.path.basename(path)[:-5],
78 "file": os.path.basename(path),
79 "kind": report_kind(doc),
80 "config": doc.get("config", {}),
81 # The viewer cannot import the probe's table, so older reports get kinds here.
82 "correctness": correctness.annotate(doc.get("correctness")),
83 "results": doc.get("results", []),
84 **run_fields(doc),
85 }
86 if entry["kind"] == "unknown":
87 print(
88 f" WARNING: {path} has neither 'benchmark' nor 'results' — "
89 f"not a report this suite wrote",
90 file=sys.stderr,
91 )
92 if "reports" in doc:
93 # A row without integer passed/total is not a score ("/ = 0%").
94 scored = [
95 {
96 "label": r.get("label"),
97 "model": r.get("model"),
98 "passed": r.get("passed"),
99 "total": r.get("total"),
100 "effective_n": r.get("effective_n"),
101 # Observed passing cases: passed * n / total is wrong for uneven attempts.
102 "effective_k": r.get("effective_k"),
103 "deterministic": r.get("deterministic"),
104 "truncated": r.get("truncated"),
105 "errored": r.get("errored"),
106 "total_wall_s": r.get("total_wall_s"),
107 "median_wall_s": r.get("median_wall_s"),
108 "results": r.get("results", []),
109 }
110 for r in doc["reports"]
111 if is_scored(r)
112 ]
113 unscored = [
114 {k: v for k, v in r.items() if k != "results"}
115 for r in doc["reports"]
116 if not is_scored(r)
117 ]
118 if scored:
119 entry["scored"] = scored
120 if unscored:
121 entry["unscored"] = unscored
122 entry["results"] = [
123 row for r in doc["reports"] for row in r.get("results", [])
124 ]
125 manifest["configs"].append(entry)
126 manifest["host_hardware"] = host_hardware(records)
127 return manifest
130def host_hardware(records):
131 """The Hardware card's record: the first real `hardware` block, else a provenance one."""
132 for which in (0, 1):
133 for record in records:
134 if isinstance(record[which], dict) and record[which]:
135 return record[which]
136 return {}
139def run_fields(doc):
140 """Per-run viewer fields beyond the rows: runtime, energy, threads, timestamp, speed."""
141 provenance = doc.get("provenance")
142 if not isinstance(provenance, dict):
143 provenance = {}
144 hardware = doc.get("hardware")
145 if not isinstance(hardware, dict):
146 hardware = {}
147 return {
148 "backend": doc.get("backend"),
149 "model": doc.get("model"),
150 # The runtime is this endpoint's only: a lanes report records its first lane's.
151 "base_url": provenance.get("base_url") or doc.get("api_url"),
152 "runtime": provenance.get("runtime"),
153 "energy": doc.get("energy"),
154 "cpu_threads": hardware.get("cpu_total_threads"),
155 "timestamp": provenance.get("timestamp_utc") or doc.get("timestamp"),
156 "speed": speed_summary.summarise(_rows(doc)),
157 }
160def _count(value):
161 return isinstance(value, int) and not isinstance(value, bool)
164def is_scored(row):
165 """A report row is a score only when passed AND total are integer counts."""
166 return _count(row.get("passed")) and _count(row.get("total"))
169def report_kind(doc):
170 """'benchmark' (shared envelope), 'throughput' (legacy shape) or 'unknown'."""
171 if doc.get("benchmark"):
172 return doc["benchmark"]
173 return "throughput" if "results" in doc else "unknown"
176def comparison_rows(directory):
177 rows = []
178 for path in result_files(directory):
179 with open(path) as f:
180 doc = json.load(f)
181 s = summarise(doc)
182 if s:
183 rows.append((os.path.basename(path)[:-5], s))
184 return rows
187def _fmt(value, spec, missing=" -"):
188 return format(value, spec) if isinstance(value, (int, float)) else missing
191def _answer(s, spec):
192 """Mean seconds to an answer, with "(k/n)" whenever a reply was cut."""
193 k, n = s.get("answered") or (None, 0)
194 cut = f" ({k}/{n} answered)" if n and k < n else ""
195 return f"{_fmt(s['answer_s'], spec)}s{cut}"
198def summary_line(s):
199 """`report summary`: one run's figures under the speed runner's own names."""
200 return (
201 f" -> Decode: {_fmt(s['decode_tok_s'], '.1f')} tok/s "
202 f"Overall: {_fmt(s['overall_tok_s'], '.1f')} tok/s "
203 f"Answer: {_answer(s, '.1f')} avg "
204 f"CPU: {_fmt(s['cpu_percent'], '.1f')}% "
205 f"RAM: {_fmt(s['ram_used_gb'], '.1f')}GB"
206 + (f" TTFT: {s['ttft_s']:.2f}s avg" if s["ttft_s"] is not None else "")
207 + (
208 f" GPU: {s['gpu_utilization_percent']:.1f}%"
209 if s["gpu_utilization_percent"] is not None
210 else ""
211 )
212 + (f" ERRORS: {s['errored']}" if s["errored"] else "")
213 )
216def table_line(name, s):
217 """`report table`: one result file per line, the figures of summary_line."""
218 return (
219 f" {name:25s} Decode: {_fmt(s['decode_tok_s'], '5.1f')} "
220 f"Overall: {_fmt(s['overall_tok_s'], '5.1f')} "
221 f"TTFT: {_fmt(s['ttft_s'], '5.2f')}s "
222 f"Answer: {_answer(s, '5.1f')} "
223 f"CPU: {_fmt(s['cpu_percent'], '5.1f')}% "
224 f"CT: {s['completion_tokens']:4d} PT: {s['prompt_tokens']:4d}"
225 + (f" ERRORS: {s['errored']}" if s["errored"] else "")
226 )
229def main():
230 ap = argparse.ArgumentParser(
231 description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter
232 )
233 sub = ap.add_subparsers(dest="cmd", required=True)
235 p = sub.add_parser("summary")
236 p.add_argument("file")
237 p = sub.add_parser("manifest")
238 p.add_argument("directory")
239 p.add_argument("out")
240 p.add_argument("--title", default="LLM Benchmark")
241 p.add_argument("--model", default="")
242 p.add_argument("--generated", default="")
243 p = sub.add_parser("table")
244 p.add_argument("directory")
245 args = ap.parse_args()
247 if args.cmd == "summary":
248 with open(args.file) as f:
249 s = summarise(json.load(f))
250 if not s:
251 print(" -> no successful results")
252 return
253 print(summary_line(s))
254 return
256 if args.cmd == "manifest":
257 m = build_manifest(args.directory, args.title, args.model, args.generated)
258 with open(args.out, "w") as f:
259 json.dump(m, f, indent=2)
260 print(f" Manifest: {args.out} ({len(m['configs'])} configs)")
261 return
263 for name, s in comparison_rows(args.directory):
264 print(table_line(name, s))
267if __name__ == "__main__":
268 main()