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.
- mjolnir_cli/__init__.py +8 -0
- mjolnir_cli/__main__.py +4 -0
- mjolnir_cli/accuracy.py +333 -0
- mjolnir_cli/autoresearch.py +539 -0
- mjolnir_cli/benchy.py +475 -0
- mjolnir_cli/bundled/configs/Qwen/Qwen3.8-27B/NVFP4/config.yaml +78 -0
- mjolnir_cli/bundled/configs/Qwen/Qwen3.8-27B/NVFP4_DFLASH2/config.yaml +103 -0
- mjolnir_cli/bundled/configs/Qwen/Qwen3.8-27B/NVFP4_DSPARK/config.yaml +102 -0
- mjolnir_cli/bundled/configs/Qwen/Qwen3.8-27B/NVFP4_FA2/config.yaml +84 -0
- mjolnir_cli/bundled/configs/Qwen/Qwen3.8-27B/NVFP4_FA4hd256/config.yaml +104 -0
- mjolnir_cli/bundled/configs/Qwen/Qwen3.8-27B/NVFP4_MJOLNIR/config.yaml +86 -0
- mjolnir_cli/bundled/configs/Qwen/Qwen3.8-27B/NVFP4_MJOLNIR_E2M3/config.yaml +93 -0
- mjolnir_cli/bundled/configs/Qwen/Qwen3.8-27B/NVFP4_MJOLNIR_E2M3_DFLASH2/config.yaml +123 -0
- mjolnir_cli/bundled/configs/Qwen/Qwen3.8-27B/NVFP4_MJOLNIR_E2M3_DSPARK/config.yaml +124 -0
- mjolnir_cli/bundled/configs/Qwen/Qwen3.8-27B/NVFP4_MJOLNIR_E2M3_LMH8/.env +2 -0
- mjolnir_cli/bundled/configs/Qwen/Qwen3.8-27B/NVFP4_MJOLNIR_E2M3_LMH8/config.yaml +98 -0
- mjolnir_cli/bundled/configs/Qwen/Qwen3.8-27B/NVFP4_MJOLNIR_E2M3_LMH8_VT32K/.env +4 -0
- mjolnir_cli/bundled/configs/Qwen/Qwen3.8-27B/NVFP4_MJOLNIR_E2M3_LMH8_VT32K/config.yaml +98 -0
- mjolnir_cli/bundled/configs/Qwen/Qwen3.8-27B/NVFP4_MJOLNIR_E2M3_LMH8_VT64K/.env +4 -0
- mjolnir_cli/bundled/configs/Qwen/Qwen3.8-27B/NVFP4_MJOLNIR_E2M3_LMH8_VT64K/config.yaml +102 -0
- mjolnir_cli/bundled/configs/Qwen/Qwen3.8-27B/NVFP4_MJOLNIR_E2M3_VT32K/.env +3 -0
- mjolnir_cli/bundled/configs/Qwen/Qwen3.8-27B/NVFP4_MJOLNIR_E2M3_VT32K/config.yaml +98 -0
- mjolnir_cli/bundled/configs/Qwen/Qwen3.8-27B/draft_vocab.json +1 -0
- mjolnir_cli/bundled/configs/Qwen/Qwen3.8-27B/kv_scales.json +35197 -0
- mjolnir_cli/bundled/configs/Qwen/Qwen3.8_chat_template.jinja +456 -0
- mjolnir_cli/bundled/docker/litellm/docker-compose.yaml +113 -0
- mjolnir_cli/bundled/docker/litellm/litellm.env.example +51 -0
- mjolnir_cli/bundled/docker/litellm/litellm_config.template.example.yaml +192 -0
- mjolnir_cli/bundled/docker/perses/README.md +89 -0
- mjolnir_cli/bundled/docker/perses/dashboards/global-datasource.yaml +17 -0
- mjolnir_cli/bundled/docker/perses/dashboards/performance_statistics.yaml +771 -0
- mjolnir_cli/bundled/docker/perses/dashboards/project.yaml +13 -0
- mjolnir_cli/bundled/docker/perses/dashboards/query_statistics.yaml +399 -0
- mjolnir_cli/bundled/docker/perses/docker-compose.yaml +51 -0
- mjolnir_cli/bundled/docker/perses/perses.yml +16 -0
- mjolnir_cli/bundled/docker/perses/prometheus.template.yaml +20 -0
- mjolnir_cli/bundled/scripts/hw/setup-thor.sh +1287 -0
- mjolnir_cli/cachectl.py +146 -0
- mjolnir_cli/charts.py +1687 -0
- mjolnir_cli/cli.py +1480 -0
- mjolnir_cli/clidoc.py +170 -0
- mjolnir_cli/config.py +346 -0
- mjolnir_cli/dockerctl.py +533 -0
- mjolnir_cli/drivers/fa2_driver.py +89 -0
- mjolnir_cli/drivers/fi_driver.py +134 -0
- mjolnir_cli/e2e.py +302 -0
- mjolnir_cli/fonts/InterDisplay-Bold.ttf +0 -0
- mjolnir_cli/fonts/InterDisplay-Medium.ttf +0 -0
- mjolnir_cli/fonts/InterDisplay-Regular.ttf +0 -0
- mjolnir_cli/fonts/InterDisplay-SemiBold.ttf +0 -0
- mjolnir_cli/fonts/OFL.txt +92 -0
- mjolnir_cli/gate.py +233 -0
- mjolnir_cli/history.py +159 -0
- mjolnir_cli/interactive.py +383 -0
- mjolnir_cli/legs.py +109 -0
- mjolnir_cli/litellm.py +254 -0
- mjolnir_cli/perses.py +214 -0
- mjolnir_cli/picker.py +70 -0
- mjolnir_cli/readme.py +1020 -0
- mjolnir_cli/stack.py +340 -0
- mjolnir_cli/tasks.py +122 -0
- mjolnir_cli/theme.py +281 -0
- mjolnir_cli/thor.py +108 -0
- mjolnir_cli/verify/__init__.py +2 -0
- mjolnir_cli/verify/cli.py +816 -0
- mjolnir_cli/verify/docker.py +334 -0
- mjolnir_cli/verify/kvcache.py +180 -0
- mjolnir_cli/verify/legs.py +134 -0
- mjolnir_cli/verify/ncu.py +141 -0
- mjolnir_cli/verify/profile.py +312 -0
- mjolnir_cli/verify/report.py +177 -0
- mjolnir_cli/verify/results.py +157 -0
- mjolnir_cli/verify/spec.py +279 -0
- mjolnir_cli/verify/stages.py +817 -0
- mjolnir_cli-0.2.0.dist-info/METADATA +87 -0
- mjolnir_cli-0.2.0.dist-info/RECORD +80 -0
- mjolnir_cli-0.2.0.dist-info/WHEEL +4 -0
- mjolnir_cli-0.2.0.dist-info/entry_points.txt +2 -0
- mjolnir_cli-0.2.0.dist-info/licenses/LICENSE +202 -0
- 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 <stage></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()
|