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
mjolnir_cli/readme.py
ADDED
|
@@ -0,0 +1,1020 @@
|
|
|
1
|
+
"""README + docs generation — every number, table and chart reference is
|
|
2
|
+
computed from the committed raw data.
|
|
3
|
+
|
|
4
|
+
``mjolnir plot --readme`` renders:
|
|
5
|
+
|
|
6
|
+
* ``README.md`` from ``README.md.j2``
|
|
7
|
+
* ``docs/kernel/**/*.md`` from the ``*.md.j2`` next to them
|
|
8
|
+
|
|
9
|
+
Inputs: the latest default run of each ``mjolnir verify`` stage
|
|
10
|
+
(``benchmarks/raw/verify/``), the e2e history (``benchmarks/history.jsonl``),
|
|
11
|
+
the chart manifest (``assets/benchmarks/manifest.json``, written by
|
|
12
|
+
``charts.render_all``) and the vLLM base tag in the Dockerfile. Re-rendering
|
|
13
|
+
is idempotent: the output only changes when the raw data does (CI checks it).
|
|
14
|
+
|
|
15
|
+
Template contract (Jinja2, StrictUndefined — a typo fails loud):
|
|
16
|
+
|
|
17
|
+
* ``ctx`` — a ``ReadmeContext``: versions, headline numbers, the
|
|
18
|
+
per-stage markdown tables (``ctx.tables[stage]``),
|
|
19
|
+
the per-stage findings (``ctx.findings[stage]``) and
|
|
20
|
+
``ctx.pending`` (stages without a run yet)
|
|
21
|
+
* ``chart(name, alt)`` — a light/dark ``<picture>`` for a chart in the
|
|
22
|
+
manifest, ``""`` when the chart has no data yet
|
|
23
|
+
* ``has(stage)`` — the stage has a saved run
|
|
24
|
+
* ``root`` — relative path from the rendered file to the repo root
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import json
|
|
30
|
+
import re
|
|
31
|
+
from dataclasses import dataclass, field
|
|
32
|
+
from pathlib import Path
|
|
33
|
+
|
|
34
|
+
import jinja2
|
|
35
|
+
|
|
36
|
+
from mjolnir_cli.config import DEFAULT_MODEL, IMAGE_REPO, RepoLayout
|
|
37
|
+
from mjolnir_cli.verify import results
|
|
38
|
+
from mjolnir_cli.verify.stages import ALL_LS, NOISE_PCT, STAGES
|
|
39
|
+
|
|
40
|
+
_GREEN = "#16a34a"
|
|
41
|
+
_RED = "#dc2626"
|
|
42
|
+
_GREY = "grey"
|
|
43
|
+
_MINUS = "−"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# ── formatting ──────────────────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def L_label(L: int) -> str:
|
|
50
|
+
return f"{L // 1024}K" if L >= 1024 and L % 1024 == 0 else str(L)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def fmt_us(t: float | None) -> str:
|
|
54
|
+
if t is None:
|
|
55
|
+
return "—"
|
|
56
|
+
return f"{t / 1000:.2f} ms" if t >= 1000 else f"{t:.1f} µs"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def ratio_md(r: float | None, bold_wins: bool = True) -> str:
|
|
60
|
+
"""``1.25×`` colored: green = faster beyond the ±NOISE_PCT noise band, grey =
|
|
61
|
+
within noise, red = slower beyond noise (GitHub renders the LaTeX color)."""
|
|
62
|
+
if r is None:
|
|
63
|
+
return "—"
|
|
64
|
+
noise = NOISE_PCT / 100
|
|
65
|
+
color = _GREEN if r >= 1 + noise else (_GREY if r >= 1 - noise else _RED)
|
|
66
|
+
text = f"{r:.2f}\\times"
|
|
67
|
+
if bold_wins and r >= 1 + noise:
|
|
68
|
+
text = f"\\mathbf{{{r:.2f}\\times}}"
|
|
69
|
+
return f"$\\color{{{color}}}{{{text}}}$"
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def color(text: str, color: str = "grey") -> str:
|
|
73
|
+
"""Render grey LaTeX text, escaping whitespace for math mode."""
|
|
74
|
+
return rf"$\color{{{color}}}\scriptstyle\text{{{text}}}$"
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def pct_md(p: float | None, lower_is_better: bool = True) -> str:
|
|
78
|
+
"""A colored % delta: green = better beyond the noise band, grey = within
|
|
79
|
+
noise, red = worse beyond noise. Times: lower is better; throughput: higher."""
|
|
80
|
+
if p is None:
|
|
81
|
+
return "—"
|
|
82
|
+
good = -p if lower_is_better else p
|
|
83
|
+
color = _GREEN if good >= NOISE_PCT else (_RED if good <= -NOISE_PCT else _GREY)
|
|
84
|
+
sign = "+" if p > 0 else (_MINUS if p < 0 else "")
|
|
85
|
+
# We need to double-escape the `\\\%` as Github refuses to render the `%` symbol otherwise
|
|
86
|
+
return f"$\\color{{{color}}}{{\\text{{{sign}{abs(p):.1f}\\\\%}}}}$"
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _table(header: list[str], rows: list[list[str]], align: list[str] | None = None) -> str:
|
|
90
|
+
align = align or ["---"] + ["--:"] * (len(header) - 1)
|
|
91
|
+
out = ["| " + " | ".join(header) + " |", "|" + "|".join(align) + "|"]
|
|
92
|
+
out += ["| " + " | ".join(r) + " |" for r in rows]
|
|
93
|
+
return "\n".join(out)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
# ── per-stage tables + findings ─────────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
SHORT = {
|
|
100
|
+
"fi": "FlashInfer",
|
|
101
|
+
"fi-hnd": "FlashInfer HND",
|
|
102
|
+
"fa2": "FA2",
|
|
103
|
+
"mjolnir": "Mjolnir e2m3",
|
|
104
|
+
"mjolnir-fp8": "Mjolnir fp8",
|
|
105
|
+
}
|
|
106
|
+
REF_SHORT = {"fi": "FI", "fi-hnd": "FI-HND", "fa2": "FA2"}
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _short(s: dict) -> str:
|
|
110
|
+
return SHORT.get(s["id"], s["label"])
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _series_time_table(doc: dict, ref_ids: tuple[str, ...] = ("fi",)) -> str:
|
|
114
|
+
"""Rows = L; columns = each series' time, then Mjolnir's speedup vs each ref."""
|
|
115
|
+
ser = doc.get("series", [])
|
|
116
|
+
by = results.series_by_id(doc)
|
|
117
|
+
refs = [by[r] for r in ref_ids if r in by]
|
|
118
|
+
ours = [s for s in ser if s.get("kind") == "t5"]
|
|
119
|
+
Ls = sorted({p["L"] for s in ser for p in s.get("points", [])})
|
|
120
|
+
header = ["L"] + [_short(s) for s in ser]
|
|
121
|
+
pairs = [(o, r) for r in refs for o in ours]
|
|
122
|
+
header += [f"{_short(o).replace('Mjolnir ', '')} vs {REF_SHORT.get(r['id'], r['label'])}" for o, r in pairs]
|
|
123
|
+
rows = []
|
|
124
|
+
for L in Ls:
|
|
125
|
+
times = {s["id"]: results.point_map(s).get(L) for s in ser}
|
|
126
|
+
best = min((t for t in times.values() if t), default=None)
|
|
127
|
+
r = [f"**{L_label(L)}**"]
|
|
128
|
+
for s in ser:
|
|
129
|
+
t = times[s["id"]]
|
|
130
|
+
cell = fmt_us(t)
|
|
131
|
+
r.append(f"**{cell}**" if t is not None and t == best else cell)
|
|
132
|
+
for o, ref in pairs:
|
|
133
|
+
rt, t = times.get(ref["id"]), times.get(o["id"])
|
|
134
|
+
r.append(ratio_md(rt / t) if rt and t else "—")
|
|
135
|
+
rows.append(r)
|
|
136
|
+
geo = ["geomean"] + [""] * len(ser)
|
|
137
|
+
for o, ref in pairs:
|
|
138
|
+
g = results.geomean(results.speedup(ref, o).values())
|
|
139
|
+
geo.append(ratio_md(g) if g else "—")
|
|
140
|
+
rows.append(geo)
|
|
141
|
+
return _table(header, rows)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _variant_table(doc: dict, ref_id: str = "fi") -> str:
|
|
145
|
+
"""Rows = variants; columns = speedup vs the reference at each L + geomean."""
|
|
146
|
+
by = results.series_by_id(doc)
|
|
147
|
+
if ref_id not in by:
|
|
148
|
+
return ""
|
|
149
|
+
ref = by[ref_id]
|
|
150
|
+
Ls = sorted(results.point_map(ref))
|
|
151
|
+
header = ["variant", "flags"] + [L_label(L) for L in Ls] + ["geomean"]
|
|
152
|
+
rows = []
|
|
153
|
+
best_per_L: dict[int, float] = {}
|
|
154
|
+
variants = [s for s in doc.get("series", []) if s.get("kind") == "t5"]
|
|
155
|
+
for s in variants:
|
|
156
|
+
for L, v in results.speedup(ref, s).items():
|
|
157
|
+
best_per_L[L] = max(best_per_L.get(L, 0), v)
|
|
158
|
+
for s in variants:
|
|
159
|
+
sp = results.speedup(ref, s)
|
|
160
|
+
cells = []
|
|
161
|
+
for L in Ls:
|
|
162
|
+
v = sp.get(L)
|
|
163
|
+
c = ratio_md(v, bold_wins=False) if v else "—"
|
|
164
|
+
cells.append(f"**{c}**" if v and v == best_per_L.get(L) else c)
|
|
165
|
+
g = results.geomean(sp.values())
|
|
166
|
+
rows.append(
|
|
167
|
+
[f"`{s['label']}`", f"`{s['flags']}`" if s.get("flags") else "(default)"]
|
|
168
|
+
+ cells
|
|
169
|
+
+ [ratio_md(g) if g else "—"]
|
|
170
|
+
)
|
|
171
|
+
return _table(header, rows, ["---", "---"] + ["--:"] * (len(Ls) + 1))
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _wall_table(doc: dict) -> str | None:
|
|
175
|
+
rows = [r for r in doc.get("rows", []) if r.get("median_us")]
|
|
176
|
+
if not rows:
|
|
177
|
+
return None
|
|
178
|
+
labels = {s["id"]: s["label"] for s in doc.get("series", [])}
|
|
179
|
+
sids = list(dict.fromkeys(r["series"] for r in rows))
|
|
180
|
+
Ls = sorted({r["L"] for r in rows})
|
|
181
|
+
base = sids[0]
|
|
182
|
+
header = ["L"] + [labels.get(s, s) for s in sids]
|
|
183
|
+
out = []
|
|
184
|
+
for L in Ls:
|
|
185
|
+
m = {r["series"]: r["median_us"] for r in rows if r["L"] == L}
|
|
186
|
+
cells = [f"**{L_label(L)}**"]
|
|
187
|
+
for s in sids:
|
|
188
|
+
v = m.get(s)
|
|
189
|
+
if v is None:
|
|
190
|
+
cells.append("—")
|
|
191
|
+
elif s == base or not m.get(base):
|
|
192
|
+
cells.append(fmt_us(v))
|
|
193
|
+
else:
|
|
194
|
+
cells.append(f"{fmt_us(v)} {pct_md((v - m[base]) / m[base] * 100)}")
|
|
195
|
+
out.append(cells)
|
|
196
|
+
return _table(header, out)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _quant_table(doc: dict) -> str:
|
|
200
|
+
by: dict[str, dict[int, float]] = {}
|
|
201
|
+
bits = {}
|
|
202
|
+
for r in doc.get("rows", []):
|
|
203
|
+
if r.get("rel_l2") is not None:
|
|
204
|
+
by.setdefault(r["format"], {})[r["L"]] = r["rel_l2"]
|
|
205
|
+
bits[r["format"]] = r.get("bits")
|
|
206
|
+
Ls = sorted({L for v in by.values() for L in v})
|
|
207
|
+
order = sorted(by, key=lambda f: (-(bits.get(f) or 0), f))
|
|
208
|
+
header = ["format", "bits"] + [L_label(L) for L in Ls] + ["mean"]
|
|
209
|
+
rows = []
|
|
210
|
+
for f in order:
|
|
211
|
+
vals = by[f]
|
|
212
|
+
rows.append(
|
|
213
|
+
[f"`{f}`", f"{bits.get(f):g}" if bits.get(f) else "—"]
|
|
214
|
+
+ [f"{vals[L]:.4f}" if L in vals else "—" for L in Ls]
|
|
215
|
+
+ [f"**{sum(vals.values()) / len(vals):.4f}**"]
|
|
216
|
+
)
|
|
217
|
+
return _table(header, rows, ["---", "--:"] + ["--:"] * (len(Ls) + 1))
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _pdl_table(doc: dict) -> str:
|
|
221
|
+
rows = [r for r in doc.get("rows", []) if r.get("off_us")]
|
|
222
|
+
Ls = sorted({r["L"] for r in rows})
|
|
223
|
+
keys = sorted({(r["q"], r["batches"]) for r in rows})
|
|
224
|
+
header = ["Q", "batch"] + [L_label(L) for L in Ls]
|
|
225
|
+
out = []
|
|
226
|
+
for q, b in keys:
|
|
227
|
+
m = {r["L"]: r for r in rows if r["q"] == q and r["batches"] == b}
|
|
228
|
+
out.append([q, str(b)] + [pct_md(m[L]["delta_pct"]) if L in m else "—" for L in Ls])
|
|
229
|
+
return _table(header, out, ["---", "--:"] + ["--:"] * len(Ls))
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _fp6prof_table(doc: dict) -> str:
|
|
233
|
+
profs = [p for p in doc.get("profiles", []) if p.get("ok")]
|
|
234
|
+
by_L: dict[int, list[dict]] = {}
|
|
235
|
+
for p in profs:
|
|
236
|
+
by_L.setdefault(p["L"], []).append(p)
|
|
237
|
+
header = [
|
|
238
|
+
"L",
|
|
239
|
+
"variant",
|
|
240
|
+
"bits",
|
|
241
|
+
"kernel time",
|
|
242
|
+
"time ratio",
|
|
243
|
+
"L2 bytes ratio",
|
|
244
|
+
"ideal",
|
|
245
|
+
"bytes / L2 request",
|
|
246
|
+
"top stall (cycles/instr)",
|
|
247
|
+
]
|
|
248
|
+
rows = []
|
|
249
|
+
for L, ps in sorted(by_L.items()):
|
|
250
|
+
b = ps[0]
|
|
251
|
+
bt = b["metrics"].get("gpu__time_duration.sum")
|
|
252
|
+
bs = b["metrics"].get("lts__t_sectors_op_read.sum") or b["metrics"].get("lts__t_sectors.sum")
|
|
253
|
+
for p in ps:
|
|
254
|
+
m = p["metrics"]
|
|
255
|
+
t = m.get("gpu__time_duration.sum")
|
|
256
|
+
sec = m.get("lts__t_sectors_op_read.sum") or m.get("lts__t_sectors.sum")
|
|
257
|
+
req = m.get("lts__t_requests_srcunit_tex_op_read.sum")
|
|
258
|
+
st = max(p.get("stalls", {}).items(), key=lambda kv: kv[1], default=None)
|
|
259
|
+
rows.append(
|
|
260
|
+
[
|
|
261
|
+
f"**{L_label(L)}**",
|
|
262
|
+
f"`{p['label']}`",
|
|
263
|
+
f"{p['kv_bits']:g}",
|
|
264
|
+
fmt_us(t / 1e3 if t else None),
|
|
265
|
+
f"{t / bt:.2f}" if t and bt else "—",
|
|
266
|
+
f"{sec / bs:.2f}" if sec and bs else "—",
|
|
267
|
+
f"{p['kv_bits'] / b['kv_bits']:.2f}",
|
|
268
|
+
f"{sec * 32 / req:.0f} B" if sec and req else "—",
|
|
269
|
+
f"{st[0]} {st[1]:.1f}" if st else "—",
|
|
270
|
+
]
|
|
271
|
+
)
|
|
272
|
+
return _table(header, rows, ["---", "---"] + ["--:"] * 6 + ["---"])
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _correctness_table(doc: dict) -> str:
|
|
276
|
+
cs = doc.get("cases", [])
|
|
277
|
+
groups: dict[str, list[dict]] = {}
|
|
278
|
+
for c in cs:
|
|
279
|
+
lab = c["label"]
|
|
280
|
+
g = (
|
|
281
|
+
"default config"
|
|
282
|
+
if lab.startswith("default:")
|
|
283
|
+
else "KV formats"
|
|
284
|
+
if re.search(r"e2m3|e3m2|e2m1|nvfp4|from float", lab)
|
|
285
|
+
else "KV layouts / strides"
|
|
286
|
+
if re.search(r"interl|flashinfer|HND|strided|descale", lab, re.I)
|
|
287
|
+
else "launch options"
|
|
288
|
+
if re.search(r"PDL|prefetch|evict", lab)
|
|
289
|
+
else "scheduling / paging"
|
|
290
|
+
if re.search(r"page|CTAs|chunks|padded|splits|contiguous", lab)
|
|
291
|
+
else "shapes / batches"
|
|
292
|
+
)
|
|
293
|
+
groups.setdefault(g, []).append(c)
|
|
294
|
+
rows = []
|
|
295
|
+
for g, items in groups.items():
|
|
296
|
+
ok = sum(1 for c in items if c.get("passed"))
|
|
297
|
+
mark = "✅" if ok == len(items) else "❌"
|
|
298
|
+
qs = sorted({c.get("q", "") for c in items})
|
|
299
|
+
rows.append([g, f"{mark} {ok}/{len(items)}", ", ".join(q for q in qs if q)])
|
|
300
|
+
return _table(["case group", "passed", "Q dtypes"], rows, ["---", "--:", "---"])
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def _vs_vllm_table(off: dict, mj: dict) -> str | None:
|
|
304
|
+
"""Official vLLM image vs the Mjolnir image, every shared cell: each
|
|
305
|
+
metric as 'official → mjolnir (gain)'; gain is positive when Mjolnir is
|
|
306
|
+
better (faster decode/prefill, lower TTFT/ITL)."""
|
|
307
|
+
from mjolnir_cli import e2e
|
|
308
|
+
|
|
309
|
+
rows = []
|
|
310
|
+
for r in e2e.compare(off, mj, every=True): # a cell one side lacks shows as "not measured", not dropped
|
|
311
|
+
line = [f"{e2e.ctx_label(r['ctx'])}", str(r["conc"])]
|
|
312
|
+
for m in e2e.METRICS:
|
|
313
|
+
a, b, g = r[m.key]
|
|
314
|
+
if a is None and b is None:
|
|
315
|
+
line.append("—")
|
|
316
|
+
continue
|
|
317
|
+
if a is None or b is None:
|
|
318
|
+
av = m.fmt.format(a) if a is not None else "*n/a*"
|
|
319
|
+
bv = f"**{m.fmt.format(b)}**" if b is not None else "*n/a*"
|
|
320
|
+
line.append(f"{av} → {bv}")
|
|
321
|
+
continue
|
|
322
|
+
line.append(f"{m.fmt.format(a)} → **{m.fmt.format(b)}** {ratio_pct(g, m.higher_better)}")
|
|
323
|
+
rows.append(line)
|
|
324
|
+
if not rows:
|
|
325
|
+
return None
|
|
326
|
+
head = ["context", "requests"] + [
|
|
327
|
+
f"{m.label} ({m.unit}, {'↑' if m.higher_better else '↓'}) "
|
|
328
|
+
+ color("throughput change" if m.higher_better else "latency change")
|
|
329
|
+
for m in e2e.METRICS
|
|
330
|
+
]
|
|
331
|
+
return _table(head, rows, ["--:", "--:"] + ["--:"] * len(e2e.METRICS))
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def ratio_pct(g: float | None, higher_better: bool = True) -> str:
|
|
335
|
+
"""The relative change of the measured value, colored by whether it is
|
|
336
|
+
better: ``g`` is the signed gain (positive = Mjolnir better), shown as the
|
|
337
|
+
change of the value itself — +x% throughput, −x% latency (a latency
|
|
338
|
+
reduction, never the reciprocal speedup). Green = better beyond the
|
|
339
|
+
±E2E_NOISE_PCT band, grey = within it, red = worse beyond it."""
|
|
340
|
+
if g is None:
|
|
341
|
+
return "—"
|
|
342
|
+
from mjolnir_cli import e2e
|
|
343
|
+
|
|
344
|
+
noise = e2e.E2E_NOISE_PCT
|
|
345
|
+
color = _GREEN if g >= noise else (_RED if g <= -noise else _GREY)
|
|
346
|
+
shown = g if higher_better else -g
|
|
347
|
+
sign = "+" if shown > 0 else (_MINUS if shown < 0 else "")
|
|
348
|
+
g = shown
|
|
349
|
+
# We need to double-escape the `\\\%` as Github refuses to render the `%` symbol otherwise
|
|
350
|
+
return f"$\\color{{{color}}}{{\\text{{{sign}{abs(g):.1f}\\\\%}}}}$"
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def _findings(stage: str, doc: dict) -> list[str]:
|
|
354
|
+
"""Data-driven one-liners for the docs (no hand-written numbers)."""
|
|
355
|
+
out: list[str] = []
|
|
356
|
+
if stage == "spec":
|
|
357
|
+
from mjolnir_cli.verify.spec import best_k
|
|
358
|
+
|
|
359
|
+
rows = [r for r in doc.get("rows", []) if r.get("decode_tps")]
|
|
360
|
+
for (cfg, conc), b in sorted(best_k(rows).items()):
|
|
361
|
+
ref = next((r for r in rows if r["config"] == cfg and r["concurrency"] == conc and r["k"] == 3), None)
|
|
362
|
+
vs = (
|
|
363
|
+
f", {(b['decode_tps'] / ref['decode_tps'] - 1) * 100:+.1f}% vs 3 drafts" if ref and b is not ref else ""
|
|
364
|
+
)
|
|
365
|
+
out.append(
|
|
366
|
+
f"{spec_label(cfg)}, {conc} request{'s' if conc > 1 else ''}: best with **{b['k']} draft"
|
|
367
|
+
f"{'s' if b['k'] > 1 else ''}** — {b['decode_tps']:.1f} t/s per request, "
|
|
368
|
+
f"{b['acceptance_length']:.2f} tokens per step{vs}."
|
|
369
|
+
)
|
|
370
|
+
by_cfg: dict = {}
|
|
371
|
+
for r in rows:
|
|
372
|
+
if r["k"] == 3 and r["concurrency"] == 1 and r.get("acceptance_length"):
|
|
373
|
+
by_cfg[r["config"]] = r["acceptance_length"]
|
|
374
|
+
if "NVFP4" in by_cfg:
|
|
375
|
+
for cfg, a in by_cfg.items():
|
|
376
|
+
if cfg != "NVFP4":
|
|
377
|
+
out.append(
|
|
378
|
+
f"Acceptance, 3 drafts, 1 request: {spec_label(cfg)} {a:.2f} vs official "
|
|
379
|
+
f"{by_cfg['NVFP4']:.2f} tokens/step ({(a / by_cfg['NVFP4'] - 1) * 100:+.1f}%) — "
|
|
380
|
+
"same prompts, temperature 0."
|
|
381
|
+
)
|
|
382
|
+
return out
|
|
383
|
+
if stage == "kv-cache-size":
|
|
384
|
+
rows = {r["id"]: r for r in doc.get("rows", []) if r.get("kv_tokens")}
|
|
385
|
+
e2m3 = next((r for r in rows.values() if str(r.get("kv_dtype", "")).startswith("fp6")), None)
|
|
386
|
+
for other in rows.values():
|
|
387
|
+
if e2m3 and other is not e2m3:
|
|
388
|
+
gain = e2m3["kv_tokens"] / other["kv_tokens"] - 1
|
|
389
|
+
out.append(
|
|
390
|
+
f"**{kvcache_label(e2m3)}** holds **{e2m3['kv_tokens']:,} tokens**, "
|
|
391
|
+
f"{gain * 100:+.0f}% vs {kvcache_label(other)} ({other['kv_tokens']:,})."
|
|
392
|
+
)
|
|
393
|
+
for r in rows.values():
|
|
394
|
+
if r.get("max_concurrency"):
|
|
395
|
+
out.append(
|
|
396
|
+
f"{kvcache_label(r)}: {r['max_concurrency']:.1f} requests at the full "
|
|
397
|
+
f"{L_label(r.get('max_model_len') or 0)} context (vLLM's own estimate)."
|
|
398
|
+
)
|
|
399
|
+
return out
|
|
400
|
+
by = results.series_by_id(doc)
|
|
401
|
+
noise = NOISE_PCT / 100
|
|
402
|
+
if stage in ("sweep", "hndsweep"):
|
|
403
|
+
base = by.get("fi")
|
|
404
|
+
for sid in ("mjolnir", "mjolnir-fp8", "fi-hnd", "fa2"):
|
|
405
|
+
if base and sid in by:
|
|
406
|
+
sp = results.speedup(base, by[sid])
|
|
407
|
+
if not sp:
|
|
408
|
+
continue
|
|
409
|
+
g = results.geomean(sp.values())
|
|
410
|
+
lo, hi = min(sp, key=sp.get), max(sp, key=sp.get)
|
|
411
|
+
beats = sum(1 for v in sp.values() if v >= 1 + noise)
|
|
412
|
+
out.append(
|
|
413
|
+
f"**{by[sid]['label']}** vs {base['label']}: geomean **{g:.2f}×**, "
|
|
414
|
+
f"from {sp[lo]:.2f}× (L={L_label(lo)}) to {sp[hi]:.2f}× (L={L_label(hi)}); "
|
|
415
|
+
f"faster beyond the ±{NOISE_PCT:.0f}% noise band at {beats} of {len(sp)} context lengths."
|
|
416
|
+
)
|
|
417
|
+
if stage in ("ring", "prefetch", "ab", "kvfmt") and "fi" in by:
|
|
418
|
+
variants = [s for s in doc.get("series", []) if s.get("kind") == "t5"]
|
|
419
|
+
if variants:
|
|
420
|
+
Ls = sorted(results.point_map(by["fi"]))
|
|
421
|
+
winners = {}
|
|
422
|
+
for L in Ls:
|
|
423
|
+
best = min(variants, key=lambda s: results.point_map(s).get(L, float("inf")))
|
|
424
|
+
winners[L] = best["label"]
|
|
425
|
+
counts: dict[str, int] = {}
|
|
426
|
+
for w in winners.values():
|
|
427
|
+
counts[w] = counts.get(w, 0) + 1
|
|
428
|
+
top = max(counts, key=counts.get)
|
|
429
|
+
out.append(f"Fastest variant at the most context lengths: **`{top}`** ({counts[top]} of {len(Ls)}).")
|
|
430
|
+
geo = {s["label"]: results.geomean(results.speedup(by["fi"], s).values()) for s in variants}
|
|
431
|
+
geo = {k: v for k, v in geo.items() if v}
|
|
432
|
+
if geo:
|
|
433
|
+
g_top = max(geo, key=geo.get)
|
|
434
|
+
g_low = min(geo, key=geo.get)
|
|
435
|
+
out.append(
|
|
436
|
+
f"Best geomean vs FlashInfer: **`{g_top}`** ({geo[g_top]:.2f}×); "
|
|
437
|
+
f"worst: `{g_low}` ({geo[g_low]:.2f}×)."
|
|
438
|
+
)
|
|
439
|
+
spread = geo[g_top] / geo[g_low] - 1
|
|
440
|
+
if spread < noise:
|
|
441
|
+
out.append(f"All variants sit within {spread * 100:.0f}% of each other — inside the noise band.")
|
|
442
|
+
if stage == "quant":
|
|
443
|
+
by_f: dict[str, list[float]] = {}
|
|
444
|
+
for r in doc.get("rows", []):
|
|
445
|
+
if r.get("rel_l2") is not None:
|
|
446
|
+
by_f.setdefault(r["format"], []).append(r["rel_l2"])
|
|
447
|
+
if "fp8" in by_f:
|
|
448
|
+
fp8 = sum(by_f["fp8"]) / len(by_f["fp8"])
|
|
449
|
+
for f, v in sorted(by_f.items(), key=lambda kv: sum(kv[1]) / len(kv[1])):
|
|
450
|
+
m = sum(v) / len(v)
|
|
451
|
+
out.append(f"`{f}`: mean relative L2 error **{m:.3f}** ({m / fp8:.1f}× fp8's {fp8:.3f}).")
|
|
452
|
+
if stage == "pdl":
|
|
453
|
+
ds = [r["delta_pct"] for r in doc.get("rows", []) if r.get("delta_pct") is not None]
|
|
454
|
+
if ds:
|
|
455
|
+
inside = sum(1 for d in ds if abs(d) < NOISE_PCT)
|
|
456
|
+
out.append(
|
|
457
|
+
f"{inside} of {len(ds)} measurements move by less than the ±{NOISE_PCT:.0f}% noise band "
|
|
458
|
+
f"(range {min(ds):+.1f}% … {max(ds):+.1f}%)."
|
|
459
|
+
)
|
|
460
|
+
if stage == "fp6prof":
|
|
461
|
+
profs = [p for p in doc.get("profiles", []) if p.get("ok")]
|
|
462
|
+
by_lab: dict[str, list[tuple[float, float, float]]] = {}
|
|
463
|
+
by_L: dict[int, dict[str, dict]] = {}
|
|
464
|
+
for p in profs:
|
|
465
|
+
by_L.setdefault(p["L"], {})[p["label"]] = p
|
|
466
|
+
for L, ps in by_L.items():
|
|
467
|
+
labs = list(ps)
|
|
468
|
+
b = ps[labs[0]]
|
|
469
|
+
for lab in labs[1:]:
|
|
470
|
+
p = ps[lab]
|
|
471
|
+
t = p["metrics"]["gpu__time_duration.sum"] / b["metrics"]["gpu__time_duration.sum"]
|
|
472
|
+
ideal = p["kv_bits"] / b["kv_bits"]
|
|
473
|
+
by_lab.setdefault(lab, []).append((t, ideal, L))
|
|
474
|
+
for lab, xs in by_lab.items():
|
|
475
|
+
t = sum(x[0] for x in xs) / len(xs)
|
|
476
|
+
ideal = xs[0][1]
|
|
477
|
+
realized = (1 - t) / (1 - ideal) if ideal < 1 else 0
|
|
478
|
+
out.append(
|
|
479
|
+
f"`{lab}`: kernel time **{t:.2f}×** the baseline for {ideal:.2f}× the bytes — "
|
|
480
|
+
f"{realized * 100:.0f}% of the byte saving becomes speed (mean over L)."
|
|
481
|
+
)
|
|
482
|
+
if stage == "correctness":
|
|
483
|
+
cs = doc.get("cases", [])
|
|
484
|
+
ok = sum(1 for c in cs if c.get("passed"))
|
|
485
|
+
out.append(
|
|
486
|
+
f"**{ok} / {len(cs)}** checks pass (kernel output and LSE vs the bit-faithful emulator, "
|
|
487
|
+
"poisoned unused pages must stay NaN-free)."
|
|
488
|
+
)
|
|
489
|
+
return out
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
KVCACHE_CTXS = (4096, 8192, 16384, 32768, 65536, 131072)
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
def kvcache_label(r: dict) -> str:
|
|
496
|
+
"""'official · FlashInfer fp8', 'Mjolnir · fp6 e2m3', ..."""
|
|
497
|
+
kv = str(r.get("kv_dtype") or "auto").replace("fp6_e2m3", "fp6 e2m3")
|
|
498
|
+
if r.get("side") != "official":
|
|
499
|
+
extra = "".join(
|
|
500
|
+
f" + {name}"
|
|
501
|
+
for tag, name in (("LMH8", "fp8 lm_head"), ("VT64K", "64K draft vocab"), ("VT32K", "32K draft vocab"))
|
|
502
|
+
if tag in str(r.get("config", "")).split("_")
|
|
503
|
+
)
|
|
504
|
+
return f"Mjolnir · {kv} KV{extra}"
|
|
505
|
+
be = {"flashinfer": "FlashInfer", "flash_attn": "FlashAttention"}.get(str(r.get("backend") or "").lower())
|
|
506
|
+
return f"official vLLM · {be or r.get('backend') or '?'} · {kv} KV"
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
def _kvcache_table(doc: dict) -> str:
|
|
510
|
+
rows = [r for r in doc.get("rows", []) if r.get("kv_tokens")]
|
|
511
|
+
pp, tg = doc.get("params", {}).get("e2e_pp", 2048), doc.get("params", {}).get("e2e_tg", 128)
|
|
512
|
+
base = rows[0]["kv_tokens"] if rows else None
|
|
513
|
+
out = []
|
|
514
|
+
for r in rows:
|
|
515
|
+
fit = [str(r["kv_tokens"] // (c + pp + tg)) for c in KVCACHE_CTXS]
|
|
516
|
+
out.append(
|
|
517
|
+
[
|
|
518
|
+
f"`{r['config']}`",
|
|
519
|
+
kvcache_label(r),
|
|
520
|
+
f"{r.get('kv_gib') or 0:.1f} GiB",
|
|
521
|
+
f"{r['kv_tokens']:,}",
|
|
522
|
+
f"{r['kv_tokens'] / base:.2f}×" if base else "—",
|
|
523
|
+
f"{r.get('max_concurrency') or 0:.1f}× @ {L_label(r.get('max_model_len') or 0)}",
|
|
524
|
+
*fit,
|
|
525
|
+
]
|
|
526
|
+
)
|
|
527
|
+
head = ["config", "serving", "KV memory", "KV tokens", "vs first", "max concurrency"]
|
|
528
|
+
head += [f"fits @ {L_label(c)}" for c in KVCACHE_CTXS]
|
|
529
|
+
mem = [k for k in ("load_gib", "weights_gib", "activation_gib", "non_torch_gib") if any(r.get(k) for r in rows)]
|
|
530
|
+
if mem: # where the rest of the budget went, when the logs carried it
|
|
531
|
+
names = {"load_gib": "model load", "weights_gib": "weights", "activation_gib": "activation peak",
|
|
532
|
+
"non_torch_gib": "non-torch"} # fmt: skip
|
|
533
|
+
head += [f"{names[k]} {color('GiB')}" for k in mem]
|
|
534
|
+
for r, line in zip(rows, out):
|
|
535
|
+
line += [f"{r[k]:.1f}" if r.get(k) else "—" for k in mem]
|
|
536
|
+
return _table(head, out, ["---", "---"] + ["--:"] * (len(head) - 2))
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
def spec_label(cfg: str, side: str | None = None) -> str:
|
|
540
|
+
return {
|
|
541
|
+
"NVFP4": "official vLLM · FlashInfer fp8",
|
|
542
|
+
"NVFP4_MJOLNIR": "Mjolnir · fp8 KV",
|
|
543
|
+
"NVFP4_MJOLNIR_E2M3": "Mjolnir · fp6 KV",
|
|
544
|
+
"NVFP4_MJOLNIR_E2M3_LMH8_VT64K": "Mjolnir · fp6 KV + fp8 lm_head + 64K draft vocab",
|
|
545
|
+
}.get(cfg, cfg)
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
def _spec_table(doc: dict) -> str:
|
|
549
|
+
from mjolnir_cli.verify.spec import best_k
|
|
550
|
+
|
|
551
|
+
rows = [r for r in doc.get("rows", []) if r.get("decode_tps")]
|
|
552
|
+
best = {(cfg, c): r["k"] for (cfg, c), r in best_k(rows).items()}
|
|
553
|
+
kmax = max((len(r.get("per_pos") or []) for r in rows), default=0)
|
|
554
|
+
out = []
|
|
555
|
+
for r in sorted(rows, key=lambda r: (r["config"], r["concurrency"], r["k"])):
|
|
556
|
+
star = " **★**" if best.get((r["config"], r["concurrency"])) == r["k"] else ""
|
|
557
|
+
pos = list(r.get("per_pos") or []) + [None] * (kmax - len(r.get("per_pos") or []))
|
|
558
|
+
out.append(
|
|
559
|
+
[
|
|
560
|
+
spec_label(r["config"]),
|
|
561
|
+
str(r["concurrency"]),
|
|
562
|
+
f"{r['k']}{star}",
|
|
563
|
+
f"{r.get('acceptance_length') or 0:.2f}",
|
|
564
|
+
*(f"{p:.2f}" if p is not None else "" for p in pos),
|
|
565
|
+
f"{r['decode_tps']:.1f}",
|
|
566
|
+
f"{r.get('agg_tps') or 0:.1f}",
|
|
567
|
+
f"{(r.get('ttft_ms') or 0) / 1000:.2f} s",
|
|
568
|
+
]
|
|
569
|
+
)
|
|
570
|
+
head = ["config", "c", "drafts", "tokens/step", *(f"accept @{i + 1}" for i in range(kmax))]
|
|
571
|
+
head += ["decode t/s/req", "aggregate t/s", "TTFT"]
|
|
572
|
+
return _table(head, out, ["---"] + ["--:"] * (len(head) - 1))
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
TABLES = {
|
|
576
|
+
"sweep": lambda d: _series_time_table(d, ("fi", "fa2")),
|
|
577
|
+
"hndsweep": lambda d: _series_time_table(d, ("fi", "fi-hnd")),
|
|
578
|
+
"kvfmt": lambda d: _variant_table(d, "fi"),
|
|
579
|
+
"ring": lambda d: _variant_table(d, "fi"),
|
|
580
|
+
"prefetch": lambda d: _variant_table(d, "fi"),
|
|
581
|
+
"ab": lambda d: _variant_table(d, "fi"),
|
|
582
|
+
"quant": _quant_table,
|
|
583
|
+
"pdl": _pdl_table,
|
|
584
|
+
"fp6prof": _fp6prof_table,
|
|
585
|
+
"correctness": _correctness_table,
|
|
586
|
+
"kv-cache-size": _kvcache_table,
|
|
587
|
+
"spec": _spec_table,
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
# ── context ─────────────────────────────────────────────────────────────────
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
@dataclass
|
|
595
|
+
class ReadmeContext:
|
|
596
|
+
vllm_version: str = ""
|
|
597
|
+
model: str = DEFAULT_MODEL
|
|
598
|
+
image: str = ""
|
|
599
|
+
kernel_version: str = ""
|
|
600
|
+
cli_version: str = ""
|
|
601
|
+
ls: str = " ".join(L_label(L) for L in ALL_LS)
|
|
602
|
+
noise_pct: str = f"{NOISE_PCT:.0f}"
|
|
603
|
+
e2e_noise_pct: str = field(default_factory=lambda: f"{__import__('mjolnir_cli.e2e').e2e.E2E_NOISE_PCT:.0f}")
|
|
604
|
+
# headline (sweep)
|
|
605
|
+
has_sweep: bool = False
|
|
606
|
+
geo_vs_fi: str = ""
|
|
607
|
+
geo_vs_fi_fp8: str = ""
|
|
608
|
+
geo_vs_fa2: str = ""
|
|
609
|
+
min_vs_fi: str = ""
|
|
610
|
+
max_vs_fi: str = ""
|
|
611
|
+
min_vs_fi_L: str = ""
|
|
612
|
+
max_vs_fi_L: str = ""
|
|
613
|
+
long_vs_fi: str = "" # at the largest L
|
|
614
|
+
long_L: str = ""
|
|
615
|
+
beats_fi: str = "" # "8 of 8"
|
|
616
|
+
us_64k_mjolnir: str = ""
|
|
617
|
+
us_64k_fi: str = ""
|
|
618
|
+
us_64k_fa2: str = ""
|
|
619
|
+
correctness: str = "" # "96 / 96"
|
|
620
|
+
sweep_run: str = ""
|
|
621
|
+
tables: dict = field(default_factory=dict)
|
|
622
|
+
findings: dict = field(default_factory=dict)
|
|
623
|
+
runs: dict = field(default_factory=dict) # stage -> run_id
|
|
624
|
+
pending: list = field(default_factory=list)
|
|
625
|
+
# official vLLM image vs the Mjolnir image (mjolnir_cli.e2e)
|
|
626
|
+
vs_vllm: str | None = None # the comparison table
|
|
627
|
+
vs_vllm_hero: dict = field(default_factory=dict) # c=1 headline gains, formatted
|
|
628
|
+
vs_vllm_official: str = ""
|
|
629
|
+
vs_vllm_mjolnir: str = ""
|
|
630
|
+
vs_vllm_spec: str = "" # speculative decoding both sides ran with, e.g. "MTP, 3 speculative tokens"
|
|
631
|
+
vs_vllm_missing: str = ""
|
|
632
|
+
e2e_example: dict = field(default_factory=dict)
|
|
633
|
+
e2e_max_ctx: str = field(default_factory=lambda: L_label(__import__("mjolnir_cli.e2e").e2e.MAX_CTX))
|
|
634
|
+
# the README's Key Features grid: measured numbers, each None when its run is missing
|
|
635
|
+
feat: dict = field(default_factory=dict)
|
|
636
|
+
default_config: str = field(
|
|
637
|
+
default_factory=lambda: __import__("mjolnir_cli.config").config.DEFAULT_QUANT
|
|
638
|
+
) # contexts only one side measured (the command that completes the table)
|
|
639
|
+
# model quality per serving config (mjolnir bench accuracy)
|
|
640
|
+
accuracy_table: str = ""
|
|
641
|
+
accuracy_categories: str = ""
|
|
642
|
+
accuracy_findings: list = field(default_factory=list)
|
|
643
|
+
accuracy_runs: list = field(default_factory=list) # run ids the table draws on
|
|
644
|
+
accuracy_args: list = field(default_factory=list) # (suite label, tool-eval-bench args)
|
|
645
|
+
accuracy_baseline: str = ""
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
def _vllm_base_version(layout: RepoLayout) -> str:
|
|
649
|
+
if layout.repo_root is None:
|
|
650
|
+
return ""
|
|
651
|
+
dockerfile = layout.repo_root / "docker" / "vllm-thor" / "Dockerfile"
|
|
652
|
+
if dockerfile.exists():
|
|
653
|
+
m = re.search(r"vllm/vllm-openai:v(\d+\.\d+\.\d+)", dockerfile.read_text())
|
|
654
|
+
if m:
|
|
655
|
+
return m.group(1)
|
|
656
|
+
return ""
|
|
657
|
+
|
|
658
|
+
|
|
659
|
+
def _version_from(path: Path) -> str:
|
|
660
|
+
try:
|
|
661
|
+
m = re.search(r'__version__\s*=\s*"([^"]+)"', path.read_text())
|
|
662
|
+
return m.group(1) if m else ""
|
|
663
|
+
except OSError:
|
|
664
|
+
return ""
|
|
665
|
+
|
|
666
|
+
|
|
667
|
+
def collect_context(layout: RepoLayout, data: dict | None = None) -> ReadmeContext:
|
|
668
|
+
from mjolnir_cli.charts import load_verify
|
|
669
|
+
|
|
670
|
+
data = data if data is not None else load_verify(layout)
|
|
671
|
+
ctx = ReadmeContext(vllm_version=_vllm_base_version(layout))
|
|
672
|
+
ctx.image = f"{IMAGE_REPO}:v{ctx.vllm_version}" if ctx.vllm_version else IMAGE_REPO
|
|
673
|
+
if layout.repo_root is not None:
|
|
674
|
+
ctx.kernel_version = _version_from(layout.repo_root / "src" / "mjolnir" / "mjolnir" / "__init__.py")
|
|
675
|
+
ctx.cli_version = _version_from(layout.repo_root / "src" / "mjolnir-cli" / "mjolnir_cli" / "__init__.py")
|
|
676
|
+
for stage, doc in data.items():
|
|
677
|
+
ctx.runs[stage] = doc["run_id"]
|
|
678
|
+
if stage in TABLES:
|
|
679
|
+
try:
|
|
680
|
+
ctx.tables[stage] = TABLES[stage](doc)
|
|
681
|
+
except (KeyError, ZeroDivisionError, TypeError, ValueError) as e:
|
|
682
|
+
print(f"[mjolnir] table {stage} skipped: {e!r}")
|
|
683
|
+
ctx.findings[stage] = _findings(stage, doc)
|
|
684
|
+
if stage in ("kvfmt", "ab"):
|
|
685
|
+
w = _wall_table(doc)
|
|
686
|
+
if w:
|
|
687
|
+
ctx.tables[f"{stage}_wall"] = w
|
|
688
|
+
ctx.pending = [s for s in STAGES if s not in data and s not in ("timing", "vllm", "profile")]
|
|
689
|
+
|
|
690
|
+
sweep = data.get("sweep")
|
|
691
|
+
if sweep:
|
|
692
|
+
by = results.series_by_id(sweep)
|
|
693
|
+
ours = by.get("mjolnir") or by.get("mjolnir-fp8")
|
|
694
|
+
if ours and "fi" in by:
|
|
695
|
+
ctx.has_sweep = True
|
|
696
|
+
ctx.sweep_run = sweep["run_id"]
|
|
697
|
+
sp = results.speedup(by["fi"], ours)
|
|
698
|
+
ctx.geo_vs_fi = f"{results.geomean(sp.values()):.2f}"
|
|
699
|
+
lo, hi = min(sp, key=sp.get), max(sp, key=sp.get)
|
|
700
|
+
ctx.min_vs_fi, ctx.min_vs_fi_L = f"{sp[lo]:.2f}", L_label(lo)
|
|
701
|
+
ctx.max_vs_fi, ctx.max_vs_fi_L = f"{sp[hi]:.2f}", L_label(hi)
|
|
702
|
+
last = max(sp)
|
|
703
|
+
ctx.long_vs_fi, ctx.long_L = f"{sp[last]:.2f}", L_label(last)
|
|
704
|
+
ctx.beats_fi = f"{sum(1 for v in sp.values() if v >= 1 + NOISE_PCT / 100)} of {len(sp)}"
|
|
705
|
+
if "mjolnir-fp8" in by:
|
|
706
|
+
ctx.geo_vs_fi_fp8 = f"{results.geomean(results.speedup(by['fi'], by['mjolnir-fp8']).values()):.2f}"
|
|
707
|
+
if "fa2" in by:
|
|
708
|
+
ctx.geo_vs_fa2 = f"{results.geomean(results.speedup(by['fa2'], ours).values()):.2f}"
|
|
709
|
+
pm = results.point_map(ours)
|
|
710
|
+
if 65536 in pm:
|
|
711
|
+
ctx.us_64k_mjolnir = fmt_us(pm[65536])
|
|
712
|
+
ctx.us_64k_fi = fmt_us(results.point_map(by["fi"]).get(65536))
|
|
713
|
+
if "fa2" in by:
|
|
714
|
+
ctx.us_64k_fa2 = fmt_us(results.point_map(by["fa2"]).get(65536))
|
|
715
|
+
corr = data.get("correctness")
|
|
716
|
+
if corr:
|
|
717
|
+
cs = corr.get("cases", [])
|
|
718
|
+
ctx.correctness = f"{sum(1 for c in cs if c.get('passed'))} / {len(cs)}"
|
|
719
|
+
from mjolnir_cli import e2e
|
|
720
|
+
|
|
721
|
+
off, mj = e2e.latest_pair(layout.history_file)
|
|
722
|
+
if off and mj:
|
|
723
|
+
ctx.vs_vllm = _vs_vllm_table(off, mj)
|
|
724
|
+
ctx.vs_vllm_official, ctx.vs_vllm_mjolnir = e2e.side_label(off), e2e.side_label(mj)
|
|
725
|
+
ctx.vs_vllm_spec = e2e.spec_label(off, mj)
|
|
726
|
+
gaps = e2e.missing_contexts(off, mj)
|
|
727
|
+
notes = [
|
|
728
|
+
f"the {'Mjolnir' if side == 'mjolnir' else 'official'} side has no run at "
|
|
729
|
+
f"{', '.join(e2e.ctx_label(c) for c in ctxs)} context yet (`mjolnir bench e2e --only {side} --depths "
|
|
730
|
+
f"{','.join(str(c) for c in ctxs)}`)"
|
|
731
|
+
for side, ctxs in gaps.items()
|
|
732
|
+
if ctxs
|
|
733
|
+
]
|
|
734
|
+
ctx.vs_vllm_missing = "; ".join(notes)
|
|
735
|
+
h = e2e.hero(off, mj)
|
|
736
|
+
if h and h["ttft_ms"][2] is not None:
|
|
737
|
+
a, b = h["ttft_ms"][0], h["ttft_ms"][1]
|
|
738
|
+
ctx.e2e_example = {
|
|
739
|
+
"ctx": e2e.ctx_label(h["ctx"]),
|
|
740
|
+
"a": a,
|
|
741
|
+
"b": b,
|
|
742
|
+
"change": e2e.change_pct(a, b),
|
|
743
|
+
"ratio": a / b,
|
|
744
|
+
}
|
|
745
|
+
if h:
|
|
746
|
+
ctx.vs_vllm_hero = {
|
|
747
|
+
"ctx": e2e.ctx_label(h["ctx"]),
|
|
748
|
+
"tg_tps": e2e.pct(h["tg_tps"][2]),
|
|
749
|
+
"pp_tps": e2e.pct(h["pp_tps"][2]),
|
|
750
|
+
"ttft_ms": f"{h['ttft_ms'][0] / 1000:.2f} s → {h['ttft_ms'][1] / 1000:.2f} s "
|
|
751
|
+
f"({e2e.pct(-h['ttft_ms'][2])})"
|
|
752
|
+
if h["ttft_ms"][2] is not None
|
|
753
|
+
else "—",
|
|
754
|
+
"itl_ms": f"{h['itl_ms'][0]:.1f} ms → {h['itl_ms'][1]:.1f} ms ({e2e.pct(-h['itl_ms'][2])})"
|
|
755
|
+
if h["itl_ms"][2] is not None
|
|
756
|
+
else "—",
|
|
757
|
+
}
|
|
758
|
+
_accuracy_context(ctx, layout)
|
|
759
|
+
_features(ctx, data, layout)
|
|
760
|
+
return ctx
|
|
761
|
+
|
|
762
|
+
|
|
763
|
+
def _features(ctx: ReadmeContext, data: dict, layout: RepoLayout) -> None:
|
|
764
|
+
"""The numbers the Key Features grid quotes — from the same raw results as
|
|
765
|
+
the tables (the grid falls back to plain text where a run is missing)."""
|
|
766
|
+
from mjolnir_cli import accuracy, e2e
|
|
767
|
+
|
|
768
|
+
f: dict = {"geo": ctx.geo_vs_fi or None}
|
|
769
|
+
off, mj = e2e.latest_pair(layout.history_file)
|
|
770
|
+
h = e2e.hero(off, mj) if off and mj else None
|
|
771
|
+
if h:
|
|
772
|
+
f.update(
|
|
773
|
+
ctx=e2e.ctx_label(h["ctx"]),
|
|
774
|
+
decode=f"+{h['tg_tps'][2]:.0f}%",
|
|
775
|
+
prefill=f"+{h['pp_tps'][2]:.0f}%",
|
|
776
|
+
ttft=f"−{h['ttft_ms'][2]:.0f}%" if h["ttft_ms"][2] is not None else None,
|
|
777
|
+
)
|
|
778
|
+
kv = data.get("kv-cache-size")
|
|
779
|
+
if kv:
|
|
780
|
+
rows = [r for r in kv.get("rows", []) if r.get("kv_tokens")]
|
|
781
|
+
base = next((r for r in rows if r.get("side") == "official"), None)
|
|
782
|
+
fp6 = max(
|
|
783
|
+
(r for r in rows if str(r.get("kv_dtype", "")).startswith("fp6")),
|
|
784
|
+
key=lambda r: r["kv_tokens"],
|
|
785
|
+
default=None,
|
|
786
|
+
)
|
|
787
|
+
if base and fp6:
|
|
788
|
+
f["kv"] = f"+{(fp6['kv_tokens'] / base['kv_tokens'] - 1) * 100:.0f}%"
|
|
789
|
+
rows = accuracy.latest_legs(accuracy.load_runs(layout))
|
|
790
|
+
b = accuracy.baseline(rows)
|
|
791
|
+
if b:
|
|
792
|
+
mine = [r for r in rows if r is not b and str(r.get("config", "")).startswith("NVFP4_MJOLNIR")]
|
|
793
|
+
deltas = [
|
|
794
|
+
abs(r["suites"][n]["score"] - b["suites"][n]["score"])
|
|
795
|
+
for r in mine
|
|
796
|
+
for n in ("tools", "hardmode")
|
|
797
|
+
if n in r["suites"] and n in b["suites"]
|
|
798
|
+
]
|
|
799
|
+
if deltas:
|
|
800
|
+
f["acc"] = f"{max(deltas):.1f}"
|
|
801
|
+
ctx.feat = f
|
|
802
|
+
|
|
803
|
+
|
|
804
|
+
def _accuracy_context(ctx: ReadmeContext, layout: RepoLayout) -> None:
|
|
805
|
+
from mjolnir_cli import accuracy
|
|
806
|
+
|
|
807
|
+
rows = accuracy.latest_legs(accuracy.load_runs(layout))
|
|
808
|
+
if not rows:
|
|
809
|
+
return
|
|
810
|
+
base = accuracy.baseline(rows)
|
|
811
|
+
names = accuracy.suites_in(rows)
|
|
812
|
+
ctx.accuracy_baseline = base["label"] if base else ""
|
|
813
|
+
ctx.accuracy_runs = sorted({v["run_id"] for r in rows for v in r["runs"].values()})
|
|
814
|
+
args = {}
|
|
815
|
+
for r in rows:
|
|
816
|
+
for n, v in r["runs"].items():
|
|
817
|
+
args.setdefault(n, " ".join(v.get("args") or []))
|
|
818
|
+
ctx.accuracy_args = [(accuracy.suite_label(n), args[n]) for n in names if n in args]
|
|
819
|
+
|
|
820
|
+
def cell(r: dict, n: str) -> str:
|
|
821
|
+
v = (r["suites"].get(n) or {}).get("score")
|
|
822
|
+
if v is None:
|
|
823
|
+
return "—"
|
|
824
|
+
b = ((base or {})["suites"].get(n) or {}).get("score") if base else None
|
|
825
|
+
out = f"{v:.1f}"
|
|
826
|
+
if (r.get("suites", {}).get(n) or {}).get("n", 1) > 1:
|
|
827
|
+
out += f" <sub>n={r['suites'][n]['n']}</sub>"
|
|
828
|
+
if r is not base and b is not None:
|
|
829
|
+
out += f" ({v - b:+.1f})"
|
|
830
|
+
return out
|
|
831
|
+
|
|
832
|
+
head = ["config", "KV", *[f"{accuracy.suite_label(n)} %" for n in names], "completion"]
|
|
833
|
+
body = []
|
|
834
|
+
for r in rows:
|
|
835
|
+
comp = [x.get("completion_rate") for x in r["suites"].values() if x.get("completion_rate") is not None]
|
|
836
|
+
label = f"**{r['label']}**" if r is base else r["label"]
|
|
837
|
+
kv = str(r.get("kv_cache_dtype") or "auto").replace("fp6_e2m3", "fp6 e2m3").replace("auto", "bf16")
|
|
838
|
+
body.append([label, kv, *[cell(r, n) for n in names], f"{min(comp):.0f}%" if comp else "—"])
|
|
839
|
+
ctx.accuracy_table = _table(head, body, ["---", "---"] + ["--:"] * (len(head) - 2))
|
|
840
|
+
|
|
841
|
+
cats = sorted({c for r in rows for c in r.get("categories", {})})
|
|
842
|
+
if cats:
|
|
843
|
+
labels = {c: next(r["categories"][c]["label"] for r in rows if c in r.get("categories", {})) for c in cats}
|
|
844
|
+
chead = ["category", *[r["label"] for r in rows]]
|
|
845
|
+
cbody = []
|
|
846
|
+
for c in cats:
|
|
847
|
+
cbody.append(
|
|
848
|
+
[f"{c} · {labels[c]}"]
|
|
849
|
+
+ [f"{r['categories'][c]['percent']:.0f}" if c in r.get("categories", {}) else "—" for r in rows]
|
|
850
|
+
)
|
|
851
|
+
ctx.accuracy_categories = _table(chead, cbody)
|
|
852
|
+
|
|
853
|
+
if base:
|
|
854
|
+
for r in rows:
|
|
855
|
+
if r is base:
|
|
856
|
+
continue
|
|
857
|
+
deltas = []
|
|
858
|
+
for n in names:
|
|
859
|
+
v, b = (r["suites"].get(n) or {}).get("score"), (base["suites"].get(n) or {}).get("score")
|
|
860
|
+
if v is not None and b is not None:
|
|
861
|
+
deltas.append(f"{accuracy.suite_label(n)} {v - b:+.1f}")
|
|
862
|
+
if deltas:
|
|
863
|
+
changed = []
|
|
864
|
+
if r.get("scenarios") and base.get("scenarios"):
|
|
865
|
+
common = r["scenarios"].keys() & base["scenarios"].keys()
|
|
866
|
+
worse = sum(1 for k in common if r["scenarios"][k] < base["scenarios"][k])
|
|
867
|
+
better = sum(1 for k in common if r["scenarios"][k] > base["scenarios"][k])
|
|
868
|
+
changed = [f"{worse} scenarios scored lower, {better} higher of {len(common)}"]
|
|
869
|
+
ctx.accuracy_findings.append(
|
|
870
|
+
f"**{r['label']}** vs {base['label']}: {', '.join(deltas)} points"
|
|
871
|
+
+ (f" ({changed[0]})" if changed else "")
|
|
872
|
+
+ "."
|
|
873
|
+
)
|
|
874
|
+
|
|
875
|
+
|
|
876
|
+
# ── "how is this calculated" panels (one under every table / figure with % or ×) ──
|
|
877
|
+
|
|
878
|
+
_HOW: dict[str, str] = {
|
|
879
|
+
"kernel": """\
|
|
880
|
+
- **Time per decode step** = Nsight Compute kernel time (`gpu__time_duration`) of the decode kernel **plus** its split-KV merge kernel, at base clocks with a cold L2, median over the iterations. The reference (FlashInfer, FlashAttention 2) is measured the same way, at the same L, in the same run.
|
|
881
|
+
- **Speedup ×** = reference time ÷ Mjolnir time. `1.24×` means Mjolnir needs 1 ÷ 1.24 = 81% of the reference's time per step (19% less time); below 1 the reference is faster.
|
|
882
|
+
- **Geomean over L** = exp(mean of ln(speedup at each L)): every context length weighs the same, and one large ratio cannot dominate the way it would in an arithmetic mean.
|
|
883
|
+
- **Colors / "beyond the noise band"**: green = speedup ≥ 1.03, grey = within ±3%, red ≤ 0.97. ±3% (`NOISE_PCT`) is how far a same-run ratio moves between repeated runs on a dedicated Thor.""",
|
|
884
|
+
"variant": """\
|
|
885
|
+
- Each cell = **reference time ÷ variant time** (Nsight Compute kernel time, decode + merge, base clocks, cold L2, same run, same L). `1.10×` = the variant needs 1 ÷ 1.10 = 91% of the reference's time; below 1 it is slower.
|
|
886
|
+
- **geomean** = exp(mean of ln(ratio)) over the context lengths; **bold** = the fastest variant at that L.
|
|
887
|
+
- Green ≥ 1.03, grey within ±3% (`NOISE_PCT`, the run-to-run spread of a same-run ratio), red ≤ 0.97.""",
|
|
888
|
+
"wall": """\
|
|
889
|
+
- Wall-clock median per launch over the stage's iterations (CUDA events), every variant in the same run.
|
|
890
|
+
- **%** = (variant − first column) ÷ first column × 100 — the change in **time**: negative = faster. `−6.0%` = 6% less time than the first column.
|
|
891
|
+
- Green = at least 3% less time, red = at least 3% more, grey = within ±3% (`NOISE_PCT`).""",
|
|
892
|
+
"pdl": """\
|
|
893
|
+
- **%** = (time with PDL − time without PDL) ÷ time without PDL × 100, harness median per L, both measured back to back in the same run. Negative = PDL saves time.
|
|
894
|
+
- Green ≤ −3%, red ≥ +3%, grey within ±3% (`NOISE_PCT`).""",
|
|
895
|
+
"fp6prof": """\
|
|
896
|
+
- **time ratio** = this row's Nsight Compute kernel time ÷ the first row's at the same L; **L2 bytes ratio** = L2 read sectors ÷ the first row's; **ideal** = this row's KV bits ÷ the first row's — the ratio a purely bandwidth-bound kernel would show.
|
|
897
|
+
- **bytes / L2 request** = 32 B × L2 read sectors ÷ L2 read requests.""",
|
|
898
|
+
"kvcache": """\
|
|
899
|
+
- **KV tokens** and KV memory come from vLLM's own startup log ("GPU KV cache size", "Available KV cache memory") for the same model and memory budget.
|
|
900
|
+
- **vs first** = this row's KV tokens ÷ the first row's (`1.33×` = 33% more tokens).
|
|
901
|
+
- **fits @ L** = ⌊KV tokens ÷ (L + 2,048 prompt + 128 generated)⌋ — whole requests of the e2e bench's shape that fit at once (`max-num-seqs` ignored).""",
|
|
902
|
+
"spec": """\
|
|
903
|
+
- **tokens/step** = 1 + accepted draft tokens ÷ verify steps, from vLLM's speculative-decoding counters read before and after the block; **accept @i** = the share of verify steps whose i-th draft was accepted.
|
|
904
|
+
- **decode t/s per request** = (generated tokens − 1) ÷ (time of the last token − time of the first token); **aggregate t/s** = all generated tokens ÷ the block's wall time.
|
|
905
|
+
- ★ = the draft count with the highest decode t/s for that config and concurrency.""",
|
|
906
|
+
"accuracy": """\
|
|
907
|
+
- **Score** = points ÷ maximum points × 100, each scenario 2 (pass), 1 (partial) or 0 (fail); GSM8K / MMLU = accuracy in %; IFEval = prompt-level accuracy in %.
|
|
908
|
+
- **(±)** = this row's score − the baseline row's score, in percentage **points**, not a relative change: 86.5 vs 88.0 is −1.5 points (a relative change would be −1.7%).
|
|
909
|
+
- **n** = runs averaged (`--trials`); **completion** = the share of scenarios the endpoint answered — compare rows only at equal completion.""",
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
|
|
913
|
+
def _signed(v: float) -> str:
|
|
914
|
+
return f"{'−' if v < 0 else '+'}{abs(v):.1f}%"
|
|
915
|
+
|
|
916
|
+
|
|
917
|
+
def _how_e2e(ctx: "ReadmeContext") -> str:
|
|
918
|
+
ex = ctx.e2e_example
|
|
919
|
+
lines = [
|
|
920
|
+
"- **Throughput** (decode, prefill — tokens/s, higher is better): change = (Mjolnir − official) ÷ official × 100. "
|
|
921
|
+
"`+57%` = 1.57× the tokens per second. On the infographic a change of +100% or more is written as the multiple "
|
|
922
|
+
"Mjolnir ÷ official (`+200%` = `3×`).",
|
|
923
|
+
"- **Latency** (TTFT, ITL — milliseconds, lower is better): change = (Mjolnir − official) ÷ official × 100, so a "
|
|
924
|
+
"negative value is a **latency reduction**: `−27%` = 27% fewer milliseconds. It is *not* the reciprocal speedup "
|
|
925
|
+
"official ÷ Mjolnir − 1, which reads larger for the same pair.",
|
|
926
|
+
]
|
|
927
|
+
if ex:
|
|
928
|
+
lines.append(
|
|
929
|
+
f"- **Worked example** ({ex['ctx']} context, 1 request): TTFT {ex['a']:,.0f} → {ex['b']:,.0f} ms: "
|
|
930
|
+
f"({ex['b']:,.0f} − {ex['a']:,.0f}) ÷ {ex['a']:,.0f} × 100 = **{_signed(ex['change'])}** (a latency reduction "
|
|
931
|
+
f"of {-ex['change']:.1f}%). The reciprocal speedup would be {ex['a']:,.0f} ÷ {ex['b']:,.0f} = {ex['ratio']:.2f}× "
|
|
932
|
+
f"({_signed((ex['ratio'] - 1) * 100)}) — the same measurement, a different convention."
|
|
933
|
+
)
|
|
934
|
+
lines += [
|
|
935
|
+
"- **Values**: the mean over the measured runs per cell (6 measured after 2 warm-ups, llama-benchy): decode = "
|
|
936
|
+
"generated tokens ÷ generation time per request (summed over requests at higher concurrency), prefill = prompt "
|
|
937
|
+
"tokens ÷ prompt-processing time, TTFT = time to the first content token, ITL = 1000 ÷ the per-request decode rate.",
|
|
938
|
+
f"- **Colors**: green = better by at least {ctx.e2e_noise_pct}%, grey = within ±{ctx.e2e_noise_pct}% "
|
|
939
|
+
"(`E2E_NOISE_PCT`), red = worse by at least that. With speculative decoding a single decode cell moves more "
|
|
940
|
+
"between runs (acceptance depends on the prompt), so repeat (`--repeat`) before reading a few-% difference.",
|
|
941
|
+
]
|
|
942
|
+
return "\n".join(lines)
|
|
943
|
+
|
|
944
|
+
|
|
945
|
+
def how(ctx: "ReadmeContext", kind: str) -> str:
|
|
946
|
+
"""A collapsed "How this is calculated" panel for the tables / figures of
|
|
947
|
+
``kind`` (GitHub renders the markdown inside ``<details>``)."""
|
|
948
|
+
body = _how_e2e(ctx) if kind == "e2e" else _HOW[kind]
|
|
949
|
+
return f"<details>\n<summary>How the % and × here are calculated</summary>\n\n{body}\n\n</details><br />"
|
|
950
|
+
|
|
951
|
+
|
|
952
|
+
# ── rendering ───────────────────────────────────────────────────────────────
|
|
953
|
+
|
|
954
|
+
|
|
955
|
+
def _manifest(layout: RepoLayout) -> dict:
|
|
956
|
+
p = layout.charts_dir / "manifest.json"
|
|
957
|
+
try:
|
|
958
|
+
return json.loads(p.read_text())
|
|
959
|
+
except (OSError, json.JSONDecodeError):
|
|
960
|
+
return {}
|
|
961
|
+
|
|
962
|
+
|
|
963
|
+
def _env(layout: RepoLayout, ctx: ReadmeContext, out_path: Path) -> jinja2.Environment:
|
|
964
|
+
import os
|
|
965
|
+
|
|
966
|
+
manifest = _manifest(layout)
|
|
967
|
+
root_rel = os.path.relpath(layout.repo_root or out_path.parent, out_path.parent).replace(os.sep, "/")
|
|
968
|
+
prefix = "" if root_rel == "." else root_rel + "/"
|
|
969
|
+
|
|
970
|
+
def chart(name: str, alt: str = "", width: str = "100%") -> str:
|
|
971
|
+
m = manifest.get(name)
|
|
972
|
+
if not m:
|
|
973
|
+
return ""
|
|
974
|
+
light, dark = prefix + m["light"], prefix + m.get("dark", m["light"])
|
|
975
|
+
return (
|
|
976
|
+
"<picture>\n"
|
|
977
|
+
f' <source media="(prefers-color-scheme: dark)" srcset="{dark}">\n'
|
|
978
|
+
f' <img src="{light}" alt="{alt or name}" width="{width}">\n'
|
|
979
|
+
"</picture>"
|
|
980
|
+
)
|
|
981
|
+
|
|
982
|
+
env = jinja2.Environment(undefined=jinja2.StrictUndefined, keep_trailing_newline=True, autoescape=False)
|
|
983
|
+
env.globals.update(
|
|
984
|
+
how=lambda kind: how(ctx, kind),
|
|
985
|
+
ctx=ctx,
|
|
986
|
+
color=color,
|
|
987
|
+
chart=chart,
|
|
988
|
+
has=lambda stage: stage in ctx.runs,
|
|
989
|
+
root=prefix,
|
|
990
|
+
stages=STAGES,
|
|
991
|
+
)
|
|
992
|
+
return env
|
|
993
|
+
|
|
994
|
+
|
|
995
|
+
def render_file(layout: RepoLayout, tpl: Path, out: Path, ctx: ReadmeContext) -> Path:
|
|
996
|
+
env = _env(layout, ctx, out)
|
|
997
|
+
out.write_text(env.from_string(tpl.read_text()).render())
|
|
998
|
+
return out
|
|
999
|
+
|
|
1000
|
+
|
|
1001
|
+
def render_readme(layout: RepoLayout, template: Path | None = None, out: Path | None = None) -> list[Path]:
|
|
1002
|
+
"""Render README.md + every docs ``*.md.j2`` (checkout only)."""
|
|
1003
|
+
root = layout.require_repo("rendering the README")
|
|
1004
|
+
tpl = Path(template) if template else root / "README.md.j2"
|
|
1005
|
+
dest = Path(out) if out else root / "README.md"
|
|
1006
|
+
if not tpl.exists():
|
|
1007
|
+
raise FileNotFoundError(f"README template not found: {tpl}")
|
|
1008
|
+
ctx = collect_context(layout)
|
|
1009
|
+
from mjolnir_cli import clidoc
|
|
1010
|
+
from mjolnir_cli.cli import app as cli_app
|
|
1011
|
+
|
|
1012
|
+
cli_md = root / "docs" / "cli.md"
|
|
1013
|
+
cli_md.parent.mkdir(parents=True, exist_ok=True)
|
|
1014
|
+
cli_md.write_text(clidoc.reference(cli_app))
|
|
1015
|
+
written = [render_file(layout, tpl, dest, ctx), cli_md]
|
|
1016
|
+
for t in sorted((root / "docs").rglob("*.md.j2")):
|
|
1017
|
+
written.append(render_file(layout, t, t.with_suffix(""), ctx))
|
|
1018
|
+
for p in written:
|
|
1019
|
+
print(f"[mjolnir] wrote {p}")
|
|
1020
|
+
return written
|