mjolnir-cli 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. mjolnir_cli/__init__.py +8 -0
  2. mjolnir_cli/__main__.py +4 -0
  3. mjolnir_cli/accuracy.py +333 -0
  4. mjolnir_cli/autoresearch.py +539 -0
  5. mjolnir_cli/benchy.py +475 -0
  6. mjolnir_cli/bundled/configs/Qwen/Qwen3.8-27B/NVFP4/config.yaml +78 -0
  7. mjolnir_cli/bundled/configs/Qwen/Qwen3.8-27B/NVFP4_DFLASH2/config.yaml +103 -0
  8. mjolnir_cli/bundled/configs/Qwen/Qwen3.8-27B/NVFP4_DSPARK/config.yaml +102 -0
  9. mjolnir_cli/bundled/configs/Qwen/Qwen3.8-27B/NVFP4_FA2/config.yaml +84 -0
  10. mjolnir_cli/bundled/configs/Qwen/Qwen3.8-27B/NVFP4_FA4hd256/config.yaml +104 -0
  11. mjolnir_cli/bundled/configs/Qwen/Qwen3.8-27B/NVFP4_MJOLNIR/config.yaml +86 -0
  12. mjolnir_cli/bundled/configs/Qwen/Qwen3.8-27B/NVFP4_MJOLNIR_E2M3/config.yaml +93 -0
  13. mjolnir_cli/bundled/configs/Qwen/Qwen3.8-27B/NVFP4_MJOLNIR_E2M3_DFLASH2/config.yaml +123 -0
  14. mjolnir_cli/bundled/configs/Qwen/Qwen3.8-27B/NVFP4_MJOLNIR_E2M3_DSPARK/config.yaml +124 -0
  15. mjolnir_cli/bundled/configs/Qwen/Qwen3.8-27B/NVFP4_MJOLNIR_E2M3_LMH8/.env +2 -0
  16. mjolnir_cli/bundled/configs/Qwen/Qwen3.8-27B/NVFP4_MJOLNIR_E2M3_LMH8/config.yaml +98 -0
  17. mjolnir_cli/bundled/configs/Qwen/Qwen3.8-27B/NVFP4_MJOLNIR_E2M3_LMH8_VT32K/.env +4 -0
  18. mjolnir_cli/bundled/configs/Qwen/Qwen3.8-27B/NVFP4_MJOLNIR_E2M3_LMH8_VT32K/config.yaml +98 -0
  19. mjolnir_cli/bundled/configs/Qwen/Qwen3.8-27B/NVFP4_MJOLNIR_E2M3_LMH8_VT64K/.env +4 -0
  20. mjolnir_cli/bundled/configs/Qwen/Qwen3.8-27B/NVFP4_MJOLNIR_E2M3_LMH8_VT64K/config.yaml +102 -0
  21. mjolnir_cli/bundled/configs/Qwen/Qwen3.8-27B/NVFP4_MJOLNIR_E2M3_VT32K/.env +3 -0
  22. mjolnir_cli/bundled/configs/Qwen/Qwen3.8-27B/NVFP4_MJOLNIR_E2M3_VT32K/config.yaml +98 -0
  23. mjolnir_cli/bundled/configs/Qwen/Qwen3.8-27B/draft_vocab.json +1 -0
  24. mjolnir_cli/bundled/configs/Qwen/Qwen3.8-27B/kv_scales.json +35197 -0
  25. mjolnir_cli/bundled/configs/Qwen/Qwen3.8_chat_template.jinja +456 -0
  26. mjolnir_cli/bundled/docker/litellm/docker-compose.yaml +113 -0
  27. mjolnir_cli/bundled/docker/litellm/litellm.env.example +51 -0
  28. mjolnir_cli/bundled/docker/litellm/litellm_config.template.example.yaml +192 -0
  29. mjolnir_cli/bundled/docker/perses/README.md +89 -0
  30. mjolnir_cli/bundled/docker/perses/dashboards/global-datasource.yaml +17 -0
  31. mjolnir_cli/bundled/docker/perses/dashboards/performance_statistics.yaml +771 -0
  32. mjolnir_cli/bundled/docker/perses/dashboards/project.yaml +13 -0
  33. mjolnir_cli/bundled/docker/perses/dashboards/query_statistics.yaml +399 -0
  34. mjolnir_cli/bundled/docker/perses/docker-compose.yaml +51 -0
  35. mjolnir_cli/bundled/docker/perses/perses.yml +16 -0
  36. mjolnir_cli/bundled/docker/perses/prometheus.template.yaml +20 -0
  37. mjolnir_cli/bundled/scripts/hw/setup-thor.sh +1287 -0
  38. mjolnir_cli/cachectl.py +146 -0
  39. mjolnir_cli/charts.py +1687 -0
  40. mjolnir_cli/cli.py +1480 -0
  41. mjolnir_cli/clidoc.py +170 -0
  42. mjolnir_cli/config.py +346 -0
  43. mjolnir_cli/dockerctl.py +533 -0
  44. mjolnir_cli/drivers/fa2_driver.py +89 -0
  45. mjolnir_cli/drivers/fi_driver.py +134 -0
  46. mjolnir_cli/e2e.py +302 -0
  47. mjolnir_cli/fonts/InterDisplay-Bold.ttf +0 -0
  48. mjolnir_cli/fonts/InterDisplay-Medium.ttf +0 -0
  49. mjolnir_cli/fonts/InterDisplay-Regular.ttf +0 -0
  50. mjolnir_cli/fonts/InterDisplay-SemiBold.ttf +0 -0
  51. mjolnir_cli/fonts/OFL.txt +92 -0
  52. mjolnir_cli/gate.py +233 -0
  53. mjolnir_cli/history.py +159 -0
  54. mjolnir_cli/interactive.py +383 -0
  55. mjolnir_cli/legs.py +109 -0
  56. mjolnir_cli/litellm.py +254 -0
  57. mjolnir_cli/perses.py +214 -0
  58. mjolnir_cli/picker.py +70 -0
  59. mjolnir_cli/readme.py +1020 -0
  60. mjolnir_cli/stack.py +340 -0
  61. mjolnir_cli/tasks.py +122 -0
  62. mjolnir_cli/theme.py +281 -0
  63. mjolnir_cli/thor.py +108 -0
  64. mjolnir_cli/verify/__init__.py +2 -0
  65. mjolnir_cli/verify/cli.py +816 -0
  66. mjolnir_cli/verify/docker.py +334 -0
  67. mjolnir_cli/verify/kvcache.py +180 -0
  68. mjolnir_cli/verify/legs.py +134 -0
  69. mjolnir_cli/verify/ncu.py +141 -0
  70. mjolnir_cli/verify/profile.py +312 -0
  71. mjolnir_cli/verify/report.py +177 -0
  72. mjolnir_cli/verify/results.py +157 -0
  73. mjolnir_cli/verify/spec.py +279 -0
  74. mjolnir_cli/verify/stages.py +817 -0
  75. mjolnir_cli-0.2.0.dist-info/METADATA +87 -0
  76. mjolnir_cli-0.2.0.dist-info/RECORD +80 -0
  77. mjolnir_cli-0.2.0.dist-info/WHEEL +4 -0
  78. mjolnir_cli-0.2.0.dist-info/entry_points.txt +2 -0
  79. mjolnir_cli-0.2.0.dist-info/licenses/LICENSE +202 -0
  80. mjolnir_cli-0.2.0.dist-info/licenses/NOTICE +64 -0
