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