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,383 @@
1
+ """The interactive benchmark site (GitHub Pages): one self-contained HTML page
2
+ built from the same raw verify documents as the PNG charts.
3
+
4
+ ``mjolnir plot --site <dir>`` writes ``<dir>/index.html`` (+ ``data.json``);
5
+ the ``pages`` workflow publishes it. Plotly (pinned, jsdelivr) draws the charts
6
+ with hover tooltips; colors and the light/dark switch follow ``theme.py``.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ from pathlib import Path
13
+
14
+ from mjolnir_cli import theme
15
+ from mjolnir_cli.config import RepoLayout
16
+
17
+ PLOTLY = "https://cdn.jsdelivr.net/npm/plotly.js-dist-min@2.35.2/plotly.min.js"
18
+
19
+
20
+ def _slim(doc: dict) -> dict:
21
+ """Drop per-kernel detail and full ncu detail pages to keep the page small."""
22
+ d = {k: v for k, v in doc.items() if not k.startswith("_")}
23
+ if "series" in d:
24
+ d["series"] = [
25
+ {**s, "points": [{k: v for k, v in p.items() if k != "kernels"} for p in s.get("points", [])]}
26
+ for s in d["series"]
27
+ ]
28
+ if "profiles" in d:
29
+ d["profiles"] = [{k: v for k, v in p.items() if k not in ("details", "pipes")} for p in d["profiles"]]
30
+ if "cases" in d:
31
+ d["cases"] = [{k: c.get(k) for k in ("label", "q", "passed", "check")} for c in d["cases"]]
32
+ return d
33
+
34
+
35
+ def build(layout: RepoLayout, out_dir: Path, data: dict | None = None) -> Path:
36
+ from mjolnir_cli.charts import e2e_rows, load_verify
37
+ from mjolnir_cli.verify.stages import STAGES
38
+
39
+ data = data if data is not None else load_verify(layout)
40
+ payload = {
41
+ "stages": {k: _slim(v) for k, v in data.items()},
42
+ "titles": {k: {"title": s.title, "summary": s.summary} for k, s in STAGES.items()},
43
+ "e2e": {
44
+ k: {
45
+ "config": v.get("config"),
46
+ "image": v.get("image"),
47
+ "cells": [
48
+ {
49
+ "concurrency": c.get("concurrency"),
50
+ "context": c.get("context") or 0,
51
+ "tg": (c.get("tg_tps") or {}).get("mean"),
52
+ "pp": (c.get("pp_tps") or {}).get("mean") if c.get("context") else None, # no ctx: no prefill
53
+ }
54
+ for c in v.get("cells", [])
55
+ ],
56
+ }
57
+ for k, v in e2e_rows(layout).items()
58
+ },
59
+ "colors": {
60
+ m.name: {
61
+ "bg": m.bg,
62
+ "panel": m.panel,
63
+ "text": m.text,
64
+ "dim": m.text_dim,
65
+ "grid": m.grid,
66
+ "mjolnir": m.mjolnir,
67
+ "fi": m.flashinfer,
68
+ "fa2": m.fa2,
69
+ "neutral": m.neutral,
70
+ }
71
+ for m in theme.MODES
72
+ },
73
+ }
74
+ out_dir.mkdir(parents=True, exist_ok=True)
75
+ (out_dir / "data.json").write_text(json.dumps(payload, separators=(",", ":")))
76
+ from mjolnir_cli.verify.stages import NOISE_PCT
77
+
78
+ html = (
79
+ _PAGE.replace("__PLOTLY__", PLOTLY)
80
+ .replace("__NOISE_PCT__", f"{NOISE_PCT:g}")
81
+ .replace("__DATA__", json.dumps(payload, separators=(",", ":")))
82
+ )
83
+ p = out_dir / "index.html"
84
+ p.write_text(html)
85
+ print(f"[mjolnir] interactive site -> {p}")
86
+ return p
87
+
88
+
89
+ _PAGE = r"""<!doctype html>
90
+ <html lang="en">
91
+ <head>
92
+ <meta charset="utf-8">
93
+ <meta name="viewport" content="width=device-width, initial-scale=1">
94
+ <title>Mjolnir Benchmarks</title>
95
+ <meta name="description" content="Mjolnir decode-attention kernel on Jetson AGX Thor vs FlashInfer and FlashAttention 2 — interactive, generated from the repo's raw benchmark data.">
96
+ <script src="__PLOTLY__"></script>
97
+ <style>
98
+ :root {
99
+ color-scheme: light;
100
+ --bg: #ffffff; --panel: #fcfcfb; --text: #0b0b0b; --dim: #52514e; --grid: #e6e5e1;
101
+ --accent: #eb6834; --link: #2a78d6;
102
+ }
103
+ @media (prefers-color-scheme: dark) {
104
+ :root:not([data-theme="light"]) {
105
+ color-scheme: dark;
106
+ --bg: #0d1117; --panel: #151b26; --text: #e6edf3; --dim: #8b949e; --grid: #21262d;
107
+ --accent: #d95926; --link: #3987e5;
108
+ }
109
+ }
110
+ :root[data-theme="dark"] {
111
+ color-scheme: dark;
112
+ --bg: #0d1117; --panel: #151b26; --text: #e6edf3; --dim: #8b949e; --grid: #21262d;
113
+ --accent: #d95926; --link: #3987e5;
114
+ }
115
+ * { box-sizing: border-box; }
116
+ body { margin: 0; background: var(--bg); color: var(--text);
117
+ font: 15px/1.55 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; }
118
+ a { color: var(--link); }
119
+ header, main, footer { max-width: 1180px; margin: 0 auto; padding: 0 16px; }
120
+ header { padding-top: 32px; display: flex; gap: 16px; align-items: flex-start; justify-content: space-between; flex-wrap: wrap; }
121
+ h1 { font-size: 30px; margin: 0 0 6px; letter-spacing: -0.01em; }
122
+ h1 span { color: var(--accent); }
123
+ .sub { color: var(--dim); margin: 0; max-width: 760px; }
124
+ .toggle { border: 1px solid var(--grid); background: var(--panel); color: var(--text); border-radius: 8px;
125
+ padding: 6px 12px; font: inherit; cursor: pointer; }
126
+ nav { position: sticky; top: 0; background: var(--bg); z-index: 2; border-bottom: 1px solid var(--grid); }
127
+ nav div { max-width: 1180px; margin: 0 auto; padding: 8px 16px; display: flex; gap: 6px; overflow-x: auto; }
128
+ nav a { white-space: nowrap; text-decoration: none; color: var(--dim); padding: 4px 10px; border-radius: 6px; font-size: 14px; }
129
+ nav a:hover { color: var(--text); background: var(--panel); }
130
+ .tiles { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 12px; margin: 24px 0 8px; }
131
+ .tile { background: var(--panel); border: 1px solid var(--grid); border-radius: 10px; padding: 14px 16px; }
132
+ .tile b { display: block; font-size: 28px; color: var(--text); }
133
+ .tile span { color: var(--dim); font-size: 13px; }
134
+ section { margin: 36px 0; }
135
+ section h2 { font-size: 21px; margin: 0 0 4px; }
136
+ section p.what { color: var(--dim); margin: 0 0 10px; }
137
+ .chart { background: var(--panel); border: 1px solid var(--grid); border-radius: 10px; min-height: 380px; }
138
+ details { margin-top: 8px; }
139
+ summary { cursor: pointer; color: var(--dim); font-size: 14px; }
140
+ table { border-collapse: collapse; width: 100%; font-size: 13px; margin-top: 8px; font-variant-numeric: tabular-nums; }
141
+ th, td { border-bottom: 1px solid var(--grid); padding: 5px 8px; text-align: right; white-space: nowrap; }
142
+ th:first-child, td:first-child { text-align: left; }
143
+ .tablewrap { overflow-x: auto; }
144
+ .empty { color: var(--dim); padding: 24px; }
145
+ footer { color: var(--dim); font-size: 13px; padding: 32px 16px 48px; }
146
+ code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 0.92em; }
147
+ </style>
148
+ </head>
149
+ <body>
150
+ <header>
151
+ <div>
152
+ <h1>⚡ <span>Mjolnir</span> benchmarks</h1>
153
+ <p class="sub">A decode-attention kernel for the Jetson AGX Thor (<code>sm_110</code>) against FlashInfer and
154
+ FlashAttention 2, at every context length from 128 to 256K tokens. Every chart here is generated from the
155
+ raw results in <a href="https://github.com/thor-arena/mjolnir/tree/main/benchmarks/raw/verify">benchmarks/raw/verify</a>
156
+ — hover any point for its value.</p>
157
+ </div>
158
+ <button class="toggle" id="toggle" type="button">Toggle theme</button>
159
+ </header>
160
+ <nav><div id="nav"></div></nav>
161
+ <main>
162
+ <div class="tiles" id="tiles"></div>
163
+ <div id="sections"></div>
164
+ </main>
165
+ <footer>
166
+ Generated by <code>mjolnir plot --site</code> from <a href="https://github.com/thor-arena/mjolnir">thor-arena/mjolnir</a>.
167
+ Reproduce any number with <code>mjolnir verify &lt;stage&gt;</code>; ncu kernel time, cold L2, base clocks;
168
+ ±__NOISE_PCT__% is the run-to-run noise of a same-run ratio.
169
+ </footer>
170
+ <script>
171
+ const D = __DATA__;
172
+ const NOISE = __NOISE_PCT__ / 100;
173
+ const LS_LABEL = L => L >= 1024 && L % 1024 === 0 ? (L / 1024) + "K" : String(L);
174
+ const NAMES = {"mjolnir": "Mjolnir · e2m3 KV", "mjolnir-fp8": "Mjolnir · fp8 KV", "fi": "FlashInfer · fp8 KV",
175
+ "fi-hnd": "FlashInfer · HND", "fa2": "FlashAttention 2 · bf16 KV"};
176
+ const STYLE = {"mjolnir": ["mjolnir", "circle", true], "mjolnir-fp8": ["mjolnir", "square-open", false],
177
+ "fi": ["fi", "diamond", true], "fi-hnd": ["fi", "diamond-open", false], "fa2": ["fa2", "triangle-up", true]};
178
+ function mode() {
179
+ const t = document.documentElement.dataset.theme;
180
+ if (t) return t;
181
+ return matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
182
+ }
183
+ function C() { return D.colors[mode()]; }
184
+ function byId(doc) { const o = {}; (doc.series || []).forEach(s => o[s.id] = s); return o; }
185
+ function pm(s, f) { const o = {}; (s.points || []).forEach(p => { if (p[f || "total_us"] != null) o[p.L] = p[f || "total_us"]; }); return o; }
186
+ function geomean(xs) { xs = xs.filter(x => x > 0); return xs.length ? Math.exp(xs.reduce((a, x) => a + Math.log(x), 0) / xs.length) : null; }
187
+ function fmtUs(t) { return t == null ? "—" : t >= 1000 ? (t / 1000).toFixed(2) + " ms" : t.toFixed(1) + " µs"; }
188
+ function layout(extra) {
189
+ const c = C();
190
+ return Object.assign({
191
+ paper_bgcolor: c.panel, plot_bgcolor: c.panel, font: {color: c.text, family: "system-ui, sans-serif", size: 13},
192
+ margin: {l: 64, r: 24, t: 24, b: 56}, hovermode: "x unified",
193
+ hoverlabel: {bgcolor: c.bg, bordercolor: c.grid, font: {color: c.text}},
194
+ legend: {orientation: "h", y: -0.2, font: {color: c.text}},
195
+ xaxis: {gridcolor: c.grid, zeroline: false, color: c.dim},
196
+ yaxis: {gridcolor: c.grid, zeroline: false, color: c.dim},
197
+ }, extra || {});
198
+ }
199
+ function lAxis(Ls) { return {type: "log", tickvals: Ls, ticktext: Ls.map(LS_LABEL), title: "context length L (tokens)", gridcolor: C().grid, color: C().dim}; }
200
+ function line(sid, xs, ys, name, hover) {
201
+ const c = C(), st = STYLE[sid] || ["mjolnir", "circle", true];
202
+ return {x: xs, y: ys, name: name || NAMES[sid] || sid, mode: "lines+markers", type: "scatter",
203
+ line: {color: c[st[0]], width: 2}, marker: {symbol: st[1], size: 9, color: st[2] ? c[st[0]] : c.panel, line: {color: c[st[0]], width: 2}},
204
+ hovertemplate: hover || "%{y:.2f}<extra>%{fullData.name}</extra>"};
205
+ }
206
+ function noiseShapes(c) { return [{type: "rect", xref: "paper", x0: 0, x1: 1, y0: 1 - NOISE, y1: 1 + NOISE, fillcolor: c.grid, line: {width: 0}, layer: "below"},
207
+ {type: "line", xref: "paper", x0: 0, x1: 1, y0: 1, y1: 1, line: {color: c.fi, width: 1.5}}]; }
208
+ function table(header, rows) {
209
+ return '<div class="tablewrap"><table><thead><tr>' + header.map(h => "<th>" + h + "</th>").join("") + "</tr></thead><tbody>" +
210
+ rows.map(r => "<tr>" + r.map(x => "<td>" + x + "</td>").join("") + "</tr>").join("") + "</tbody></table></div>";
211
+ }
212
+ const CHARTS = [];
213
+ function section(id, title, what, draw, tableHtml) {
214
+ const el = document.createElement("section");
215
+ el.id = id;
216
+ el.innerHTML = "<h2>" + title + "</h2><p class='what'>" + what + "</p><div class='chart' id='c-" + id + "'></div>" +
217
+ (tableHtml ? "<details><summary>data table</summary>" + tableHtml + "</details>" : "");
218
+ document.getElementById("sections").appendChild(el);
219
+ const a = document.createElement("a"); a.href = "#" + id; a.textContent = title;
220
+ document.getElementById("nav").appendChild(a);
221
+ CHARTS.push(() => draw("c-" + id));
222
+ }
223
+ function speedupSection() {
224
+ const doc = D.stages.sweep; if (!doc) return;
225
+ const by = byId(doc); if (!by.fi) return;
226
+ const fi = pm(by.fi), Ls = Object.keys(fi).map(Number).sort((a, b) => a - b);
227
+ section("sweep", "Mjolnir vs FlashInfer vs FlashAttention 2", "Speedup over FlashInfer at every context length (ncu kernel time; gray band = ±__NOISE_PCT__% noise).",
228
+ id => {
229
+ const traces = ["mjolnir", "mjolnir-fp8", "fa2"].filter(s => by[s]).map(s => {
230
+ const o = pm(by[s]); return line(s, Ls.filter(L => o[L]), Ls.filter(L => o[L]).map(L => fi[L] / o[L]), null, "%{y:.2f}×<extra>%{fullData.name}</extra>");
231
+ });
232
+ Plotly.react(id, traces, layout({xaxis: lAxis(Ls), yaxis: {title: "speedup over FlashInfer (×)", gridcolor: C().grid, color: C().dim, rangemode: "tozero"}, shapes: noiseShapes(C())}), {responsive: true, displaylogo: false});
233
+ },
234
+ table(["L"].concat((doc.series || []).map(s => s.label)), Ls.map(L => [LS_LABEL(L)].concat((doc.series || []).map(s => fmtUs(pm(s)[L]))))));
235
+ section("latency", "Decode latency per step", "Time for one decode step over the whole KV cache (log scale, lower is faster).",
236
+ id => {
237
+ const traces = ["fa2", "fi", "mjolnir-fp8", "mjolnir"].filter(s => by[s]).map(s => {
238
+ const o = pm(by[s]); const xs = Object.keys(o).map(Number).sort((a, b) => a - b);
239
+ return line(s, xs, xs.map(L => o[L]), null, "%{y:.1f} µs<extra>%{fullData.name}</extra>");
240
+ });
241
+ Plotly.react(id, traces, layout({xaxis: lAxis(Ls), yaxis: {type: "log", title: "µs per step", gridcolor: C().grid, color: C().dim}}), {responsive: true, displaylogo: false});
242
+ });
243
+ section("bandwidth", "Effective KV bandwidth", "Each leg's real KV bytes ÷ decode time, against the 273 GB/s LPDDR5X peak.",
244
+ id => {
245
+ const traces = ["fa2", "fi", "mjolnir-fp8", "mjolnir"].filter(s => by[s]).map(s => {
246
+ const o = pm(by[s], "gbps"); const xs = Object.keys(o).map(Number).sort((a, b) => a - b);
247
+ return line(s, xs, xs.map(L => o[L]), null, "%{y:.0f} GB/s<extra>%{fullData.name}</extra>");
248
+ });
249
+ Plotly.react(id, traces, layout({xaxis: lAxis(Ls), yaxis: {title: "GB/s", range: [0, 300], gridcolor: C().grid, color: C().dim},
250
+ shapes: [{type: "line", xref: "paper", x0: 0, x1: 1, y0: 273, y1: 273, line: {color: C().dim, width: 1.2}}]}), {responsive: true, displaylogo: false});
251
+ });
252
+ }
253
+ function ratioLines(stage, ids, title, what) {
254
+ const doc = D.stages[stage]; if (!doc) return;
255
+ const by = byId(doc); if (!by.fi) return;
256
+ const fi = pm(by.fi), Ls = Object.keys(fi).map(Number).sort((a, b) => a - b);
257
+ section(stage, title, what, id => {
258
+ const traces = ids.filter(s => by[s]).map(s => { const o = pm(by[s]); return line(s, Ls.filter(L => o[L]), Ls.filter(L => o[L]).map(L => fi[L] / o[L]), by[s].label, "%{y:.2f}×<extra>%{fullData.name}</extra>"); });
259
+ Plotly.react(id, traces, layout({xaxis: lAxis(Ls), yaxis: {title: "speedup over FlashInfer NHD (×)", gridcolor: C().grid, color: C().dim}, shapes: noiseShapes(C())}), {responsive: true, displaylogo: false});
260
+ });
261
+ }
262
+ function heatmap(stage, title, what) {
263
+ const doc = D.stages[stage]; if (!doc) return;
264
+ const by = byId(doc); if (!by.fi) return;
265
+ const fi = pm(by.fi), Ls = Object.keys(fi).map(Number).sort((a, b) => a - b);
266
+ const vs = (doc.series || []).filter(s => s.kind === "t5");
267
+ if (!vs.length) return;
268
+ const z = vs.map(s => { const o = pm(s); return Ls.map(L => o[L] ? fi[L] / o[L] : null); });
269
+ const zlog = z.map(r => r.map(v => v ? Math.max(-1, Math.min(1, Math.log2(v))) : null));
270
+ section(stage, title, what, id => {
271
+ const c = C();
272
+ Plotly.react(id, [{type: "heatmap", x: Ls.map(LS_LABEL), y: vs.map(s => s.label), z: zlog, customdata: z, zmin: -1, zmax: 1,
273
+ colorscale: [[0, c.fi], [0.5, c.neutral], [1, c.mjolnir]], xgap: 2, ygap: 2, showscale: false,
274
+ text: z.map(r => r.map(v => v ? v.toFixed(2) + "×" : "")), texttemplate: "%{text}", textfont: {color: c.text},
275
+ hovertemplate: "%{y} @ L=%{x}: %{customdata:.3f}× vs FlashInfer<extra></extra>"}],
276
+ layout({margin: {l: 150, r: 24, t: 16, b: 48}, height: 120 + 44 * vs.length, xaxis: {color: c.dim, title: "context length L"}, yaxis: {color: c.text, autorange: "reversed"}, hovermode: "closest"}), {responsive: true, displaylogo: false});
277
+ }, table(["variant", "flags"].concat(Ls.map(LS_LABEL)), vs.map((s, i) => [s.label, "<code>" + (s.flags || "(default)") + "</code>"].concat(z[i].map(v => v ? v.toFixed(2) + "×" : "—")))));
278
+ }
279
+ function quantSection() {
280
+ const doc = D.stages.quant; if (!doc) return;
281
+ const by = {}, bits = {};
282
+ (doc.rows || []).forEach(r => { if (r.rel_l2 != null) { (by[r.format] = by[r.format] || []).push(r); bits[r.format] = r.bits; } });
283
+ const fmts = Object.keys(by).sort((a, b) => (bits[b] || 0) - (bits[a] || 0));
284
+ if (!fmts.length) return;
285
+ const mean = f => by[f].reduce((a, r) => a + r.rel_l2, 0) / by[f].length;
286
+ section("quant", "KV format accuracy", "Relative L2 error of the kernel output vs attention over unquantized KV — bar = mean over L, whisker = min–max over L.", id => {
287
+ const c = C();
288
+ const m = fmts.map(mean);
289
+ Plotly.react(id, [{type: "bar", x: fmts.map(f => f + " · " + bits[f] + " bit"), y: m, marker: {color: c.mjolnir},
290
+ error_y: {type: "data", symmetric: false, array: fmts.map((f, i) => Math.max(...by[f].map(r => r.rel_l2)) - m[i]),
291
+ arrayminus: fmts.map((f, i) => m[i] - Math.min(...by[f].map(r => r.rel_l2))), color: c.dim, thickness: 1.5, width: 6},
292
+ customdata: fmts.map(f => by[f].map(r => LS_LABEL(r.L) + ": " + r.rel_l2.toFixed(4)).join("<br>")),
293
+ hovertemplate: "%{x}<br>mean %{y:.4f}<br>%{customdata}<extra></extra>"}],
294
+ layout({yaxis: {title: "relative L2 error (lower is better)", gridcolor: c.grid, color: c.dim}, xaxis: {color: c.text}, hovermode: "closest", showlegend: false}),
295
+ {responsive: true, displaylogo: false});
296
+ }, table(["format", "bits"].concat(by[fmts[0]].map(r => LS_LABEL(r.L))), fmts.map(f => [f, bits[f]].concat(by[f].map(r => r.rel_l2.toFixed(4))))));
297
+ }
298
+ function pdlSection() {
299
+ const doc = D.stages.pdl; if (!doc) return;
300
+ const rows = (doc.rows || []).filter(r => r.delta_pct != null);
301
+ const bs = [...new Set(rows.map(r => r.batches))];
302
+ section("pdl", "Programmatic dependent launch", "Wall-clock change with PDL on — orange = faster, blue = slower; hatched = batch 8; gray band = ±__NOISE_PCT__% noise.", id => {
303
+ const c = C();
304
+ Plotly.react(id, bs.map((b, i) => { const rs = rows.filter(r => r.batches === b);
305
+ return {type: "bar", name: "batch " + b, x: rs.map(r => LS_LABEL(r.L)), y: rs.map(r => r.delta_pct),
306
+ marker: {color: rs.map(r => r.delta_pct < 0 ? c.mjolnir : c.fi), pattern: {shape: i ? "/" : "", fgcolor: c.panel, solidity: 0.35}},
307
+ hovertemplate: "batch " + b + " @ L=%{x}: %{y:+.1f}%<extra></extra>"}; }),
308
+ layout({barmode: "group", yaxis: {title: "Δ wall clock (%)", gridcolor: c.grid, color: c.dim}, xaxis: {color: c.dim}, hovermode: "closest",
309
+ shapes: [{type: "rect", xref: "paper", x0: 0, x1: 1, y0: -7, y1: 7, fillcolor: c.grid, line: {width: 0}, layer: "below"}]}), {responsive: true, displaylogo: false});
310
+ });
311
+ }
312
+ function profSection() {
313
+ const doc = D.stages.fp6prof; if (!doc) return;
314
+ const ps = (doc.profiles || []).filter(p => p.ok);
315
+ if (!ps.length) return;
316
+ const labs = [...new Set(ps.map(p => p.label))], base = labs[0];
317
+ const Ls = [...new Set(ps.map(p => p.L))].sort((a, b) => a - b);
318
+ const get = (lab, L) => ps.find(p => p.label === lab && p.L === L);
319
+ section("fp6prof", "Bytes vs time per KV format", "Full ncu profiles: L2 bytes read and kernel time relative to " + base + " (the ideal is the bits ratio).", id => {
320
+ const c = C(), traces = [];
321
+ labs.slice(1).forEach((lab, i) => {
322
+ const t = Ls.map(L => { const b = get(base, L), p = get(lab, L); return b && p ? p.metrics["gpu__time_duration.sum"] / b.metrics["gpu__time_duration.sum"] : null; });
323
+ const s = Ls.map(L => { const b = get(base, L), p = get(lab, L); const k = "lts__t_sectors_op_read.sum"; return b && p && b.metrics[k] ? p.metrics[k] / b.metrics[k] : null; });
324
+ traces.push({type: "bar", name: lab + " · kernel time", x: Ls.map(LS_LABEL), y: t, marker: {color: c.mjolnir, opacity: i ? 0.55 : 1}});
325
+ traces.push({type: "bar", name: lab + " · L2 bytes", x: Ls.map(LS_LABEL), y: s, marker: {color: c.dim, opacity: i ? 0.55 : 1}});
326
+ });
327
+ Plotly.react(id, traces, layout({barmode: "group", yaxis: {title: "ratio vs " + base, range: [0, 1.15], gridcolor: c.grid, color: c.dim}, xaxis: {color: c.dim}, hovermode: "x unified"}), {responsive: true, displaylogo: false});
328
+ });
329
+ }
330
+ function e2eSection() {
331
+ const rows = D.e2e; const keys = ["NVFP4", "NVFP4_FA2", "NVFP4_MJOLNIR", "NVFP4_MJOLNIR_E2M3"].filter(k => rows[k]);
332
+ if (!(rows.NVFP4_MJOLNIR || rows.NVFP4_MJOLNIR_E2M3) || keys.length < 2) return;
333
+ const label = {"NVFP4_MJOLNIR": "Mjolnir · fp8 KV", "NVFP4_MJOLNIR_E2M3": "Mjolnir · fp6 KV", "NVFP4": "FlashInfer · official vLLM", "NVFP4_FA2": "FlashAttention 2 · official vLLM"};
334
+ const col = {"NVFP4_MJOLNIR": "mjolnir", "NVFP4_MJOLNIR_E2M3": "mjolnir", "NVFP4": "fi", "NVFP4_FA2": "fa2"};
335
+ section("e2e", "End-to-end decode throughput", "The official vLLM image (native backends) vs the Mjolnir image, same model and config (llama-benchy, gated clean windows).", id => {
336
+ const c = C();
337
+ Plotly.react(id, keys.map(k => { const cs = rows[k].cells.filter(x => x.tg != null);
338
+ return {type: "bar", name: label[k] || k, x: cs.map(x => "c" + x.concurrency + " · " + (x.context ? LS_LABEL(x.context) + " ctx" : "no ctx")), y: cs.map(x => x.tg),
339
+ marker: {color: c[col[k]] || c.dim, opacity: k === "NVFP4_MJOLNIR" ? 0.55 : 1}, hovertemplate: "%{y:.1f} tok/s<extra>%{fullData.name}</extra>"}; }),
340
+ layout({barmode: "group", yaxis: {title: "decode tokens/s", gridcolor: c.grid, color: c.dim}, xaxis: {color: c.dim}}), {responsive: true, displaylogo: false});
341
+ });
342
+ }
343
+ function tiles() {
344
+ const doc = D.stages.sweep, el = document.getElementById("tiles"), out = [];
345
+ if (doc) {
346
+ const by = byId(doc), ours = by.mjolnir || by["mjolnir-fp8"];
347
+ if (ours && by.fi) {
348
+ const fi = pm(by.fi), o = pm(ours), sp = Object.keys(o).filter(L => fi[L]).map(L => fi[L] / o[L]);
349
+ out.push(["" + geomean(sp).toFixed(2) + "×", "geomean speedup over FlashInfer (all L)"]);
350
+ out.push(["" + Math.max(...sp).toFixed(2) + "×", "best speedup over FlashInfer"]);
351
+ if (by.fa2) { const fa = pm(by.fa2); const s2 = Object.keys(o).filter(L => fa[L]).map(L => fa[L] / o[L]); out.push([geomean(s2).toFixed(2) + "×", "geomean speedup over FlashAttention 2"]); }
352
+ }
353
+ }
354
+ const corr = D.stages.correctness;
355
+ if (corr) { const cs = corr.cases || []; out.push([cs.filter(c => c.passed).length + " / " + cs.length, "correctness checks passing"]); }
356
+ if (!out.length) out.push(["—", "no verify runs yet — run mjolnir verify bench"]);
357
+ el.innerHTML = out.map(t => "<div class='tile'><b>" + t[0] + "</b><span>" + t[1] + "</span></div>").join("");
358
+ }
359
+ function build() {
360
+ tiles();
361
+ speedupSection();
362
+ ratioLines("hndsweep", ["fi-hnd", "mjolnir-fp8"], "Head-major KV pages", "Same data on NHD vs HND pages — FlashInfer gains a little, Mjolnir more.");
363
+ heatmap("kvfmt", "KV layouts × formats", "Speedup over FlashInfer per KV format and page layout.");
364
+ quantSection();
365
+ heatmap("ring", "KV ring depth", "TMA → MMA ring depth (--kv_stages).");
366
+ heatmap("prefetch", "L2 prefetch", "TMA L2 prefetch distance (--l2_prefetch).");
367
+ pdlSection();
368
+ heatmap("ab", "Flag A/B", "Harness flag sets (default: evict-first × PDL).");
369
+ profSection();
370
+ e2eSection();
371
+ if (!document.getElementById("sections").children.length)
372
+ document.getElementById("sections").innerHTML = "<p class='empty'>No benchmark data yet — run <code>mjolnir verify bench</code> and re-publish.</p>";
373
+ }
374
+ function redraw() { CHARTS.forEach(f => f()); }
375
+ document.getElementById("toggle").addEventListener("click", () => {
376
+ document.documentElement.dataset.theme = mode() === "dark" ? "light" : "dark"; redraw();
377
+ });
378
+ matchMedia("(prefers-color-scheme: dark)").addEventListener("change", redraw);
379
+ build(); redraw();
380
+ </script>
381
+ </body>
382
+ </html>
383
+ """
mjolnir_cli/legs.py ADDED
@@ -0,0 +1,109 @@
1
+ """Default legs of every benchmark — the one place that decides what gets
2
+ measured against what.
3
+
4
+ A **serving leg** is ``(side, config)``: ``official`` serves the config on the
5
+ upstream ``vllm/vllm-openai`` image (native backends are only ever measured
6
+ there), ``mjolnir`` on the Mjolnir image. Each command renders the table in
7
+ its own CLI syntax — ``verify`` stages take ``side:config``
8
+ (:func:`verify_legs`), ``bench`` commands take ``<image>:<config>`` or a bare
9
+ config served from ``--image`` (:func:`bench_legs`).
10
+
11
+ A **kernel leg** is a harness leg id from :mod:`mjolnir_cli.verify.legs`
12
+ (``fi``, ``fi-hnd``, ``fa2``, ``mjolnir-fp8``, ``mjolnir``), used by the ncu
13
+ stages.
14
+
15
+ Change a list here and every command, its ``--help`` default and the
16
+ "is this a headline (default) run" check follow. ``--legs`` / ``--backends``
17
+ on the command line still override one run.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ from mjolnir_cli.config import BASELINE_QUANT, DEFAULT_QUANT, FAST_QUANT, STOCK_IMAGE
23
+
24
+ OFFICIAL, MJOLNIR = "official", "mjolnir"
25
+ Leg = tuple[str, str] # (side, config)
26
+
27
+ FP8 = "NVFP4_MJOLNIR" # Mjolnir, fp8 KV
28
+ FP6 = "NVFP4_MJOLNIR_E2M3" # Mjolnir, fp6 (e2m3) KV
29
+
30
+
31
+ def _uniq(legs: list[Leg]) -> tuple[Leg, ...]:
32
+ """Drop repeats, keep order (DEFAULT_QUANT may equal FP6 or FAST_QUANT)."""
33
+ return tuple(dict.fromkeys(legs))
34
+
35
+
36
+ # ── serving legs ────────────────────────────────────────────────────────────
37
+ SERVING: dict[str, tuple[Leg, ...]] = {
38
+ # vLLM's KV sizing at startup: stock fp8 KV, Mjolnir fp8 / fp6 KV, and the
39
+ # fast config (its fp8 lm_head copy costs KV memory)
40
+ "verify.kv-cache-size": _uniq([(OFFICIAL, BASELINE_QUANT), (MJOLNIR, FP8), (MJOLNIR, FP6), (MJOLNIR, FAST_QUANT)]),
41
+ # MTP acceptance and decode rate per draft count
42
+ "verify.spec": _uniq([(OFFICIAL, BASELINE_QUANT), (MJOLNIR, FP8), (MJOLNIR, FP6), (MJOLNIR, FAST_QUANT)]),
43
+ # torch-profiler trace of one decode window
44
+ "verify.profile": _uniq([(OFFICIAL, BASELINE_QUANT), (MJOLNIR, FP6), (MJOLNIR, FAST_QUANT)]),
45
+ # llama-benchy A/B, one leg after the other
46
+ "bench.ab": _uniq([(OFFICIAL, BASELINE_QUANT), (MJOLNIR, DEFAULT_QUANT), (MJOLNIR, FAST_QUANT)]),
47
+ # official vs Mjolnir end to end: the first official and the first mjolnir
48
+ # leg are the headline pair (comparison table, README infographic); further
49
+ # mjolnir legs are measured after it (`bench e2e --also`) and join the charts
50
+ "bench.e2e": _uniq([(OFFICIAL, BASELINE_QUANT), (MJOLNIR, DEFAULT_QUANT), (MJOLNIR, FAST_QUANT)]),
51
+ # model quality (tool-eval-bench)
52
+ "bench.accuracy": _uniq(
53
+ [
54
+ (OFFICIAL, BASELINE_QUANT),
55
+ (MJOLNIR, FP8),
56
+ (MJOLNIR, FP6),
57
+ (MJOLNIR, FAST_QUANT),
58
+ ]
59
+ ),
60
+ }
61
+
62
+ # Opt-in serving legs: measured only when asked (`bench e2e --optional`), after
63
+ # the default legs — other speculative decoders on both images. They land in
64
+ # the history (`mjolnir history`, `bench compare --config <cfg>`) but never
65
+ # replace the headline pair, so the README stays on the default legs.
66
+ OPTIONAL: dict[str, tuple[Leg, ...]] = {
67
+ "bench.e2e": (
68
+ (OFFICIAL, "NVFP4_DSPARK"), # stock vLLM: FlashInfer, fp8 KV, DSpark K=7
69
+ (OFFICIAL, "NVFP4_DFLASH2"), # stock vLLM: FlashInfer, fp8 KV, DFlash2 K=7
70
+ (MJOLNIR, "NVFP4_MJOLNIR_E2M3_DSPARK"), # Mjolnir fp6 KV, DSpark K=7
71
+ (MJOLNIR, "NVFP4_MJOLNIR_E2M3_DFLASH2"), # Mjolnir fp6 KV, DFlash2 K=7
72
+ ),
73
+ }
74
+
75
+ # ── kernel legs (ncu stages) ────────────────────────────────────────────────
76
+ KERNEL: dict[str, str] = {
77
+ "verify.sweep": "fi,fa2,mjolnir-fp8,mjolnir",
78
+ "verify.hndsweep": "fi,fi-hnd,mjolnir-fp8",
79
+ "verify.kvfmt.refs": "fi,fi-hnd",
80
+ # the autoresearch keep/discard harness: FlashInfer vs the kernel under test
81
+ "autoresearch.evaluate": "fi,mjolnir",
82
+ }
83
+
84
+ # ── `verify vllm`: greedy MJOLNIR-vs-FlashInfer smoke modes ────────────────
85
+ SMOKE_MODES = "graph eager e2m3 lmh8"
86
+
87
+
88
+ def verify_legs(name: str) -> str:
89
+ """``official:NVFP4,mjolnir:NVFP4_MJOLNIR,…`` — the ``verify --legs`` form."""
90
+ return ",".join(f"{side}:{cfg}" for side, cfg in SERVING[name])
91
+
92
+
93
+ def bench_legs(name: str) -> str:
94
+ """``vllm/vllm-openai:…:NVFP4,NVFP4_MJOLNIR_E2M3,…`` — the ``bench --backends``
95
+ form (official legs pinned to the upstream image, Mjolnir legs bare)."""
96
+ return ",".join(f"{STOCK_IMAGE}:{cfg}" if side == OFFICIAL else cfg for side, cfg in SERVING[name])
97
+
98
+
99
+ def side_configs(name: str, side: str) -> list[str]:
100
+ return [cfg for s, cfg in SERVING[name] if s == side]
101
+
102
+
103
+ def parse_leg(text: str, default_side: str = MJOLNIR) -> Leg:
104
+ """``official:NVFP4_DSPARK`` / ``mjolnir:NVFP4_MJOLNIR_E2M3_DSPARK`` / a bare
105
+ config (``default_side``) -> (side, config)."""
106
+ side, sep, cfg = text.strip().partition(":")
107
+ if sep and side.lower() in (OFFICIAL, MJOLNIR):
108
+ return side.lower(), cfg.strip()
109
+ return default_side, text.strip()