@@ -0,0 +1,817 @@
1
+ """The ``mjolnir verify`` stages — a Python port of the old ``verify.sh``.
2
+
3
+ Every stage returns ONE structured result document (``results.py``) that is
4
+ saved under ``benchmarks/raw/verify/<stage>/<run_id>.json`` and printed as a
5
+ terminal table. Charts, README tables and the deep-dive docs are generated
6
+ from those documents only (``mjolnir plot --readme``).
7
+
8
+ Default L axis for every stage: ``ALL_LS`` (128 → 262144 tokens) — the
9
+ kernel has to be fast at every context length, not at a hand-picked one.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import re
15
+ import shlex
16
+ import sys
17
+ import time
18
+ from dataclasses import dataclass, field
19
+ from pathlib import Path
20
+ from typing import Callable
21
+
22
+ from mjolnir_cli.verify import legs as L_
23
+ from mjolnir_cli.legs import KERNEL, SMOKE_MODES
24
+ from mjolnir_cli.verify import results
25
+ from mjolnir_cli.verify.docker import Runner
26
+
27
+ ALL_LS: tuple[int, ...] = (128, 4096, 8192, 16384, 32768, 65536, 131072, 262144)
28
+ DEFAULT_ITERS = 50
29
+ # Run-to-run noise of a SAME-RUN ratio (reference time ÷ variant time) on a
30
+ # dedicated Thor (no live server, ncu base clocks, cold L2). Measured from the
31
+ # 2026-10-11 bench: the same FlashInfer/Mjolnir pair repeated in 4-5 stage runs
32
+ # gives a per-L ratio cv of 0.6-2.6% (median 1.25%), so ±3% is ~2.3σ of a
33
+ # typical cell. Absolute µs drift more (~10% between runs at long L, both legs
34
+ # together), so only ever compare legs measured in the same run. L=16K is
35
+ # bimodal (FI/Mjolnir 0.98 or 1.14 depending on the run) — never judge a change
36
+ # on 16K alone. Was 7% while the GPU was shared with a live vLLM server.
37
+ NOISE_PCT = 3.0
38
+
39
+ _MEDIAN = re.compile(r"^median ([0-9.]+) us(?:, ([0-9.e+]+) GB/s)?", re.M)
40
+
41
+
42
+ @dataclass
43
+ class Ctx:
44
+ """What every stage gets: the runner, where to write, how to gate."""
45
+
46
+ runner: Runner
47
+ run_id: str
48
+ logdir: Path
49
+ metrics_url: str
50
+ gate: bool = True
51
+ gate_confirm: int = 3
52
+ gate_timeout: float = 3600.0
53
+ ls: tuple[int, ...] = ALL_LS
54
+ iters: int = DEFAULT_ITERS
55
+ out: Callable[[str], None] = print
56
+ gate_log: dict = field(default_factory=lambda: {"gated": False, "clean": None, "server": "unknown"})
57
+
58
+ def wait_clean(self) -> None:
59
+ """Clean-window gate before a measurement: waits only if the live
60
+ vLLM server is reachable; an offline server = nothing to gate against."""
61
+ if not self.gate or self.runner.dry_run:
62
+ return
63
+ from mjolnir_cli.gate import server_load, wait_for_idle
64
+
65
+ load = server_load(self.metrics_url)
66
+ if load is None:
67
+ self.gate_log.update(server="offline", gated=False)
68
+ return
69
+ self.gate_log.update(server="up", gated=True)
70
+ if load != (0.0, 0.0):
71
+ self.out(f" … waiting for a clean window (running={load[0]:g} waiting={load[1]:g})")
72
+ ts = wait_for_idle(self.metrics_url, confirm=self.gate_confirm, timeout_s=self.gate_timeout)
73
+ self.gate_log["clean"] = ts is not None and self.gate_log.get("clean") is not False
74
+
75
+
76
+ @dataclass(frozen=True)
77
+ class Stage:
78
+ name: str
79
+ title: str
80
+ kind: str # "correctness" | "ncu" | "harness" | "profile" | "tests" | "serving"
81
+ summary: str # one line (help text, docs index)
82
+ fn: Callable[..., dict] | None = None
83
+
84
+
85
+ def _median(text: str) -> tuple[float | None, float | None]:
86
+ m = _MEDIAN.search(text or "")
87
+ if not m:
88
+ return None, None
89
+ return float(m.group(1)), (float(m.group(2)) if m.group(2) else None)
90
+
91
+
92
+ # ── ncu sweeps (sweep / hndsweep / ring / prefetch / ab / kvfmt) ────────────
93
+
94
+
95
+ def run_series(ctx: Ctx, legs: list[L_.Leg], stage: str, cache: str = "all", clock: str = "base") -> list[dict]:
96
+ """Profile every leg at every L; one series dict per leg."""
97
+ series = []
98
+ for leg in legs:
99
+ pts = []
100
+ ctx.out(f" {leg.label} [{leg.describe()}]")
101
+ for L in ctx.ls:
102
+ ctx.wait_clean()
103
+ log = ctx.logdir / stage / f"{_slug(leg.id)}-L{L}.txt"
104
+ rc, pt = ctx.runner.ncu_leg(log, leg, L, ctx.iters, cache=cache, clock=clock)
105
+ if pt:
106
+ pts.append(pt)
107
+ ctx.out(f" L={L:>7,} {pt['total_us']:>10.1f} µs {pt['gbps']:>6.1f} GB/s")
108
+ elif not ctx.runner.dry_run:
109
+ ctx.out(f" L={L:>7,} FAILED (exit {rc}) — {log}")
110
+ series.append(
111
+ {
112
+ "id": leg.id,
113
+ "label": leg.label,
114
+ "kind": leg.kind,
115
+ "role": leg.role,
116
+ "flags": leg.flags,
117
+ "env": dict(leg.env),
118
+ "q": leg.q,
119
+ "kv_format": leg.kv_format,
120
+ "kv_layout": leg.kv_layout,
121
+ "kv_bits": leg.kv_bits,
122
+ "points": pts,
123
+ }
124
+ )
125
+ return series
126
+
127
+
128
+ def _wall_rows(ctx: Ctx, legs: list[L_.Leg], stage: str, iters: int, ls: tuple[int, ...]) -> list[dict]:
129
+ """Harness wall-clock medians (cold L2, b=1) — includes the launch gap a
130
+ separate split-KV merge kernel costs, which ncu cannot see."""
131
+ rows = []
132
+ for L in ls:
133
+ for leg in legs:
134
+ if leg.kind != "t5":
135
+ continue
136
+ ctx.wait_clean()
137
+ log = ctx.logdir / stage / "wall" / f"{_slug(leg.id)}-L{L}.log"
138
+ args = ["--seqlen", str(L), "--skip_ref_check", "--warmup_iterations", "10", "--iterations", str(iters)]
139
+ rc, text = ctx.runner.harness(log, leg.q, args + shlex.split(leg.flags))
140
+ us, gbps = _median(text)
141
+ rows.append({"L": L, "series": leg.id, "median_us": us, "gbps": gbps, "ok": rc == 0 and us is not None})
142
+ return rows
143
+
144
+
145
+ def _slug(s: str) -> str:
146
+ return re.sub(r"[^A-Za-z0-9_.=-]+", "_", s).strip("_")
147
+
148
+
149
+ SWEEP_LEGS = KERNEL["verify.sweep"] # mjolnir_cli.legs
150
+
151
+
152
+ def stage_sweep(ctx: Ctx, legs: str = SWEEP_LEGS, page: int | None = None, batch: int = 1, q_len: int = 1) -> dict:
153
+ """Every leg at every L. ``batch`` sequences of L tokens with ``q_len``
154
+ query tokens each (default one decode token of one sequence; serving with
155
+ MTP=3 verifies 4 tokens per sequence: ``batch=4, q_len=4``)."""
156
+ sel = L_.legs_from_ids(legs)
157
+ if (batch, q_len) != (1, 1) and any(x.kind == "fa2" for x in sel):
158
+ # the FA2 driver only times one decode token of one sequence
159
+ if legs != SWEEP_LEGS:
160
+ raise ValueError("the fa2 leg measures b=1, q_len=1 only — drop it from --legs for --batch/--q-len")
161
+ sel = [x for x in sel if x.kind != "fa2"]
162
+ ctx.out(f" (fa2 leg skipped: it measures b=1, q_len=1 only — legs {' '.join(x.id for x in sel)})")
163
+ ctx.runner.batch, ctx.runner.q_len = batch, q_len
164
+ params: dict = {"legs": [x.id for x in sel]}
165
+ if (batch, q_len) != (1, 1):
166
+ params.update(batch=batch, q_len=q_len)
167
+ return {"series": run_series(ctx, sel, "sweep"), "params": params}
168
+
169
+
170
+ def stage_hndsweep(ctx: Ctx, legs: str = KERNEL["verify.hndsweep"]) -> dict:
171
+ sel = L_.legs_from_ids(legs)
172
+ return {"series": run_series(ctx, sel, "hndsweep"), "params": {"legs": [x.id for x in sel]}}
173
+
174
+
175
+ def _variant_stage(
176
+ ctx: Ctx, stage: str, variants: list[L_.Leg], refs: str, wall_iters: int = 0, wall_ls: tuple[int, ...] = ()
177
+ ) -> dict:
178
+ ref_legs = L_.legs_from_ids(refs) if refs else []
179
+ series = run_series(ctx, ref_legs + variants, stage)
180
+ doc = {"series": series, "params": {"variants": [(v.id, v.flags) for v in variants], "refs": refs}}
181
+ if wall_iters:
182
+ ctx.out(" wall clock (harness median, cold L2, b=1) — includes launch gaps")
183
+ doc["rows"] = _wall_rows(ctx, variants, stage, wall_iters, wall_ls or ctx.ls)
184
+ doc["params"]["wall_iters"] = wall_iters
185
+ return doc
186
+
187
+
188
+ RING_DEFAULT = "2 3 4 5 6"
189
+
190
+
191
+ def stage_ring(ctx: Ctx, stages: str = RING_DEFAULT, base: str = "", refs: str = "fi") -> dict:
192
+ """KV ring depth (``--kv_stages``) at the harness default."""
193
+ variants = [L_.t5_variant(f"stages={s}", f"{base} --kv_stages {s}".strip()) for s in stages.split()]
194
+ variants.insert(0, L_.t5_variant("default", base))
195
+ return _variant_stage(ctx, "ring", variants, refs)
196
+
197
+
198
+ def stage_prefetch(ctx: Ctx, distances: str = "0 1 2 4", base: str = "--kv_format fp8", refs: str = "fi") -> dict:
199
+ """L2 prefetch distance (``--l2_prefetch``, in 128-row tiles ahead).
200
+ Runs on fp8 KV: the prefetch path is not wired for nvfp4 and the fp6
201
+ ring is already tuned without it."""
202
+ variants = [L_.t5_variant(f"prefetch={k}", f"{base} --l2_prefetch {k}".strip()) for k in distances.split()]
203
+ return _variant_stage(ctx, "prefetch", variants, refs)
204
+
205
+
206
+ AB_DEFAULT = (
207
+ "base:--kv_evict_first 0|evict:--kv_evict_first 1|pdl:--kv_evict_first 0 --pdl|both:--kv_evict_first 1 --pdl"
208
+ )
209
+
210
+
211
+ def stage_ab(ctx: Ctx, variants: str = AB_DEFAULT, refs: str = "fi", wall_iters: int = 200) -> dict:
212
+ """Free-form A/B of harness flag sets (``name:flags|name:flags``)."""
213
+ return _variant_stage(ctx, "ab", L_.parse_variants(variants), refs, wall_iters)
214
+
215
+
216
+ KVFMT_DEFAULT = (
217
+ "fp8-nhd:--kv_format fp8 --kv_layout nhd|fp8-interleaved:--kv_format fp8 --kv_layout interleaved|"
218
+ "fp8-hnd:--kv_format fp8 --kv_layout hnd|e2m3-hnd:--kv_format e2m3 --kv_layout hnd|"
219
+ "e3m2-hnd:--kv_format e3m2 --kv_layout hnd|e2m1-hnd:--kv_format e2m1 --kv_layout hnd|"
220
+ "nvfp4-hnd:--kv_format nvfp4 --kv_layout hnd"
221
+ )
222
+
223
+
224
+ def stage_kvfmt(
225
+ ctx: Ctx, variants: str = KVFMT_DEFAULT, refs: str = KERNEL["verify.kvfmt.refs"], wall_iters: int = 200
226
+ ) -> dict:
227
+ """KV cache layouts × formats (fp8 / fp6 / fp4 / nvfp4; NHD / interleaved / HND)."""
228
+ return _variant_stage(ctx, "kvfmt", L_.parse_variants(variants), refs, wall_iters)
229
+
230
+
231
+ # ── harness stages (timing / pdl / quant) ───────────────────────────────────
232
+
233
+
234
+ def stage_timing(ctx: Ctx, dtypes: str = "fp8 bf16", ctas: str = "1 2", iters: int = 50) -> dict:
235
+ """Harness-timed medians at every L, 1 vs 2 CTAs/SM, per Q dtype."""
236
+ rows = []
237
+ for dt in dtypes.split():
238
+ for L in ctx.ls:
239
+ for c in ctas.split():
240
+ ctx.wait_clean()
241
+ log = ctx.logdir / "timing" / f"{dt}-L{L}-ctas{c}.log"
242
+ args = f"--seqlen {L} --skip_ref_check --warmup_iterations 5 --iterations {iters} --ctas_per_sm {c}"
243
+ rc, text = ctx.runner.harness(log, dt, args)
244
+ us, gbps = _median(text)
245
+ rows.append({"q": dt, "L": L, "ctas_per_sm": int(c), "median_us": us, "gbps": gbps, "ok": rc == 0})
246
+ if us is not None:
247
+ ctx.out(f" {dt:<5} L={L:>7,} ctas/SM={c} {us:>10.1f} µs")
248
+ return {"rows": rows, "params": {"dtypes": dtypes, "ctas": ctas, "iters": iters}}
249
+
250
+
251
+ def stage_pdl(ctx: Ctx, batches: str = "1 8", dtypes: str = "fp8", iters: int = 200) -> dict:
252
+ """Programmatic dependent launch on/off (wall clock — ncu serializes
253
+ kernels, so it cannot see PDL)."""
254
+ rows = []
255
+ for dt in dtypes.split():
256
+ for b in batches.split():
257
+ for L in ctx.ls:
258
+ base = f"--batches {b} --seqlen {L} --skip_ref_check --warmup_iterations 10 --iterations {iters}"
259
+ if int(b) > 1:
260
+ base += " --varlen"
261
+ out = {}
262
+ for mode, extra in (("off", ""), ("on", " --pdl")):
263
+ ctx.wait_clean()
264
+ log = ctx.logdir / "pdl" / f"{dt}-b{b}-L{L}-{mode}.log"
265
+ _, text = ctx.runner.harness(log, dt, base + extra)
266
+ out[mode] = _median(text)[0]
267
+ d = (out["on"] - out["off"]) / out["off"] * 100 if out["on"] and out["off"] else None
268
+ rows.append(
269
+ {"q": dt, "batches": int(b), "L": L, "off_us": out["off"], "on_us": out["on"], "delta_pct": d}
270
+ )
271
+ if d is not None:
272
+ ctx.out(f" {dt} b={b} L={L:>7,} off {out['off']:.1f} µs on {out['on']:.1f} µs ({d:+.1f}%)")
273
+ return {"rows": rows, "params": {"batches": batches, "dtypes": dtypes, "iters": iters}}
274
+
275
+
276
+ QUANT_FORMATS = "fp8 e2m3 e3m2 e2m1 nvfp4"
277
+
278
+
279
+ def stage_quant(ctx: Ctx, formats: str = QUANT_FORMATS, batches: int = 2) -> dict:
280
+ """Accuracy of each KV format: kernel output vs attention over the
281
+ unquantized float KV (same queries), relative L2 error."""
282
+ rows = []
283
+ for f in formats.split():
284
+ for L in ctx.ls:
285
+ log = ctx.logdir / "quant" / f"{f}-L{L}.log"
286
+ extra = "--kv_format fp8 --kv_from_float" if f == "fp8" else f"--kv_format {f}"
287
+ rc, text = ctx.runner.harness(log, "fp8", f"--batches {batches} --seqlen {L} {extra}", timeout=900)
288
+ m = re.search(r"rel_l2=([0-9.eE+-]+)", text)
289
+ mx = re.search(r"unquantized\| max=([0-9.eE+-]+)", text)
290
+ rel = float(m.group(1)) if m else None
291
+ rows.append(
292
+ {
293
+ "format": f,
294
+ "bits": L_.KV_BITS.get(f),
295
+ "L": L,
296
+ "rel_l2": rel,
297
+ "max_abs": float(mx.group(1)) if mx else None,
298
+ "emulator_pass": bool(re.search(r"^PASS$", text, re.M)),
299
+ "ok": rc == 0 and rel is not None,
300
+ }
301
+ )
302
+ if rel is not None:
303
+ ctx.out(f" {f:<6} L={L:>7,} rel L2 {rel:.4f}")
304
+ return {"rows": rows, "params": {"formats": formats, "batches": batches}}
305
+
306
+
307
+ # ── fp6prof (full ncu profiles) ─────────────────────────────────────────────
308
+
309
+ PROF_DEFAULT = (
310
+ "fp8-hnd:--kv_format fp8 --kv_layout hnd|e2m3-hnd:--kv_format e2m3 --kv_layout hnd|"
311
+ "e2m1-hnd:--kv_format e2m1 --kv_layout hnd"
312
+ )
313
+ # The metrics kept per profile (the full set is ~1000 metrics x launches —
314
+ # the .ncu-rep files keep everything; the JSON keeps what the charts read).
315
+ PROF_METRICS = (
316
+ "gpu__time_duration.sum",
317
+ "lts__t_sectors.sum",
318
+ "lts__t_sectors_op_read.sum",
319
+ "lts__t_requests_srcunit_tex_op_read.sum",
320
+ "lts__t_sector_hit_rate.pct",
321
+ "gpu__compute_memory_throughput.avg.pct_of_peak_sustained_elapsed",
322
+ "lts__throughput.avg.pct_of_peak_sustained_elapsed",
323
+ "sm__throughput.avg.pct_of_peak_sustained_elapsed",
324
+ "l1tex__throughput.avg.pct_of_peak_sustained_active",
325
+ "sm__warps_active.avg.pct_of_peak_sustained_active",
326
+ "sm__inst_executed.sum",
327
+ "smsp__cycles_active.avg",
328
+ "launch__registers_per_thread",
329
+ "launch__shared_mem_per_block_dynamic",
330
+ "launch__grid_size",
331
+ )
332
+
333
+
334
+ def stage_fp6prof(ctx: Ctx, variants: str = PROF_DEFAULT, count: int = 3, section_set: str = "full") -> dict:
335
+ """Full ncu profiles of the decode kernel per KV format: bytes moved vs
336
+ time, L2 request granularity, warp stalls — why fp6 saves fewer µs than
337
+ bytes. ``.ncu-rep`` files land in the run's log dir (open in Nsight Compute)."""
338
+ profiles = []
339
+ for leg in L_.parse_variants(variants):
340
+ for L in ctx.ls:
341
+ ctx.wait_clean()
342
+ name = f"{_slug(leg.id)}-L{L}"
343
+ ctx.out(f" {leg.label} L={L:,}")
344
+ rc, prof = ctx.runner.ncu_profile(ctx.logdir / "fp6prof", name, leg, L, count, section_set)
345
+ if rc != 0 or not prof:
346
+ profiles.append({"series": leg.id, "label": leg.label, "L": L, "ok": False})
347
+ continue
348
+ raw = prof["raw"]
349
+ keep = {k: raw[k] for k in PROF_METRICS if k in raw}
350
+ stalls = {
351
+ k.replace("smsp__average_warps_issue_stalled_", "").replace("_per_issue_active.ratio", ""): v
352
+ for k, v in raw.items()
353
+ if k.startswith("smsp__average_warps_issue_stalled_") and k.endswith("_per_issue_active.ratio")
354
+ }
355
+ pipes = {
356
+ k: v
357
+ for k, v in raw.items()
358
+ if re.search(r"pipe_(tc|tensor|tma|uniform)|tmem|utc", k) and "pct_of_peak" in k
359
+ }
360
+ profiles.append(
361
+ {
362
+ "series": leg.id,
363
+ "label": leg.label,
364
+ "flags": leg.flags,
365
+ "kv_format": leg.kv_format,
366
+ "kv_bits": leg.kv_bits,
367
+ "L": L,
368
+ "ok": True,
369
+ "metrics": keep,
370
+ "stalls": stalls,
371
+ "pipes": pipes,
372
+ "details": prof["details"],
373
+ "report": str(ctx.logdir / "fp6prof" / f"{name}.ncu-rep"),
374
+ }
375
+ )
376
+ return {"profiles": profiles, "params": {"variants": variants, "count": count, "set": section_set}}
377
+
378
+
379
+ # ── correctness ─────────────────────────────────────────────────────────────
380
+
381
+ # (label, args, q dtypes). Cases pin the vLLM production config (fp8 KV, NHD)
382
+ # unless they pass their own --kv_format / --kv_layout (later flags win);
383
+ # "default:" cases run the bare harness default (e2m3 on HND for fp8 Q).
384
+ _PIN = "--kv_format fp8 --kv_layout nhd"
385
+ BOTH = ("fp8", "bf16")
386
+ FP8 = ("fp8",)
387
+ CASES: list[tuple[str, str, tuple[str, ...]]] = [
388
+ ("L=8192", "--seqlen 8192", BOTH),
389
+ ("L=128 (1 chunk, direct out)", "--seqlen 128", BOTH),
390
+ ("L=1000 (partial tile mask)", "--seqlen 1000", BOTH),
391
+ ("L=1 (minimal)", "--seqlen 1", BOTH),
392
+ ("L=65536 (ring wraps)", "--seqlen 65536", BOTH),
393
+ ("varlen 6x<=20000", "--batches 6 --seqlen 20000 --varlen", BOTH),
394
+ ("varlen 32x<=2048 (>1 wave)", "--batches 32 --seqlen 2048 --varlen", BOTH),
395
+ ("spec q_len=2 varlen 4x<=3000", "--batches 4 --seqlen 3000 --varlen --q_len 2", BOTH),
396
+ ("spec q_len=4 2x777", "--batches 2 --seqlen 777 --q_len 4", BOTH),
397
+ ("page 16 varlen", "--batches 4 --seqlen 1000 --varlen --page_size 16", BOTH),
398
+ ("page 64 varlen", "--batches 4 --seqlen 1000 --varlen --page_size 64", BOTH),
399
+ ("contiguous pages", "--seqlen 8192 --contiguous_pages", BOTH),
400
+ ("2 CTAs/SM (40-CTA plan)", "--seqlen 8192 --ctas_per_sm 2", BOTH),
401
+ ("forced 7 chunks", "--seqlen 8192 --kv_splits 7", BOTH),
402
+ ("padded work list (40)", "--batches 3 --seqlen 5000 --varlen --pad_work_to 40", BOTH),
403
+ ("max_splits=1 (no reduction)", "--seqlen 8192 --max_splits 1", BOTH),
404
+ ("headdim 128", "--seqlen 8192 --headdim 128", BOTH),
405
+ ("KV interleaved (B,H,N,2D)", "--batches 4 --seqlen 3000 --varlen --page_size 16 --kv_layout interleaved", BOTH),
406
+ ("KV flashinfer (B,2,N,H,D)", "--batches 4 --seqlen 3000 --varlen --page_size 32 --kv_layout flashinfer", BOTH),
407
+ ("strided Q/O (fused QKV view)", "--batches 4 --seqlen 3000 --varlen --strided_qo", BOTH),
408
+ ("per-tensor descales", "--batches 4 --seqlen 3000 --varlen --descale per_tensor", BOTH),
409
+ ("per-head descales", "--batches 4 --seqlen 3000 --varlen --descale per_head", BOTH),
410
+ (
411
+ "vLLM-like: interl+strided+tensor p16",
412
+ "--batches 8 --seqlen 6000 --varlen --page_size 16 --kv_layout interleaved --strided_qo --descale per_tensor",
413
+ BOTH,
414
+ ),
415
+ ("PDL (decode + reduction)", "--batches 4 --seqlen 20000 --varlen --pdl", BOTH),
416
+ ("L2 prefetch 2, page 128", "--batches 2 --seqlen 30000 --varlen --l2_prefetch 2", BOTH),
417
+ ("L2 prefetch 4, page 16", "--batches 4 --seqlen 9000 --varlen --page_size 16 --l2_prefetch 4", BOTH),
418
+ ("PDL + L2 prefetch 1, q_len=2", "--batches 3 --seqlen 5000 --varlen --q_len 2 --pdl --l2_prefetch 1", BOTH),
419
+ ("HND layout p128", "--seqlen 8192 --kv_layout hnd", BOTH),
420
+ ("HND layout p16 varlen", "--batches 4 --seqlen 3000 --varlen --page_size 16 --kv_layout hnd", BOTH),
421
+ ("fp8 from float (accuracy ref)", "--batches 2 --seqlen 5000 --varlen --kv_from_float", FP8),
422
+ ("e3m2 KV p128", "--seqlen 8192 --kv_format e3m2", FP8),
423
+ ("e2m3 KV p16 varlen", "--batches 4 --seqlen 3000 --varlen --page_size 16 --kv_format e2m3", FP8),
424
+ (
425
+ "e2m3 KV vLLM layout (interl p128 q_len=4)",
426
+ "--batches 4 --seqlen 5000 --varlen --page_size 128 --q_len 4 --kv_format e2m3 --kv_layout interleaved "
427
+ "--strided_qo",
428
+ FP8,
429
+ ),
430
+ ("e2m1 KV p128", "--seqlen 8192 --kv_format e2m1", FP8),
431
+ (
432
+ "e2m1 KV HND p16 q_len=2",
433
+ "--batches 3 --seqlen 2500 --varlen --page_size 16 --q_len 2 --kv_layout hnd --kv_format e2m1",
434
+ FP8,
435
+ ),
436
+ ("nvfp4 KV p128", "--seqlen 8192 --kv_format nvfp4", FP8),
437
+ ("nvfp4 KV p16 varlen", "--batches 4 --seqlen 3000 --varlen --page_size 16 --kv_format nvfp4", FP8),
438
+ (
439
+ "nvfp4 KV HND q_len=2 pad",
440
+ "--batches 3 --seqlen 2500 --varlen --q_len 2 --kv_layout hnd --kv_format nvfp4 --pad_work_to 40",
441
+ FP8,
442
+ ),
443
+ ("default: e2m3 HND L=8192", "@default --seqlen 8192", BOTH),
444
+ ("default: L=65536 (ring wraps)", "@default --seqlen 65536", BOTH),
445
+ ("default: varlen 6x<=20000", "@default --batches 6 --seqlen 20000 --varlen", BOTH),
446
+ (
447
+ "default: q_len=3 p16 pad",
448
+ "@default --batches 4 --seqlen 2000 --varlen --q_len 3 --page_size 16 --pad_work_to 40",
449
+ BOTH,
450
+ ),
451
+ ("default: headdim 128 + PDL", "@default --batches 2 --seqlen 9000 --varlen --headdim 128 --pdl", BOTH),
452
+ # parallel drafters (DSpark / DFlash): a non-causal block of 1 + 7 queries,
453
+ # the DSpark drafter's heads (40 query / 8 KV, d = 128)
454
+ (
455
+ "non-causal q_len=8 (drafter block)",
456
+ "@default --batches 4 --seqlen 5000 --varlen --q_len 8 --heads_q 40 --heads_k 8 --headdim 128 --non_causal",
457
+ BOTH,
458
+ ),
459
+ (
460
+ "non-causal q_len=8 fp8 p16 pad",
461
+ "--batches 3 --seqlen 2500 --varlen --q_len 8 --heads_q 40 --heads_k 8 --headdim 128 --page_size 16 "
462
+ "--non_causal --pad_work_to 40",
463
+ BOTH,
464
+ ),
465
+ ("ring depth 2, fp8 (ring wraps)", "--seqlen 65536 --kv_layout hnd --kv_stages 2", BOTH),
466
+ ("ring depth 2, e2m3 varlen p16", "@default --batches 4 --seqlen 9000 --varlen --page_size 16 --kv_stages 2", FP8),
467
+ ("kv evict_first", "--batches 4 --seqlen 20000 --varlen --kv_evict_first", BOTH),
468
+ ("evict + PDL L=8192", "--seqlen 8192 --kv_evict_first --pdl", BOTH),
469
+ (
470
+ "evict + PDL q_len=3 + pad",
471
+ "--batches 4 --seqlen 2000 --varlen --q_len 3 --kv_evict_first --pdl --pad_work_to 40",
472
+ BOTH,
473
+ ),
474
+ (
475
+ "evict + PDL, interl p16",
476
+ "--batches 4 --seqlen 9000 --varlen --page_size 16 --kv_layout interleaved --kv_evict_first --pdl",
477
+ BOTH,
478
+ ),
479
+ (
480
+ "vLLM-like spec q_len=3 p16",
481
+ "--batches 4 --seqlen 2000 --varlen --q_len 3 --page_size 16 --kv_layout interleaved --strided_qo "
482
+ "--descale per_tensor --pad_work_to 40",
483
+ BOTH,
484
+ ),
485
+ ]
486
+
487
+
488
+ # One representative case per group: the fast pre-check autoresearch runs on
489
+ # every experiment (~10 containers) before the full matrix is worth paying for.
490
+ QUICK_CASES = (
491
+ "L=8192",
492
+ "L=1000 (partial tile mask)",
493
+ "L=65536 (ring wraps)",
494
+ "varlen 6x<=20000",
495
+ "spec q_len=2 varlen 4x<=3000",
496
+ "page 16 varlen",
497
+ "KV interleaved (B,H,N,2D)",
498
+ "HND layout p16 varlen",
499
+ "e2m3 KV p16 varlen",
500
+ "default: e2m3 HND L=8192",
501
+ "default: varlen 6x<=20000",
502
+ "evict + PDL q_len=3 + pad",
503
+ "non-causal q_len=8 (drafter block)",
504
+ )
505
+
506
+
507
+ def case_args(args: str) -> str:
508
+ if args.startswith("@default"):
509
+ return args[len("@default") :].strip()
510
+ return f"{_PIN} {args}"
511
+
512
+
513
+ LABEL_W = max(len(label) for label, _, _ in CASES)
514
+
515
+
516
+ def _short_check(check: str | None) -> str:
517
+ """'|kern-emu| max=0.03051 mean=0.0007534' -> 'max 3.05e-02 mean 7.53e-04'
518
+ (fixed width, so the live correctness table never wraps)."""
519
+ if not check:
520
+ return ""
521
+ m = re.search(r"max=([0-9.eE+-]+)\s+mean=([0-9.eE+-]+)", check)
522
+ if not m:
523
+ return check[:26]
524
+ try:
525
+ return f"max {float(m.group(1)):.2e} mean {float(m.group(2)):.2e}"
526
+ except ValueError:
527
+ return check[:26]
528
+
529
+
530
+ def stage_correctness(
531
+ ctx: Ctx,
532
+ dtypes: str = "fp8 bf16",
533
+ match: str = "",
534
+ fail_fast: bool = False,
535
+ timeout: float = 180.0,
536
+ quick: bool = False,
537
+ ) -> dict:
538
+ """Kernel vs the bit-faithful emulator: output + LSE + poisoned-page NaN
539
+ check, for every case x Q dtype."""
540
+ want = set(dtypes.split())
541
+ cases = []
542
+ for label, args, dts in CASES:
543
+ if match and match.lower() not in label.lower():
544
+ continue
545
+ if quick and label not in QUICK_CASES:
546
+ continue
547
+ for dt in dts:
548
+ if dt not in want:
549
+ continue
550
+ log = ctx.logdir / "correctness" / f"{len(cases) + 1:02d}-{dt}-{_slug(label)}.log"
551
+ t0 = time.time()
552
+ rc, text = ctx.runner.harness(log, dt, case_args(args), timeout=timeout)
553
+ ok = rc == 0 and bool(re.search(r"^PASS$", text, re.M))
554
+ check = re.search(r"^check: (.*?)(;|$)", text, re.M)
555
+ plan = re.search(r"plan: SplitPlan\((.*)\)", text)
556
+ c = {
557
+ "label": label,
558
+ "q": dt,
559
+ "args": case_args(args),
560
+ "passed": ok or ctx.runner.dry_run,
561
+ "rc": rc,
562
+ "seconds": round(time.time() - t0, 1),
563
+ "check": check.group(1) if check else None,
564
+ "plan": plan.group(1) if plan else None,
565
+ "log": str(log),
566
+ }
567
+ cases.append(c)
568
+ mark = "✅" if c["passed"] else "❌"
569
+ ctx.out(f" {mark} {dt:<5} {label:<{LABEL_W}} {_short_check(c['check']):<26} {c['seconds']:>4.0f}s")
570
+ if not c["passed"]:
571
+ for line in [
572
+ ln for ln in text.splitlines() if re.search(r"Error|Traceback|Assertion|NaN|Mismatch", ln)
573
+ ][:3]:
574
+ ctx.out(f" │ {line}")
575
+ if fail_fast:
576
+ return {"cases": cases, "params": {"dtypes": dtypes, "match": match, "quick": quick}}
577
+ return {"cases": cases, "params": {"dtypes": dtypes, "match": match, "quick": quick}}
578
+
579
+
580
+ # ── vllm (runner tests + end-to-end smoke) ──────────────────────────────────
581
+
582
+
583
+ def parse_smoke(text: str) -> dict:
584
+ """Metrics from a ``smoke_vllm.py`` log (absent ones are None)."""
585
+ m = re.search(r"mean match ([0-9.]+), worst prefix (\d+)", text)
586
+ lp = re.search(
587
+ r"logprob agreement: mean \|Δ\| (\S+), max \|Δ\| (\S+), near-ties (\d+)/(\d+), worst gap (\S+)", text
588
+ )
589
+
590
+ def num(v: str) -> float | None:
591
+ return None if v == "n/a" else float(v.rstrip(","))
592
+
593
+ return {
594
+ "mean_match": float(m.group(1)) if m else None,
595
+ "worst_prefix": int(m.group(2)) if m else None,
596
+ "mean_dlp": num(lp.group(1).rstrip(",")) if lp else None,
597
+ "max_dlp": num(lp.group(2).rstrip(",")) if lp else None,
598
+ "near_ties": int(lp.group(3)) if lp else None,
599
+ "divergences": int(lp.group(4)) if lp else None,
600
+ "worst_gap": num(lp.group(5)) if lp else None,
601
+ }
602
+
603
+
604
+ def smoke_summary(c: dict) -> str:
605
+ """One compact line for a smoke case: logprob agreement, then the match."""
606
+ parts = []
607
+ if c.get("mean_dlp") is not None:
608
+ parts.append(f"|Δlp| {c['mean_dlp']:.3f} mean · {c['max_dlp']:.3f} max")
609
+ if c.get("divergences") is not None:
610
+ parts.append(f"near-ties {c['near_ties']}/{c['divergences']}")
611
+ if c.get("mean_match") is not None:
612
+ parts.append(f"match {c['mean_match']:.2f}")
613
+ return (" " + " · ".join(parts)) if parts else ""
614
+
615
+
616
+ def stage_vllm(
617
+ ctx: Ctx,
618
+ repo: Path,
619
+ smoke_modes: str = SMOKE_MODES,
620
+ model: str = "Qwen/Qwen3-0.6B",
621
+ tokens: int = 128,
622
+ timeout: float = 1800.0,
623
+ hf_home: Path | None = None,
624
+ ) -> dict:
625
+ """The vLLM plugin: runner-vs-emulator tests (every KV layout, strided Q,
626
+ scales, q_len 2, CUDA-graph replay), then greedy MJOLNIR-vs-FLASHINFER
627
+ smoke runs in a real ``vllm`` process, judged by logprob agreement and
628
+ near-tie divergences (see ``tests/kernel/smoke_vllm.py``)."""
629
+ hf = hf_home or Path.home() / ".cache" / "huggingface"
630
+ cases = []
631
+ log = ctx.logdir / "vllm" / "test_runner.log"
632
+ rc = ctx.runner.tests(log, repo, "python3 tests/test_runner.py", timeout, hf)
633
+ text = log.read_text(errors="replace") if log.exists() else ""
634
+ for line in text.splitlines():
635
+ m = re.search(r"(✅|❌) (test_\S+)", line)
636
+ if m:
637
+ cases.append({"label": f"runner {m.group(2).rstrip(':')}", "passed": m.group(1) == "✅", "log": str(log)})
638
+ ctx.out(" " + line.strip())
639
+ if rc != 0 and not any(not c["passed"] for c in cases):
640
+ cases.append({"label": f"runner tests (exit {rc})", "passed": ctx.runner.dry_run, "log": str(log)})
641
+ for mode in smoke_modes.split():
642
+ args = f"--model {model} --max_tokens {tokens}"
643
+ if mode == "eager":
644
+ args += " --eager"
645
+ elif mode == "e2m3": # Mjolnir on a 6-bit e2m3 KV cache (calibrates its own scales)
646
+ args += " --kv fp6_e2m3"
647
+ elif mode == "lmh8": # Mjolnir with the opt-in fp8 lm_head (MJOLNIR_LM_HEAD=fp8)
648
+ args += " --lm-head fp8"
649
+ elif mode.startswith("spec"):
650
+ args += f" --spec {mode[4:] or 2}"
651
+ elif mode != "graph":
652
+ continue
653
+ log = ctx.logdir / "vllm" / f"smoke-{mode}.log"
654
+ rc = ctx.runner.tests(log, repo, f"python3 tests/smoke_vllm.py {args}", timeout, hf)
655
+ text = log.read_text(errors="replace") if log.exists() else ""
656
+ ok = (rc == 0 and bool(re.search(r"^PASS$", text, re.M))) or ctx.runner.dry_run
657
+ cases.append({"label": f"smoke {mode}", "passed": ok, **parse_smoke(text), "log": str(log)})
658
+ ctx.out(f" {'✅' if ok else '❌'} smoke {mode:<6}" + smoke_summary(cases[-1]))
659
+ if not ok:
660
+ for line in re.findall(r"^ ❌ (.*)$", text, re.M)[:3]:
661
+ ctx.out(f" │ {line}")
662
+ return {"cases": cases, "params": {"smoke_modes": smoke_modes, "model": model, "tokens": tokens}}
663
+
664
+
665
+ def _profile_stage(ctx: Ctx, **kw) -> dict:
666
+ from mjolnir_cli.verify.profile import stage_profile
667
+
668
+ return stage_profile(ctx, **kw)
669
+
670
+
671
+ def _spec_stage(ctx: Ctx, **kw) -> dict:
672
+ from mjolnir_cli.verify.spec import stage_spec
673
+
674
+ return stage_spec(ctx, **kw)
675
+
676
+
677
+ def _kvcache_stage(ctx: Ctx, **kw) -> dict:
678
+ from mjolnir_cli.verify.kvcache import stage_kvcache
679
+
680
+ return stage_kvcache(ctx, **kw)
681
+
682
+
683
+ # ── registry ────────────────────────────────────────────────────────────────
684
+
685
+ STAGES: dict[str, Stage] = {
686
+ s.name: s
687
+ for s in [
688
+ Stage(
689
+ "correctness",
690
+ "Correctness matrix",
691
+ "correctness",
692
+ "kernel vs the bit-faithful emulator: every layout, format, page size, q_len, Q dtype",
693
+ stage_correctness,
694
+ ),
695
+ Stage(
696
+ "sweep",
697
+ "Headline L-sweep",
698
+ "ncu",
699
+ "Mjolnir vs FlashInfer vs FlashAttention 2, ncu kernel time at every L",
700
+ stage_sweep,
701
+ ),
702
+ Stage(
703
+ "hndsweep",
704
+ "Head-major pages",
705
+ "ncu",
706
+ "FlashInfer on NHD and HND vs Mjolnir on HND — is HND a kernel win or a layout win?",
707
+ stage_hndsweep,
708
+ ),
709
+ Stage(
710
+ "kvfmt",
711
+ "KV layouts and formats",
712
+ "ncu",
713
+ "fp8 / fp6 / fp4 / nvfp4 KV on NHD / interleaved / HND pages (ncu + wall clock)",
714
+ stage_kvfmt,
715
+ ),
716
+ Stage(
717
+ "quant",
718
+ "KV format accuracy",
719
+ "harness",
720
+ "relative L2 error of each KV format vs unquantized KV, at every L",
721
+ stage_quant,
722
+ ),
723
+ Stage("ring", "KV ring depth", "ncu", "TMA→MMA ring depth (--kv_stages) sweep at every L", stage_ring),
724
+ Stage("prefetch", "L2 prefetch", "ncu", "TMA L2 prefetch distance (--l2_prefetch) sweep", stage_prefetch),
725
+ Stage(
726
+ "pdl",
727
+ "Programmatic dependent launch",
728
+ "harness",
729
+ "PDL on/off wall clock (decode overlaps the previous kernel; merge launches early)",
730
+ stage_pdl,
731
+ ),
732
+ Stage(
733
+ "ab", "Flag A/B", "ncu", "any harness flag sets, ncu + wall clock (default: evict-first x PDL)", stage_ab
734
+ ),
735
+ Stage(
736
+ "fp6prof",
737
+ "Full profiles per KV format",
738
+ "profile",
739
+ "full ncu profiles: L2 sectors vs requests vs time, warp stalls, per KV format",
740
+ stage_fp6prof,
741
+ ),
742
+ Stage(
743
+ "timing", "Harness timing", "harness", "harness-timed medians, 1 vs 2 CTAs/SM, fp8 and bf16 Q", stage_timing
744
+ ),
745
+ Stage(
746
+ "kv-cache-size",
747
+ "KV cache capacity",
748
+ "serving",
749
+ "tokens of KV each config fits (vLLM's startup sizing), and the concurrency that buys per context",
750
+ _kvcache_stage,
751
+ ),
752
+ Stage(
753
+ "spec",
754
+ "Speculative decoding",
755
+ "serving",
756
+ "MTP acceptance per draft position and decode t/s for 1-N draft tokens, per config — the best draft count",
757
+ _spec_stage,
758
+ ),
759
+ Stage(
760
+ "profile",
761
+ "Decode step profile",
762
+ "serving",
763
+ "torch-profiler trace of a decode workload per config: CUDA-graph replays, eager kernels, GPU idle",
764
+ _profile_stage,
765
+ ),
766
+ Stage(
767
+ "vllm",
768
+ "vLLM integration",
769
+ "tests",
770
+ "runner-vs-emulator tests + greedy MJOLNIR vs FLASHINFER smoke runs",
771
+ stage_vllm,
772
+ ),
773
+ ]
774
+ }
775
+
776
+ # `mjolnir verify all` / `mjolnir verify bench`
777
+ ALL_ORDER = ["correctness", "sweep", "hndsweep", "kvfmt", "quant", "ring", "prefetch", "pdl", "ab", "fp6prof"]
778
+ BENCH_ORDER = [s for s in ALL_ORDER if s != "correctness"]
779
+
780
+
781
+ def summarize(stage: str, doc: dict) -> dict:
782
+ """Headline numbers stored with the run (the README recomputes from the
783
+ series; this is for `mjolnir verify show` and the ledger)."""
784
+ out: dict = {}
785
+ by = results.series_by_id(doc)
786
+ if stage in ("sweep", "hndsweep"):
787
+ ours = by.get("mjolnir") or by.get("mjolnir-fp8")
788
+ for base_id in ("fi", "fi-hnd", "fa2"):
789
+ if ours and base_id in by:
790
+ sp = results.speedup(by[base_id], ours)
791
+ if sp:
792
+ out[f"vs_{base_id}"] = {
793
+ "geomean": round(results.geomean(sp.values()) or 0, 4),
794
+ "min": round(min(sp.values()), 4),
795
+ "max": round(max(sp.values()), 4),
796
+ "per_L": {str(k): round(v, 4) for k, v in sp.items()},
797
+ }
798
+ if stage == "kv-cache-size":
799
+ out = {
800
+ r["id"]: {"kv_tokens": r.get("kv_tokens"), "max_concurrency": r.get("max_concurrency")}
801
+ for r in doc.get("rows", [])
802
+ }
803
+ if stage == "spec":
804
+ from mjolnir_cli.verify.spec import best_k
805
+
806
+ out = {
807
+ f"{cfg} c={conc}": {"k": r["k"], "decode_tps": r["decode_tps"], "acceptance_length": r["acceptance_length"]}
808
+ for (cfg, conc), r in sorted(best_k(doc.get("rows", [])).items())
809
+ }
810
+ if stage == "correctness" or stage == "vllm":
811
+ cs = doc.get("cases", [])
812
+ out = {"passed": sum(1 for c in cs if c.get("passed")), "failed": sum(1 for c in cs if not c.get("passed"))}
813
+ return out
814
+
815
+
816
+ def eprint(*a):
817
+ print(*a, file=sys.stderr)