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,816 @@
1
+ """``mjolnir verify`` — correctness, benchmarks and profiles of the Mjolnir
2
+ kernel, every run saved as structured raw data (``benchmarks/raw/verify``).
3
+
4
+ mjolnir verify correctness # kernel vs emulator matrix
5
+ mjolnir verify sweep # Mjolnir vs FlashInfer vs FA2 at every L (ncu)
6
+ mjolnir verify hndsweep|kvfmt|quant|ring|prefetch|pdl|ab|fp6prof|timing|vllm
7
+ mjolnir verify bench # every benchmark stage, default parameters
8
+ mjolnir verify all # correctness + every benchmark stage
9
+ mjolnir verify show [stage] # re-print the latest saved run(s)
10
+ mjolnir verify list # stages + saved runs
11
+
12
+ Every stage defaults to the full L axis: 128 4096 8192 16384 32768 65536
13
+ 131072 262144 (``--ls`` overrides). A run with non-default parameters is
14
+ saved too, but marked ``default: false`` so it never replaces a headline
15
+ chart (``mjolnir plot`` reads the latest default run of each stage).
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import shutil
21
+ import sys
22
+ from typing import Optional
23
+
24
+ import typer
25
+
26
+ from mjolnir_cli.config import DEFAULT_IMAGE, load_layout, resolve
27
+ from mjolnir_cli.legs import KERNEL, SMOKE_MODES
28
+ from mjolnir_cli.verify import results, stages
29
+ from mjolnir_cli.verify.kvcache import DEFAULT_LEGS as KV_LEGS
30
+ from mjolnir_cli.verify.profile import DEFAULT_LEGS as PROFILE_LEGS
31
+ from mjolnir_cli.verify.spec import DEFAULT_DRAFTS, DEFAULT_LEGS as SPEC_LEGS
32
+ from mjolnir_cli.verify.docker import Runner, ensure_image
33
+ from mjolnir_cli.verify.report import console, show
34
+
35
+ app = typer.Typer(
36
+ help="Kernel correctness + benchmarks (Mjolnir vs FlashInfer vs FA2), saved as raw data for the charts.",
37
+ no_args_is_help=True,
38
+ context_settings={"help_option_names": ["-h", "--help"]},
39
+ )
40
+
41
+ _LS_HELP = "L values (default: 128 4096 8192 16384 32768 65536 131072 262144)"
42
+
43
+
44
+ def _parse_ls(ls: Optional[str]) -> tuple[int, ...]:
45
+ if not ls:
46
+ return stages.ALL_LS
47
+ return tuple(int(x) for x in ls.replace(",", " ").split())
48
+
49
+
50
+ def _kernel_info(layout) -> dict:
51
+ info: dict = {}
52
+ try:
53
+ import mjolnir # the kernel package (workspace / installed)
54
+
55
+ info["version"] = getattr(mjolnir, "__version__", "?")
56
+ except ImportError:
57
+ pass
58
+ if layout.repo_root is not None:
59
+ src = layout.repo_root / "src" / "mjolnir" / "mjolnir" / "__init__.py"
60
+ if src.exists():
61
+ import re
62
+
63
+ m = re.search(r'__version__\s*=\s*"([^"]+)"', src.read_text())
64
+ if m:
65
+ info["version"] = m.group(1)
66
+ info.update(results.git_info(layout.repo_root))
67
+ return info
68
+
69
+
70
+ # docker missing / image unavailable: nothing can run, stop the whole sequence
71
+ PREFLIGHT_RC = 2
72
+
73
+
74
+ def execute(
75
+ stage: str,
76
+ params: dict,
77
+ defaults: dict,
78
+ *,
79
+ image: Optional[str],
80
+ ls: Optional[str],
81
+ iters: int,
82
+ gate: bool,
83
+ dry_run: bool,
84
+ timeout: float,
85
+ save: bool,
86
+ plot: bool,
87
+ page: int = 128,
88
+ ) -> tuple[int, dict | None]:
89
+ layout = load_layout()
90
+ s = resolve(None, None, image, None)
91
+ if shutil.which("docker") is None and not dry_run:
92
+ console.print("[red]docker not found on PATH[/red]")
93
+ return PREFLIGHT_RC, None
94
+ if not dry_run:
95
+ err = ensure_image(s.image)
96
+ if err:
97
+ console.print(f"[red]✗ {err}[/red]")
98
+ return PREFLIGHT_RC, None
99
+ run_id = results.new_run_id()
100
+ logdir = layout.benchmarks_dir / "logs" / f"verify-{run_id}"
101
+ kernel_src = None
102
+ if layout.repo_root is not None:
103
+ cand = layout.repo_root / "src" / "mjolnir"
104
+ if (cand / "mjolnir" / "t5_decode.py").exists():
105
+ kernel_src = cand
106
+ runner = Runner(image=s.image, logdir=logdir, kernel_src=kernel_src, timeout=timeout, dry_run=dry_run, page=page)
107
+ ctx = stages.Ctx(
108
+ runner=runner,
109
+ run_id=run_id,
110
+ logdir=logdir,
111
+ metrics_url=s.metrics_url,
112
+ gate=gate,
113
+ ls=_parse_ls(ls),
114
+ iters=iters,
115
+ out=lambda m: console.print(m, highlight=False, soft_wrap=True), # rows never hard-wrap
116
+ )
117
+ st = stages.STAGES[stage]
118
+ console.print(f"\n[bold cyan]━━ verify {stage}[/bold cyan] — {st.summary}")
119
+ console.print(
120
+ f"[dim] image {s.image}{'' if s.image == DEFAULT_IMAGE else f' (not the default {DEFAULT_IMAGE})'} · kernel {'checkout ' + str(kernel_src) if kernel_src else 'baked into the image'}"
121
+ f" · L {' '.join(str(x) for x in ctx.ls)} · logs {logdir}[/dim]"
122
+ )
123
+ console.print(f"[dim] FlashInfer / FA2 references run on the official image {runner.ref_image}[/dim]")
124
+ from mjolnir_cli import cachectl
125
+
126
+ if not dry_run:
127
+ cachectl.prepare_bench() # clean page cache before measuring (no-op without the grant)
128
+ kwargs = dict(params)
129
+ if stage == "vllm":
130
+ kwargs["repo"] = layout.require_repo("verify vllm")
131
+ try:
132
+ body = st.fn(ctx, **kwargs)
133
+ finally:
134
+ if not dry_run: # Thor keeps the page cache the stage's containers filled
135
+ cachectl.prepare_bench()
136
+ is_default = params == defaults and ctx.ls == stages.ALL_LS and iters == stages.DEFAULT_ITERS and page == 128
137
+ doc = results.header(
138
+ stage,
139
+ run_id,
140
+ image=s.image,
141
+ default=is_default,
142
+ params={
143
+ **body.pop("params", {}),
144
+ "ls": list(ctx.ls),
145
+ "iters": iters,
146
+ "page": page,
147
+ "hq": runner.hq,
148
+ "hk": runner.hk,
149
+ "d": runner.d,
150
+ },
151
+ kernel=_kernel_info(layout),
152
+ gate=ctx.gate_log,
153
+ )
154
+ doc["ref_image"] = runner.ref_image # where the FlashInfer / FA2 legs ran (stock vLLM)
155
+ doc.update(body)
156
+ doc["summary"] = stages.summarize(stage, doc)
157
+ if dry_run:
158
+ console.print("[dim] (dry run — nothing saved)[/dim]")
159
+ return 0, None
160
+ show(doc)
161
+ if save:
162
+ p = results.save(layout.verify_dir, doc)
163
+ console.print(f" 📝 [bold]{p}[/bold]{'' if is_default else ' (custom parameters: not a headline run)'}")
164
+ if plot and layout.is_repo:
165
+ _replot(layout)
166
+ failed = [c for c in doc.get("cases", []) if not c.get("passed")]
167
+ empty = st.kind in ("ncu",) and not any(s_.get("points") for s_ in doc.get("series", []))
168
+ return (1 if failed or empty else 0), doc
169
+
170
+
171
+ def _execute(*a, **kw) -> int:
172
+ return execute(*a, **kw)[0]
173
+
174
+
175
+ def _replot(layout) -> None:
176
+ try:
177
+ from mjolnir_cli import charts, readme
178
+
179
+ charts.render_all(layout)
180
+ readme.render_readme(layout)
181
+ except Exception as e: # noqa: BLE001 — charts are a convenience here
182
+ console.print(f"[yellow] (charts/README not re-rendered: {e})[/yellow]")
183
+
184
+
185
+ # ── shared options (each command declares them; typer needs explicit params) ──
186
+ IMAGE = typer.Option(None, "--image", help="vLLM image (default: the active one, see `mjolnir image`)")
187
+ LS = typer.Option(None, "--ls", help=_LS_HELP)
188
+ ITERS = typer.Option(stages.DEFAULT_ITERS, "--iters", help="profiled iterations per point (ncu stages)")
189
+ GATE = typer.Option(True, "--gate/--no-gate", help="wait for a clean window when the live vLLM server is up")
190
+ DRY = typer.Option(False, "--dry-run", help="print the docker commands, run nothing")
191
+ TIMEOUT = typer.Option(180.0, "--timeout", help="per-container timeout (s); a hung kernel's container is killed")
192
+ SAVE = typer.Option(True, "--save/--no-save", help="write the raw result JSON")
193
+ PLOT = typer.Option(False, "--plot/--no-plot", help="re-render charts + README after saving (checkout only)")
194
+ PAGE = typer.Option(128, "--page-size", help="KV page size for every leg")
195
+
196
+
197
+ def _run(stage: str, params: dict, defaults: dict, image, ls, iters, gate, dry_run, timeout, save, plot, page=128):
198
+ raise typer.Exit(
199
+ _execute(
200
+ stage,
201
+ params,
202
+ defaults,
203
+ image=image,
204
+ ls=ls,
205
+ iters=iters,
206
+ gate=gate,
207
+ dry_run=dry_run,
208
+ timeout=timeout,
209
+ save=save,
210
+ plot=plot,
211
+ page=page,
212
+ )
213
+ )
214
+
215
+
216
+ @app.command()
217
+ def correctness(
218
+ dtypes: str = typer.Option("fp8 bf16", "--dtypes", help="Q dtypes to test"),
219
+ match: str = typer.Option("", "--match", help="only cases whose label contains this"),
220
+ quick: bool = typer.Option(False, "--quick", help="the quick subset (one case per group; what autoresearch runs)"),
221
+ fail_fast: bool = typer.Option(False, "--fail-fast"),
222
+ image: Optional[str] = IMAGE,
223
+ dry_run: bool = DRY,
224
+ timeout: float = TIMEOUT,
225
+ save: bool = SAVE,
226
+ ):
227
+ """Kernel vs the bit-faithful emulator: every layout, KV format, page size, q_len and Q dtype."""
228
+ p = {"dtypes": dtypes, "match": match, "fail_fast": fail_fast, "timeout": timeout, "quick": quick}
229
+ d = {"dtypes": "fp8 bf16", "match": "", "fail_fast": False, "timeout": 180.0, "quick": False}
230
+ _run("correctness", p, d, image, None, stages.DEFAULT_ITERS, False, dry_run, timeout, save, False)
231
+
232
+
233
+ @app.command()
234
+ def sweep(
235
+ legs: str = typer.Option(KERNEL["verify.sweep"], "--legs", help="legs: fi, fi-hnd, fa2, mjolnir-fp8, mjolnir"),
236
+ image: Optional[str] = IMAGE,
237
+ ls: Optional[str] = LS,
238
+ iters: int = ITERS,
239
+ gate: bool = GATE,
240
+ dry_run: bool = DRY,
241
+ timeout: float = TIMEOUT,
242
+ save: bool = SAVE,
243
+ plot: bool = PLOT,
244
+ page: int = PAGE,
245
+ batch: int = typer.Option(1, "--batch", help="sequences per step, each of L tokens (serving: 1-4)"),
246
+ q_len: int = typer.Option(1, "--q-len", help="query tokens per sequence (MTP=3 verification: 4)"),
247
+ ):
248
+ """The headline: Mjolnir vs FlashInfer vs FlashAttention 2 at every L (ncu, cold L2, base clocks).
249
+
250
+ `--batch 4 --q-len 4 --legs fi,mjolnir-fp8` measures the shape vLLM serves
251
+ with MTP=3 at 4 concurrent requests (saved as a non-default run)."""
252
+ _run(
253
+ "sweep",
254
+ {"legs": legs, "batch": batch, "q_len": q_len},
255
+ {"legs": KERNEL["verify.sweep"], "batch": 1, "q_len": 1},
256
+ image,
257
+ ls,
258
+ iters,
259
+ gate,
260
+ dry_run,
261
+ timeout,
262
+ save,
263
+ plot,
264
+ page,
265
+ )
266
+
267
+
268
+ @app.command()
269
+ def hndsweep(
270
+ legs: str = typer.Option(KERNEL["verify.hndsweep"], "--legs"),
271
+ image: Optional[str] = IMAGE,
272
+ ls: Optional[str] = LS,
273
+ iters: int = ITERS,
274
+ gate: bool = GATE,
275
+ dry_run: bool = DRY,
276
+ timeout: float = TIMEOUT,
277
+ save: bool = SAVE,
278
+ plot: bool = PLOT,
279
+ page: int = PAGE,
280
+ ):
281
+ """FlashInfer on NHD and HND pages vs Mjolnir (fp8 KV) on HND: the fair layout comparison."""
282
+ _run(
283
+ "hndsweep",
284
+ {"legs": legs},
285
+ {"legs": KERNEL["verify.hndsweep"]},
286
+ image,
287
+ ls,
288
+ iters,
289
+ gate,
290
+ dry_run,
291
+ timeout,
292
+ save,
293
+ plot,
294
+ page,
295
+ )
296
+
297
+
298
+ @app.command()
299
+ def kvfmt(
300
+ variants: str = typer.Option(stages.KVFMT_DEFAULT, "--variants", help="name:flags|name:flags"),
301
+ refs: str = typer.Option(KERNEL["verify.kvfmt.refs"], "--refs", help="baseline legs"),
302
+ wall_iters: int = typer.Option(200, "--wall-iters", help="harness wall-clock iterations (0 = skip)"),
303
+ image: Optional[str] = IMAGE,
304
+ ls: Optional[str] = LS,
305
+ iters: int = ITERS,
306
+ gate: bool = GATE,
307
+ dry_run: bool = DRY,
308
+ timeout: float = TIMEOUT,
309
+ save: bool = SAVE,
310
+ plot: bool = PLOT,
311
+ ):
312
+ """KV layouts x formats: fp8 / fp6 (e2m3, e3m2) / fp4 (e2m1) / nvfp4 on NHD / interleaved / HND."""
313
+ _run(
314
+ "kvfmt",
315
+ {"variants": variants, "refs": refs, "wall_iters": wall_iters},
316
+ {"variants": stages.KVFMT_DEFAULT, "refs": KERNEL["verify.kvfmt.refs"], "wall_iters": 200},
317
+ image,
318
+ ls,
319
+ iters,
320
+ gate,
321
+ dry_run,
322
+ timeout,
323
+ save,
324
+ plot,
325
+ )
326
+
327
+
328
+ @app.command()
329
+ def quant(
330
+ formats: str = typer.Option(stages.QUANT_FORMATS, "--formats"),
331
+ batches: int = typer.Option(2, "--batches"),
332
+ image: Optional[str] = IMAGE,
333
+ ls: Optional[str] = LS,
334
+ dry_run: bool = DRY,
335
+ timeout: float = TIMEOUT,
336
+ save: bool = SAVE,
337
+ plot: bool = PLOT,
338
+ ):
339
+ """Accuracy per KV format: relative L2 error vs attention over unquantized KV."""
340
+ _run(
341
+ "quant",
342
+ {"formats": formats, "batches": batches},
343
+ {"formats": stages.QUANT_FORMATS, "batches": 2},
344
+ image,
345
+ ls,
346
+ stages.DEFAULT_ITERS,
347
+ False,
348
+ dry_run,
349
+ timeout,
350
+ save,
351
+ plot,
352
+ )
353
+
354
+
355
+ @app.command()
356
+ def ring(
357
+ depths: str = typer.Option(stages.RING_DEFAULT, "--stages", help="--kv_stages values"),
358
+ base: str = typer.Option("", "--base", help="harness flags every variant gets"),
359
+ refs: str = typer.Option("fi", "--refs"),
360
+ image: Optional[str] = IMAGE,
361
+ ls: Optional[str] = LS,
362
+ iters: int = ITERS,
363
+ gate: bool = GATE,
364
+ dry_run: bool = DRY,
365
+ timeout: float = TIMEOUT,
366
+ save: bool = SAVE,
367
+ plot: bool = PLOT,
368
+ ):
369
+ """KV ring depth (TMA → MMA stages) at every L."""
370
+ _run(
371
+ "ring",
372
+ {"stages": depths, "base": base, "refs": refs},
373
+ {"stages": stages.RING_DEFAULT, "base": "", "refs": "fi"},
374
+ image,
375
+ ls,
376
+ iters,
377
+ gate,
378
+ dry_run,
379
+ timeout,
380
+ save,
381
+ plot,
382
+ )
383
+
384
+
385
+ @app.command()
386
+ def prefetch(
387
+ distances: str = typer.Option("0 1 2 4", "--distances", help="--l2_prefetch tiles ahead"),
388
+ base: str = typer.Option("--kv_format fp8", "--base"),
389
+ refs: str = typer.Option("fi", "--refs"),
390
+ image: Optional[str] = IMAGE,
391
+ ls: Optional[str] = LS,
392
+ iters: int = ITERS,
393
+ gate: bool = GATE,
394
+ dry_run: bool = DRY,
395
+ timeout: float = TIMEOUT,
396
+ save: bool = SAVE,
397
+ plot: bool = PLOT,
398
+ ):
399
+ """TMA L2-prefetch distance sweep."""
400
+ _run(
401
+ "prefetch",
402
+ {"distances": distances, "base": base, "refs": refs},
403
+ {"distances": "0 1 2 4", "base": "--kv_format fp8", "refs": "fi"},
404
+ image,
405
+ ls,
406
+ iters,
407
+ gate,
408
+ dry_run,
409
+ timeout,
410
+ save,
411
+ plot,
412
+ )
413
+
414
+
415
+ @app.command()
416
+ def pdl(
417
+ batches: str = typer.Option("1 8", "--batches"),
418
+ dtypes: str = typer.Option("fp8", "--dtypes"),
419
+ wall_iters: int = typer.Option(200, "--wall-iters"),
420
+ image: Optional[str] = IMAGE,
421
+ ls: Optional[str] = LS,
422
+ gate: bool = GATE,
423
+ dry_run: bool = DRY,
424
+ timeout: float = TIMEOUT,
425
+ save: bool = SAVE,
426
+ plot: bool = PLOT,
427
+ ):
428
+ """Programmatic dependent launch on/off (wall clock)."""
429
+ _run(
430
+ "pdl",
431
+ {"batches": batches, "dtypes": dtypes, "iters": wall_iters},
432
+ {"batches": "1 8", "dtypes": "fp8", "iters": 200},
433
+ image,
434
+ ls,
435
+ stages.DEFAULT_ITERS,
436
+ gate,
437
+ dry_run,
438
+ timeout,
439
+ save,
440
+ plot,
441
+ )
442
+
443
+
444
+ @app.command()
445
+ def ab(
446
+ variants: str = typer.Option(stages.AB_DEFAULT, "--variants", help="name:flags|name:flags"),
447
+ refs: str = typer.Option("fi", "--refs"),
448
+ wall_iters: int = typer.Option(200, "--wall-iters"),
449
+ image: Optional[str] = IMAGE,
450
+ ls: Optional[str] = LS,
451
+ iters: int = ITERS,
452
+ gate: bool = GATE,
453
+ dry_run: bool = DRY,
454
+ timeout: float = TIMEOUT,
455
+ save: bool = SAVE,
456
+ plot: bool = PLOT,
457
+ ):
458
+ """A/B any harness flag sets (ncu + wall clock). Default: evict-first x PDL."""
459
+ _run(
460
+ "ab",
461
+ {"variants": variants, "refs": refs, "wall_iters": wall_iters},
462
+ {"variants": stages.AB_DEFAULT, "refs": "fi", "wall_iters": 200},
463
+ image,
464
+ ls,
465
+ iters,
466
+ gate,
467
+ dry_run,
468
+ timeout,
469
+ save,
470
+ plot,
471
+ )
472
+
473
+
474
+ @app.command()
475
+ def fp6prof(
476
+ variants: str = typer.Option(stages.PROF_DEFAULT, "--variants"),
477
+ count: int = typer.Option(3, "--count", help="profiled launches per point"),
478
+ section_set: str = typer.Option("full", "--set"),
479
+ image: Optional[str] = IMAGE,
480
+ ls: Optional[str] = LS,
481
+ gate: bool = GATE,
482
+ dry_run: bool = DRY,
483
+ timeout: float = TIMEOUT,
484
+ save: bool = SAVE,
485
+ plot: bool = PLOT,
486
+ ):
487
+ """Full ncu profiles per KV format (bytes vs requests vs time, warp stalls)."""
488
+ _run(
489
+ "fp6prof",
490
+ {"variants": variants, "count": count, "section_set": section_set},
491
+ {"variants": stages.PROF_DEFAULT, "count": 3, "section_set": "full"},
492
+ image,
493
+ ls,
494
+ stages.DEFAULT_ITERS,
495
+ gate,
496
+ dry_run,
497
+ timeout,
498
+ save,
499
+ plot,
500
+ )
501
+
502
+
503
+ @app.command()
504
+ def timing(
505
+ dtypes: str = typer.Option("fp8 bf16", "--dtypes"),
506
+ ctas: str = typer.Option("1 2", "--ctas"),
507
+ wall_iters: int = typer.Option(50, "--wall-iters"),
508
+ image: Optional[str] = IMAGE,
509
+ ls: Optional[str] = LS,
510
+ gate: bool = GATE,
511
+ dry_run: bool = DRY,
512
+ timeout: float = TIMEOUT,
513
+ save: bool = SAVE,
514
+ ):
515
+ """Harness-timed medians, 1 vs 2 CTAs/SM, fp8 and bf16 Q (no ncu)."""
516
+ _run(
517
+ "timing",
518
+ {"dtypes": dtypes, "ctas": ctas, "iters": wall_iters},
519
+ {"dtypes": "fp8 bf16", "ctas": "1 2", "iters": 50},
520
+ image,
521
+ ls,
522
+ stages.DEFAULT_ITERS,
523
+ gate,
524
+ dry_run,
525
+ timeout,
526
+ save,
527
+ False,
528
+ )
529
+
530
+
531
+ @app.command("profile")
532
+ def profile(
533
+ legs: str = typer.Option(
534
+ PROFILE_LEGS,
535
+ "--legs",
536
+ help="side:config, comma-separated (mjolnir:NVFP4 = FlashInfer on the Mjolnir image)",
537
+ ),
538
+ drafts: int = typer.Option(3, "--drafts", help="num_speculative_tokens (0 = the config's own)"),
539
+ context_words: int = typer.Option(6000, "--context-words", help="prompt size (~1.3 tokens/word; 0 = short)"),
540
+ concurrency: int = typer.Option(1, "--concurrency"),
541
+ requests: int = typer.Option(2, "--requests", help="request rounds inside the profiled window"),
542
+ max_tokens: int = typer.Option(128, "--max-tokens"),
543
+ model: Optional[str] = typer.Option(None, "--model"),
544
+ restart: bool = typer.Option(False, "--restart", help="stop a running vLLM server first"),
545
+ image: Optional[str] = IMAGE,
546
+ dry_run: bool = DRY,
547
+ timeout: float = typer.Option(1800.0, "--timeout"),
548
+ save: bool = SAVE,
549
+ ):
550
+ """Profile a decode workload per config (torch profiler): graph replays, eager kernels, GPU idle, top kernels."""
551
+ p = {
552
+ "legs": legs,
553
+ "drafts": drafts,
554
+ "context_words": context_words,
555
+ "concurrency": concurrency,
556
+ "requests": requests,
557
+ "max_tokens": max_tokens,
558
+ "model": model,
559
+ "restart": restart,
560
+ "timeout": timeout,
561
+ }
562
+ _run(
563
+ "profile",
564
+ p,
565
+ {**p, "legs": PROFILE_LEGS},
566
+ image,
567
+ None,
568
+ stages.DEFAULT_ITERS,
569
+ False,
570
+ dry_run,
571
+ timeout,
572
+ save,
573
+ False,
574
+ )
575
+
576
+
577
+ @app.command("spec")
578
+ def spec(
579
+ legs: str = typer.Option(
580
+ SPEC_LEGS,
581
+ "--legs",
582
+ help="side:config, comma-separated; official = the stock vLLM image, mjolnir = --image",
583
+ ),
584
+ drafts: str = typer.Option(DEFAULT_DRAFTS, "--drafts", help="num_speculative_tokens values to serve"),
585
+ concurrency: str = typer.Option("1 4", "--concurrency", help="requests in flight per block"),
586
+ max_tokens: int = typer.Option(256, "--max-tokens", help="generated tokens per request"),
587
+ long_words: int = typer.Option(6000, "--long-words", help="the long-context prompt's size (0 = none)"),
588
+ model: Optional[str] = typer.Option(None, "--model", help="model (default: the active one)"),
589
+ restart: bool = typer.Option(
590
+ False, "--restart", help="stop a running vLLM server (default: refuse — the GPU must be yours)"
591
+ ),
592
+ image: Optional[str] = IMAGE,
593
+ dry_run: bool = DRY,
594
+ timeout: float = typer.Option(1800.0, "--timeout", help="per-point server start timeout (s)"),
595
+ save: bool = SAVE,
596
+ plot: bool = PLOT,
597
+ ):
598
+ """MTP per config and draft count: tokens/step, per-position acceptance, decode t/s (restarts the server per point)."""
599
+ defaults = {
600
+ "legs": SPEC_LEGS,
601
+ "drafts": DEFAULT_DRAFTS,
602
+ "concurrency": "1 4",
603
+ "max_tokens": 256,
604
+ "long_words": 6000,
605
+ "model": None,
606
+ "restart": restart,
607
+ "timeout": 1800.0,
608
+ }
609
+ _run(
610
+ "spec",
611
+ {
612
+ "legs": legs,
613
+ "drafts": drafts,
614
+ "concurrency": concurrency,
615
+ "max_tokens": max_tokens,
616
+ "long_words": long_words,
617
+ "model": model,
618
+ "restart": restart,
619
+ "timeout": timeout,
620
+ },
621
+ defaults,
622
+ image,
623
+ None,
624
+ stages.DEFAULT_ITERS,
625
+ False,
626
+ dry_run,
627
+ timeout,
628
+ save,
629
+ plot,
630
+ )
631
+
632
+
633
+ @app.command("kv-cache-size")
634
+ def kv_cache_size(
635
+ legs: str = typer.Option(
636
+ KV_LEGS,
637
+ "--legs",
638
+ help="side:config, comma-separated; official = the stock vLLM image, mjolnir = --image",
639
+ ),
640
+ model: Optional[str] = typer.Option(None, "--model", help="model (default: the active one)"),
641
+ restart: bool = typer.Option(
642
+ False, "--restart", help="stop a running vLLM server (default: refuse — the GPU must be yours)"
643
+ ),
644
+ image: Optional[str] = IMAGE,
645
+ dry_run: bool = DRY,
646
+ timeout: float = typer.Option(1800.0, "--timeout", help="per-leg wait for the KV sizing (s)"),
647
+ save: bool = SAVE,
648
+ plot: bool = PLOT,
649
+ ):
650
+ """KV cache capacity per config: serves each leg (restarting the server), reads vLLM's KV sizing, stops it."""
651
+ _run(
652
+ "kv-cache-size",
653
+ {"legs": legs, "model": model, "restart": restart, "timeout": timeout},
654
+ {
655
+ "legs": KV_LEGS,
656
+ "model": None,
657
+ "restart": restart,
658
+ "timeout": 1800.0,
659
+ },
660
+ image,
661
+ None,
662
+ stages.DEFAULT_ITERS,
663
+ False,
664
+ dry_run,
665
+ timeout,
666
+ save,
667
+ plot,
668
+ )
669
+
670
+
671
+ @app.command()
672
+ def vllm(
673
+ smoke_modes: str = typer.Option(
674
+ SMOKE_MODES,
675
+ "--smoke-modes",
676
+ help="graph | eager | e2m3 (fp6 KV) | lmh8 (fp8 lm_head) | spec<N>; '' = none",
677
+ ),
678
+ model: str = typer.Option("Qwen/Qwen3-0.6B", "--model"),
679
+ tokens: int = typer.Option(128, "--tokens"),
680
+ image: Optional[str] = IMAGE,
681
+ dry_run: bool = DRY,
682
+ timeout: float = typer.Option(1800.0, "--timeout"),
683
+ save: bool = SAVE,
684
+ ):
685
+ """The vLLM plugin: runner tests + greedy MJOLNIR-vs-FLASHINFER smoke runs (checkout only)."""
686
+ _run(
687
+ "vllm",
688
+ {"smoke_modes": smoke_modes, "model": model, "tokens": tokens, "timeout": timeout},
689
+ {"smoke_modes": SMOKE_MODES, "model": "Qwen/Qwen3-0.6B", "tokens": 128, "timeout": 1800.0},
690
+ image,
691
+ None,
692
+ stages.DEFAULT_ITERS,
693
+ False,
694
+ dry_run,
695
+ timeout,
696
+ save,
697
+ False,
698
+ )
699
+
700
+
701
+ def _from(order: list[str], start: str | None) -> list[str]:
702
+ if not start:
703
+ return list(order)
704
+ if start not in order:
705
+ raise typer.BadParameter(f"--from must be one of: {', '.join(order)}")
706
+ return order[order.index(start) :]
707
+
708
+
709
+ def _sequence(names: list[str], image, gate, dry_run, plot) -> int:
710
+ rc = 0
711
+ for name in names:
712
+ defaults = _DEFAULTS[name]
713
+ try:
714
+ r = _execute(
715
+ name,
716
+ dict(defaults),
717
+ dict(defaults),
718
+ image=image,
719
+ ls=None,
720
+ iters=stages.DEFAULT_ITERS,
721
+ gate=gate,
722
+ dry_run=dry_run,
723
+ timeout=180.0,
724
+ save=True,
725
+ plot=False,
726
+ )
727
+ except Exception as e: # noqa: BLE001 — one broken stage must not cost the rest of the suite
728
+ console.print(f"[red] verify {name} crashed: {type(e).__name__}: {e} — continuing[/red]")
729
+ r = 1
730
+ if r == PREFLIGHT_RC:
731
+ return r # no image / no docker: every later stage would fail the same way
732
+ if r != 0:
733
+ console.print(f"[red] verify {name} exit {r} — continuing[/red]")
734
+ rc = r
735
+ if plot and not dry_run:
736
+ _replot(load_layout())
737
+ return rc
738
+
739
+
740
+ _DEFAULTS: dict[str, dict] = {
741
+ "correctness": {"dtypes": "fp8 bf16", "match": "", "fail_fast": False, "timeout": 180.0, "quick": False},
742
+ "sweep": {"legs": KERNEL["verify.sweep"]},
743
+ "hndsweep": {"legs": KERNEL["verify.hndsweep"]},
744
+ "kvfmt": {"variants": stages.KVFMT_DEFAULT, "refs": KERNEL["verify.kvfmt.refs"], "wall_iters": 200},
745
+ "quant": {"formats": stages.QUANT_FORMATS, "batches": 2},
746
+ "ring": {"stages": stages.RING_DEFAULT, "base": "", "refs": "fi"},
747
+ "prefetch": {"distances": "0 1 2 4", "base": "--kv_format fp8", "refs": "fi"},
748
+ "pdl": {"batches": "1 8", "dtypes": "fp8", "iters": 200},
749
+ "ab": {"variants": stages.AB_DEFAULT, "refs": "fi", "wall_iters": 200},
750
+ "fp6prof": {"variants": stages.PROF_DEFAULT, "count": 3, "section_set": "full"},
751
+ }
752
+
753
+
754
+ @app.command()
755
+ def bench(
756
+ image: Optional[str] = IMAGE,
757
+ gate: bool = GATE,
758
+ dry_run: bool = DRY,
759
+ plot: bool = typer.Option(True, "--plot/--no-plot"),
760
+ start: Optional[str] = typer.Option(None, "--from", help="resume the sequence at this stage"),
761
+ ):
762
+ """Every benchmark stage with default parameters (one headline run each), then re-render the charts."""
763
+ raise typer.Exit(_sequence(_from(stages.BENCH_ORDER, start), image, gate, dry_run, plot))
764
+
765
+
766
+ @app.command("all")
767
+ def all_(
768
+ image: Optional[str] = IMAGE,
769
+ gate: bool = GATE,
770
+ dry_run: bool = DRY,
771
+ plot: bool = typer.Option(True, "--plot/--no-plot"),
772
+ start: Optional[str] = typer.Option(None, "--from", help="resume the sequence at this stage"),
773
+ ):
774
+ """Correctness + every benchmark stage, then re-render the charts."""
775
+ raise typer.Exit(_sequence(_from(stages.ALL_ORDER, start), image, gate, dry_run, plot))
776
+
777
+
778
+ @app.command("show")
779
+ def show_cmd(
780
+ stage: Optional[str] = typer.Argument(None, help="stage (default: every stage with a saved run)"),
781
+ any_run: bool = typer.Option(False, "--any", help="latest run of any parameters, not just default runs"),
782
+ ):
783
+ """Re-print the latest saved run of a stage (or every stage)."""
784
+ layout = load_layout()
785
+ names = [stage] if stage else list(stages.STAGES)
786
+ found = False
787
+ for n in names:
788
+ doc = results.latest(layout.verify_dir, n, default_only=not any_run)
789
+ if doc:
790
+ found = True
791
+ show(doc)
792
+ if not found:
793
+ console.print(f"[yellow]no saved verify runs under {layout.verify_dir} — run: mjolnir verify sweep[/yellow]")
794
+
795
+
796
+ @app.command("list")
797
+ def list_cmd():
798
+ """Stages and their saved runs."""
799
+ layout = load_layout()
800
+ for name, st in stages.STAGES.items():
801
+ rs = results.runs(layout.verify_dir, name)
802
+ last = rs[-1]["run_id"] if rs else "—"
803
+ n_def = sum(1 for r in rs if r.get("default"))
804
+ console.print(
805
+ f" [bold]{name:<12}[/bold] [dim]{st.kind:<11}[/dim] runs {len(rs):>3} (default {n_def:>2}) "
806
+ f"last {last:<16} {st.summary}"
807
+ )
808
+ console.print(f"[dim] raw results: {layout.verify_dir}[/dim]")
809
+
810
+
811
+ def main(): # pragma: no cover
812
+ app()
813
+
814
+
815
+ if __name__ == "__main__": # pragma: no cover
816
+ sys.exit(main())