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,334 @@
|
|
|
1
|
+
"""Container plumbing for ``mjolnir verify``: harness runs, ncu legs, full
|
|
2
|
+
profiles and test runs — each in a named, timed-out, reaped container.
|
|
3
|
+
|
|
4
|
+
The kernel under test is ``mjolnir.t5_decode`` (``python3 -m
|
|
5
|
+
mjolnir.t5_decode``). In a checkout the repo's ``src/mjolnir`` is mounted
|
|
6
|
+
over the image's installed copy (``PYTHONPATH``), so every run measures the
|
|
7
|
+
current source; a PyPI install runs the kernel baked into the image.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
import random
|
|
14
|
+
import shlex
|
|
15
|
+
import subprocess
|
|
16
|
+
import sys
|
|
17
|
+
from dataclasses import dataclass, field
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
from mjolnir_cli.verify import ncu
|
|
21
|
+
from mjolnir_cli.verify.legs import FILTERS, Leg
|
|
22
|
+
|
|
23
|
+
DRIVERS_DIR = Path(__file__).resolve().parent.parent / "drivers"
|
|
24
|
+
NCU_HOME_DEFAULT = "/opt/nvidia/nsight-compute/2025.3.0"
|
|
25
|
+
_IN_SRC = "/opt/mjolnir-src"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def ensure_image(image: str) -> str | None:
|
|
29
|
+
"""Make sure ``image`` is available locally (pull it if not) BEFORE any
|
|
30
|
+
stage starts, so a missing image is one clear error instead of one
|
|
31
|
+
failed container per case. Returns None if ready, else a message."""
|
|
32
|
+
if subprocess.run(["docker", "image", "inspect", image], capture_output=True).returncode == 0:
|
|
33
|
+
return None
|
|
34
|
+
p = subprocess.run(["docker", "pull", image], capture_output=True, text=True)
|
|
35
|
+
if p.returncode == 0:
|
|
36
|
+
return None
|
|
37
|
+
why = (p.stderr or p.stdout).strip().splitlines()[-1:] or ["pull failed"]
|
|
38
|
+
return (
|
|
39
|
+
f"image {image} is not available locally and could not be pulled ({why[0]}).\n"
|
|
40
|
+
f" build it from this checkout: mjolnir image build --tag {image}\n"
|
|
41
|
+
f" or use another image: --image <tag> (or: mjolnir image use <tag>)"
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
_IN_DRIVERS = "/opt/mjolnir-drivers"
|
|
46
|
+
_IN_NCU = "/ncu"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def default_ref_image() -> str:
|
|
50
|
+
"""Where the FlashInfer / FA2 reference legs run: the OFFICIAL upstream
|
|
51
|
+
vLLM image, never ours — the Mjolnir image patches FlashInfer paths too,
|
|
52
|
+
so a reference measured there would not be stock vLLM."""
|
|
53
|
+
from mjolnir_cli.config import STOCK_IMAGE
|
|
54
|
+
|
|
55
|
+
return os.environ.get("MJOLNIR_REF_IMAGE", STOCK_IMAGE)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass
|
|
59
|
+
class Runner:
|
|
60
|
+
image: str
|
|
61
|
+
logdir: Path
|
|
62
|
+
ref_image: str = field(default_factory=default_ref_image) # FlashInfer / FA2 legs
|
|
63
|
+
kernel_src: Path | None = None # checkout's src/mjolnir (mounted over the image copy)
|
|
64
|
+
ncu_home: str = field(default_factory=lambda: os.environ.get("NCU_HOME", NCU_HOME_DEFAULT))
|
|
65
|
+
timeout: float = 180.0
|
|
66
|
+
dry_run: bool = False
|
|
67
|
+
hq: int = 24
|
|
68
|
+
hk: int = 4
|
|
69
|
+
d: int = 256
|
|
70
|
+
page: int = 128
|
|
71
|
+
batch: int = 1 # sequences per decode step (each of L tokens) — `verify sweep --batch`
|
|
72
|
+
q_len: int = 1 # query tokens per sequence (MTP=3 verification is 4) — `verify sweep --q-len`
|
|
73
|
+
|
|
74
|
+
_ready: set = field(default_factory=set, repr=False)
|
|
75
|
+
|
|
76
|
+
# ── low level ──────────────────────────────────────────────────────────
|
|
77
|
+
def image_for(self, leg: Leg) -> str:
|
|
78
|
+
"""Mjolnir legs run on the Mjolnir image; FlashInfer / FA2 references
|
|
79
|
+
on the official vLLM image."""
|
|
80
|
+
return self.image if leg.kind == "t5" else self.ref_image
|
|
81
|
+
|
|
82
|
+
def _ensure(self, image: str, log: Path) -> bool:
|
|
83
|
+
if self.dry_run or image in self._ready:
|
|
84
|
+
return True
|
|
85
|
+
err = ensure_image(image)
|
|
86
|
+
if err:
|
|
87
|
+
log.parent.mkdir(parents=True, exist_ok=True)
|
|
88
|
+
log.write_text(err + "\n")
|
|
89
|
+
print(f" ! {err}", file=sys.stderr)
|
|
90
|
+
return False
|
|
91
|
+
self._ready.add(image)
|
|
92
|
+
return True
|
|
93
|
+
|
|
94
|
+
def _name(self, prefix: str) -> str:
|
|
95
|
+
return f"{prefix}-{os.getpid()}-{random.randint(0, 1 << 30)}"
|
|
96
|
+
|
|
97
|
+
def _overlay(self) -> list[str]:
|
|
98
|
+
if self.kernel_src is None:
|
|
99
|
+
return []
|
|
100
|
+
return ["-v", f"{self.kernel_src}:{_IN_SRC}:ro", "-e", f"PYTHONPATH={_IN_SRC}"]
|
|
101
|
+
|
|
102
|
+
def _run(self, argv: list[str], log: Path, name: str, timeout: float | None = None) -> int:
|
|
103
|
+
log.parent.mkdir(parents=True, exist_ok=True)
|
|
104
|
+
if self.dry_run:
|
|
105
|
+
print(" $ " + " ".join(shlex.quote(a) for a in argv))
|
|
106
|
+
log.write_text("(dry run)\n")
|
|
107
|
+
return 0
|
|
108
|
+
with log.open("w") as fh:
|
|
109
|
+
try:
|
|
110
|
+
p = subprocess.run(argv, stdout=fh, stderr=subprocess.STDOUT, timeout=timeout or self.timeout)
|
|
111
|
+
return p.returncode
|
|
112
|
+
except KeyboardInterrupt:
|
|
113
|
+
# the docker client dies with the CLI; the container would keep the GPU
|
|
114
|
+
subprocess.run(["docker", "rm", "-f", name], capture_output=True)
|
|
115
|
+
raise
|
|
116
|
+
except subprocess.TimeoutExpired:
|
|
117
|
+
# stopping only the docker CLI leaves the container — and the GPU — stuck
|
|
118
|
+
subprocess.run(["docker", "kill", name], capture_output=True)
|
|
119
|
+
subprocess.run(["docker", "rm", "-f", name], capture_output=True)
|
|
120
|
+
fh.write(f"\nTIMEOUT after {timeout or self.timeout:.0f}s (container killed: hung kernel?)\n")
|
|
121
|
+
return 124
|
|
122
|
+
|
|
123
|
+
# ── the kernel harness ─────────────────────────────────────────────────
|
|
124
|
+
def harness_args(self, q: str, args: list[str] | str) -> list[str]:
|
|
125
|
+
if isinstance(args, str):
|
|
126
|
+
args = shlex.split(args)
|
|
127
|
+
qd = "Float8E4M3FN" if q == "fp8" else "BFloat16"
|
|
128
|
+
return ["-m", "mjolnir.t5_decode", "--q_dtype", qd, "--kv_dtype", "Float8E4M3FN", *args]
|
|
129
|
+
|
|
130
|
+
def harness(self, log: Path, q: str, args: list[str] | str, timeout: float | None = None) -> tuple[int, str]:
|
|
131
|
+
name = self._name("mjolnir-verify")
|
|
132
|
+
argv = ["docker", "run", "--rm", "--name", name, "--gpus", "all", "--entrypoint", "python3"]
|
|
133
|
+
argv += self._overlay() + [self.image] + self.harness_args(q, args)
|
|
134
|
+
rc = self._run(argv, log, name, timeout)
|
|
135
|
+
return rc, log.read_text(errors="replace") if log.exists() else ""
|
|
136
|
+
|
|
137
|
+
# ── ncu-profiled legs ──────────────────────────────────────────────────
|
|
138
|
+
def leg_command(self, leg: Leg, L: int, iters: int) -> tuple[str, list[str]]:
|
|
139
|
+
"""(python command line inside the container, extra docker -e args)."""
|
|
140
|
+
common_env = [
|
|
141
|
+
f"BENCH_L={L}",
|
|
142
|
+
f"BENCH_ITERS={iters}",
|
|
143
|
+
f"BENCH_HQ={self.hq}",
|
|
144
|
+
f"BENCH_HK={self.hk}",
|
|
145
|
+
f"BENCH_D={self.d}",
|
|
146
|
+
f"BENCH_PAGE={self.page}",
|
|
147
|
+
f"BENCH_B={self.batch}",
|
|
148
|
+
f"BENCH_QLEN={self.q_len}",
|
|
149
|
+
]
|
|
150
|
+
if leg.kind == "t5":
|
|
151
|
+
args = self.harness_args(
|
|
152
|
+
leg.q,
|
|
153
|
+
[
|
|
154
|
+
"--seqlen",
|
|
155
|
+
str(L),
|
|
156
|
+
"--heads_q",
|
|
157
|
+
str(self.hq),
|
|
158
|
+
"--heads_k",
|
|
159
|
+
str(self.hk),
|
|
160
|
+
"--headdim",
|
|
161
|
+
str(self.d),
|
|
162
|
+
"--page_size",
|
|
163
|
+
str(self.page),
|
|
164
|
+
*(["--batches", str(self.batch)] if self.batch != 1 else []),
|
|
165
|
+
*(["--q_len", str(self.q_len)] if self.q_len != 1 else []),
|
|
166
|
+
"--iterations",
|
|
167
|
+
str(iters),
|
|
168
|
+
"--warmup_iterations",
|
|
169
|
+
"0",
|
|
170
|
+
"--skip_ref_check",
|
|
171
|
+
"--no_l2_flush",
|
|
172
|
+
*shlex.split(leg.flags),
|
|
173
|
+
],
|
|
174
|
+
)
|
|
175
|
+
return "python3 " + " ".join(shlex.quote(a) for a in args), []
|
|
176
|
+
script = {"fi": "fi_driver.py", "fa2": "fa2_driver.py"}[leg.kind]
|
|
177
|
+
env = common_env + [f"{k}={v}" for k, v in leg.env]
|
|
178
|
+
return f"python3 {_IN_DRIVERS}/{script}", env
|
|
179
|
+
|
|
180
|
+
def ncu_leg(
|
|
181
|
+
self, log: Path, leg: Leg, L: int, iters: int, cache: str = "all", clock: str = "base"
|
|
182
|
+
) -> tuple[int, dict | None]:
|
|
183
|
+
cmd, env = self.leg_command(leg, L, iters)
|
|
184
|
+
name = self._name("mjolnir-ncu")
|
|
185
|
+
argv = [
|
|
186
|
+
"docker",
|
|
187
|
+
"run",
|
|
188
|
+
"--rm",
|
|
189
|
+
"--name",
|
|
190
|
+
name,
|
|
191
|
+
"--gpus",
|
|
192
|
+
"all",
|
|
193
|
+
"--network",
|
|
194
|
+
"host",
|
|
195
|
+
"--security-opt",
|
|
196
|
+
"seccomp=unconfined",
|
|
197
|
+
"--cap-add",
|
|
198
|
+
"SYS_ADMIN",
|
|
199
|
+
"--entrypoint",
|
|
200
|
+
"bash",
|
|
201
|
+
"-v",
|
|
202
|
+
f"{self.ncu_home}:{_IN_NCU}:ro",
|
|
203
|
+
"-v",
|
|
204
|
+
f"{DRIVERS_DIR}:{_IN_DRIVERS}:ro",
|
|
205
|
+
"-e",
|
|
206
|
+
"TMPDIR=/tmp/ncu-mjolnir",
|
|
207
|
+
]
|
|
208
|
+
if leg.kind == "t5":
|
|
209
|
+
argv += self._overlay()
|
|
210
|
+
for e in env:
|
|
211
|
+
argv += ["-e", e]
|
|
212
|
+
flt = FILTERS[leg.kind]
|
|
213
|
+
inner = (
|
|
214
|
+
f"mkdir -p /tmp/ncu-mjolnir && {_IN_NCU}/ncu --target-processes all "
|
|
215
|
+
f"--clock-control {clock} --cache-control {cache} --kernel-name {shlex.quote(flt)} "
|
|
216
|
+
f"--print-summary per-kernel --metrics {ncu.METRICS} {cmd}"
|
|
217
|
+
)
|
|
218
|
+
image = self.image_for(leg)
|
|
219
|
+
if not self._ensure(image, log):
|
|
220
|
+
return 2, None
|
|
221
|
+
argv += [image, "-c", inner]
|
|
222
|
+
rc = self._run(argv, log, name, timeout=max(self.timeout, 600.0))
|
|
223
|
+
if rc != 0 or self.dry_run:
|
|
224
|
+
return rc, None
|
|
225
|
+
from mjolnir_cli.verify.legs import MAIN, MERGE
|
|
226
|
+
|
|
227
|
+
kv_bytes = self.batch * L * self.hk * self.d * 2 * leg.kv_bits / 8
|
|
228
|
+
return rc, ncu.parse_file(log, MAIN[leg.kind], MERGE[leg.kind], L, kv_bytes)
|
|
229
|
+
|
|
230
|
+
# ── full ncu profile (fp6prof) ─────────────────────────────────────────
|
|
231
|
+
def ncu_profile(
|
|
232
|
+
self, outdir: Path, name: str, leg: Leg, L: int, count: int = 3, section_set: str = "full"
|
|
233
|
+
) -> tuple[int, dict]:
|
|
234
|
+
"""Profile ``count`` launches of the leg's decode kernel with a full
|
|
235
|
+
section set; returns (rc, {"raw": {...}, "units": {...}, "details": {...}})."""
|
|
236
|
+
outdir.mkdir(parents=True, exist_ok=True)
|
|
237
|
+
cmd, env = self.leg_command(leg, L, count + 2)
|
|
238
|
+
secs = (
|
|
239
|
+
"--section SpeedOfLight --section MemoryWorkloadAnalysis --section WarpStateStats "
|
|
240
|
+
"--section SchedulerStats --section Occupancy --section LaunchStats --section ComputeWorkloadAnalysis"
|
|
241
|
+
)
|
|
242
|
+
rc = 1
|
|
243
|
+
for attempt in (f"--set {section_set}", secs):
|
|
244
|
+
cname = self._name("mjolnir-prof")
|
|
245
|
+
inner = (
|
|
246
|
+
f"mkdir -p /tmp/ncu-mjolnir && {_IN_NCU}/ncu --target-processes all --clock-control base "
|
|
247
|
+
f"--cache-control all --kernel-name regex:MixedInputFusedMultiHeadAttentionDecode "
|
|
248
|
+
f"--launch-skip 1 --launch-count {count} {attempt} -f -o /out/{name} {cmd} && "
|
|
249
|
+
f"{_IN_NCU}/ncu --import /out/{name}.ncu-rep --page raw --csv > /out/{name}.raw.csv && "
|
|
250
|
+
f"{_IN_NCU}/ncu --import /out/{name}.ncu-rep --page details --csv > /out/{name}.details.csv && "
|
|
251
|
+
f"chmod a+rw /out/{name}.*"
|
|
252
|
+
)
|
|
253
|
+
from mjolnir_cli.dockerctl import owner_suffix
|
|
254
|
+
|
|
255
|
+
inner += owner_suffix([f"/out/{name}.ncu-rep", f"/out/{name}.raw.csv", f"/out/{name}.details.csv"])
|
|
256
|
+
argv = [
|
|
257
|
+
"docker",
|
|
258
|
+
"run",
|
|
259
|
+
"--rm",
|
|
260
|
+
"--name",
|
|
261
|
+
cname,
|
|
262
|
+
"--gpus",
|
|
263
|
+
"all",
|
|
264
|
+
"--security-opt",
|
|
265
|
+
"seccomp=unconfined",
|
|
266
|
+
"--cap-add",
|
|
267
|
+
"SYS_ADMIN",
|
|
268
|
+
"--entrypoint",
|
|
269
|
+
"bash",
|
|
270
|
+
"-v",
|
|
271
|
+
f"{self.ncu_home}:{_IN_NCU}:ro",
|
|
272
|
+
"-v",
|
|
273
|
+
f"{DRIVERS_DIR}:{_IN_DRIVERS}:ro",
|
|
274
|
+
"-v",
|
|
275
|
+
f"{outdir}:/out",
|
|
276
|
+
"-e",
|
|
277
|
+
"TMPDIR=/tmp/ncu-mjolnir",
|
|
278
|
+
]
|
|
279
|
+
image = self.image_for(leg)
|
|
280
|
+
if not self._ensure(image, outdir / f"{name}.log"):
|
|
281
|
+
return 2, {}
|
|
282
|
+
argv += (self._overlay() if leg.kind == "t5" else []) + [image, "-c", inner]
|
|
283
|
+
rc = self._run(argv, outdir / f"{name}.log", cname, timeout=max(self.timeout, 900.0))
|
|
284
|
+
if rc == 0:
|
|
285
|
+
break
|
|
286
|
+
print(f" ! {name}: ncu exit {rc} with [{attempt}] — retrying with fewer sections", file=sys.stderr)
|
|
287
|
+
if rc != 0 or self.dry_run:
|
|
288
|
+
return rc, {}
|
|
289
|
+
try:
|
|
290
|
+
raw, units = ncu.parse_raw_csv((outdir / f"{name}.raw.csv").read_text(errors="replace"))
|
|
291
|
+
det_p = outdir / f"{name}.details.csv"
|
|
292
|
+
details = ncu.parse_details_csv(det_p.read_text(errors="replace")) if det_p.exists() else {}
|
|
293
|
+
except (OSError, ValueError, ZeroDivisionError) as e: # a bad export loses one profile, not the run
|
|
294
|
+
print(f" ! {name}: could not parse the ncu export ({e}) — see {outdir}", file=sys.stderr)
|
|
295
|
+
return 1, {}
|
|
296
|
+
if not raw:
|
|
297
|
+
print(f" ! {name}: the ncu export has no metrics — see {outdir / (name + '.raw.csv')}", file=sys.stderr)
|
|
298
|
+
return 1, {}
|
|
299
|
+
return rc, {"raw": raw, "units": units, "details": details}
|
|
300
|
+
|
|
301
|
+
# ── vLLM-side tests (runner tests, smoke) ──────────────────────────────
|
|
302
|
+
def tests(self, log: Path, repo: Path, cmd: str, timeout: float, hf_home: Path) -> int:
|
|
303
|
+
name = self._name("mjolnir-vllm")
|
|
304
|
+
hf_home.mkdir(parents=True, exist_ok=True)
|
|
305
|
+
argv = [
|
|
306
|
+
"docker",
|
|
307
|
+
"run",
|
|
308
|
+
"--rm",
|
|
309
|
+
"--name",
|
|
310
|
+
name,
|
|
311
|
+
"--gpus",
|
|
312
|
+
"all",
|
|
313
|
+
"--ipc=host",
|
|
314
|
+
"--entrypoint",
|
|
315
|
+
"bash",
|
|
316
|
+
"-v",
|
|
317
|
+
f"{repo / 'src' / 'mjolnir'}:{_IN_SRC}:ro",
|
|
318
|
+
"-v",
|
|
319
|
+
f"{repo / 'tests' / 'kernel'}:/work/tests:ro",
|
|
320
|
+
"-v",
|
|
321
|
+
f"{hf_home}:/root/.cache/huggingface",
|
|
322
|
+
"-e",
|
|
323
|
+
f"PYTHONPATH={_IN_SRC}",
|
|
324
|
+
"-e",
|
|
325
|
+
f"HF_TOKEN={os.environ.get('HF_TOKEN', '')}",
|
|
326
|
+
"-e",
|
|
327
|
+
"TMPDIR=/tmp",
|
|
328
|
+
"-w",
|
|
329
|
+
"/work",
|
|
330
|
+
self.image,
|
|
331
|
+
"-c",
|
|
332
|
+
cmd,
|
|
333
|
+
]
|
|
334
|
+
return self._run(argv, log, name, timeout)
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"""``mjolnir verify kv-cache-size`` — how many tokens of KV each config holds.
|
|
2
|
+
|
|
3
|
+
Serves every config in turn (the vLLM server is restarted per leg, like
|
|
4
|
+
``bench e2e``), waits for the engine to size its KV cache, and reads the
|
|
5
|
+
numbers vLLM logs at startup::
|
|
6
|
+
|
|
7
|
+
Available KV cache memory: 38.12 GiB
|
|
8
|
+
GPU KV cache size: 2,656,033 tokens
|
|
9
|
+
Maximum concurrency for 204,800 tokens per request: 12.97x
|
|
10
|
+
|
|
11
|
+
The leg's server is stopped as soon as the numbers are in (no graph capture
|
|
12
|
+
or health wait needed). Charts turn the token count into the number of
|
|
13
|
+
concurrent requests each config fits at the e2e bench's request shapes.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import re
|
|
19
|
+
import time
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
from mjolnir_cli import dockerctl
|
|
23
|
+
from mjolnir_cli.config import DEFAULT_IMAGE, STOCK_IMAGE, config_field, resolve
|
|
24
|
+
from mjolnir_cli.legs import verify_legs
|
|
25
|
+
|
|
26
|
+
DEFAULT_LEGS = verify_legs("verify.kv-cache-size") # mjolnir_cli.legs
|
|
27
|
+
|
|
28
|
+
_AVAIL = re.compile(r"Available KV cache memory:\s*([0-9.]+)\s*GiB")
|
|
29
|
+
_TOKENS = re.compile(r"GPU KV cache size:\s*([0-9,]+)\s*tokens")
|
|
30
|
+
_CONC = re.compile(r"Maximum concurrency for\s*([0-9,]+)\s*tokens per request:\s*([0-9.]+)x")
|
|
31
|
+
# where the rest of the memory budget went (vLLM's startup profile; optional —
|
|
32
|
+
# wording differs between vLLM versions, absent fields stay None)
|
|
33
|
+
_MEMORY = {
|
|
34
|
+
"load_gib": re.compile(r"Model loading took\s*([0-9.]+)\s*GiB"),
|
|
35
|
+
"weights_gib": re.compile(r"model weights take\s*([0-9.]+)\s*GiB"),
|
|
36
|
+
"activation_gib": re.compile(r"(?:PyTorch )?activation peak memory takes\s*([0-9.]+)\s*GiB"),
|
|
37
|
+
"non_torch_gib": re.compile(r"non[_ ]torch[_ ]memory takes\s*([0-9.]+)\s*GiB"),
|
|
38
|
+
"cudagraph_gib": re.compile(
|
|
39
|
+
r"(?:CUDA graph|cudagraph)[^\n]*?(?:take|took|estimate[ds]?|memory)[^\n]*?([0-9.]+)\s*GiB", re.I
|
|
40
|
+
),
|
|
41
|
+
}
|
|
42
|
+
_FAILED = re.compile(r"EngineCore failed to start|Traceback \(most recent call last\)")
|
|
43
|
+
|
|
44
|
+
# the e2e bench's request: context depth + 2048 prompt + 128 generated tokens
|
|
45
|
+
E2E_PP, E2E_TG = 2048, 128
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def parse_log(text: str) -> dict:
|
|
49
|
+
"""The KV numbers from a vLLM startup log (absent ones are None)."""
|
|
50
|
+
out: dict = {"kv_gib": None, "kv_tokens": None, "max_model_len": None, "max_concurrency": None}
|
|
51
|
+
m = _AVAIL.search(text)
|
|
52
|
+
if m:
|
|
53
|
+
out["kv_gib"] = float(m.group(1))
|
|
54
|
+
m = _TOKENS.search(text)
|
|
55
|
+
if m:
|
|
56
|
+
out["kv_tokens"] = int(m.group(1).replace(",", ""))
|
|
57
|
+
m = _CONC.search(text)
|
|
58
|
+
if m:
|
|
59
|
+
out["max_model_len"] = int(m.group(1).replace(",", ""))
|
|
60
|
+
out["max_concurrency"] = float(m.group(2))
|
|
61
|
+
for key, rx in _MEMORY.items():
|
|
62
|
+
m = rx.search(text)
|
|
63
|
+
out[key] = float(m.group(1)) if m else None
|
|
64
|
+
return out
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def fits(kv_tokens: int, tokens_per_request: int) -> int:
|
|
68
|
+
"""Whole requests of ``tokens_per_request`` that fit in ``kv_tokens``."""
|
|
69
|
+
return kv_tokens // max(1, tokens_per_request)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _legs(spec: str) -> list[tuple[str, str]]:
|
|
73
|
+
out = []
|
|
74
|
+
for part in spec.replace(" ", "").split(","):
|
|
75
|
+
if not part:
|
|
76
|
+
continue
|
|
77
|
+
side, _, quant = part.partition(":")
|
|
78
|
+
if side not in ("official", "mjolnir") or not quant:
|
|
79
|
+
raise ValueError(f"leg {part!r}: expected official:<config> or mjolnir:<config>")
|
|
80
|
+
out.append((side, quant))
|
|
81
|
+
return out
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _docker_logs() -> str:
|
|
85
|
+
import subprocess
|
|
86
|
+
|
|
87
|
+
p = subprocess.run([dockerctl._docker(), "logs", dockerctl.CONTAINER_NAME], capture_output=True, text=True)
|
|
88
|
+
return dockerctl._strip_ansi((p.stdout or "") + (p.stderr or ""))
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _container_state() -> str:
|
|
92
|
+
rc, out = dockerctl._run_quiet(
|
|
93
|
+
[dockerctl._docker(), "inspect", "-f", "{{.State.Status}}", dockerctl.CONTAINER_NAME], ok_codes=(0, 1)
|
|
94
|
+
)
|
|
95
|
+
return out.strip() if rc == 0 else "absent"
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def stage_kvcache(
|
|
99
|
+
ctx,
|
|
100
|
+
legs: str = DEFAULT_LEGS,
|
|
101
|
+
model: str | None = None,
|
|
102
|
+
restart: bool = False,
|
|
103
|
+
timeout: float = 1800.0,
|
|
104
|
+
) -> dict:
|
|
105
|
+
"""Serve each leg, read its KV cache size, stop it."""
|
|
106
|
+
rows, cases = [], []
|
|
107
|
+
mj_image = ctx.runner.image or DEFAULT_IMAGE
|
|
108
|
+
if dockerctl.container_running() and not restart and not ctx.runner.dry_run:
|
|
109
|
+
ctx.out(
|
|
110
|
+
"[red] a vLLM server is running — this stage restarts the server for every leg. "
|
|
111
|
+
"Stop it in a window you own (mjolnir serve down), or pass --restart.[/red]"
|
|
112
|
+
)
|
|
113
|
+
return {
|
|
114
|
+
"rows": [],
|
|
115
|
+
"cases": [{"label": "server busy (not restarted)", "passed": False}],
|
|
116
|
+
"params": {"legs": legs},
|
|
117
|
+
}
|
|
118
|
+
(ctx.logdir / "kv-cache-size").mkdir(parents=True, exist_ok=True)
|
|
119
|
+
for side, quant in _legs(legs):
|
|
120
|
+
s = resolve(model, quant, STOCK_IMAGE if side == "official" else mj_image, None)
|
|
121
|
+
leg_id = f"{side}-{quant}"
|
|
122
|
+
log = ctx.logdir / "kv-cache-size" / f"{leg_id}.log"
|
|
123
|
+
kv_dtype = config_field(s.config_path, "kv-cache-dtype") or "auto"
|
|
124
|
+
row = {
|
|
125
|
+
"id": leg_id,
|
|
126
|
+
"side": side,
|
|
127
|
+
"config": quant,
|
|
128
|
+
"image": s.image,
|
|
129
|
+
"model": s.model,
|
|
130
|
+
"backend": s.attention_backend,
|
|
131
|
+
"kv_dtype": kv_dtype,
|
|
132
|
+
"gpu_memory_utilization": config_field(s.config_path, "gpu-memory-utilization"),
|
|
133
|
+
"log": str(log),
|
|
134
|
+
}
|
|
135
|
+
ctx.out(f" ▸ {leg_id} ({s.image}, kv {kv_dtype})")
|
|
136
|
+
if ctx.runner.dry_run:
|
|
137
|
+
dockerctl.serve_up(s, wait=False, dry_run=True)
|
|
138
|
+
continue
|
|
139
|
+
if not s.config_path.exists():
|
|
140
|
+
cases.append({"label": leg_id, "passed": False, "error": f"config not found: {s.config_path}"})
|
|
141
|
+
continue
|
|
142
|
+
stale = dockerctl.stale_image(s.image) if side == "mjolnir" else None
|
|
143
|
+
if stale:
|
|
144
|
+
ctx.out(f"[yellow] warning: {stale}[/yellow]")
|
|
145
|
+
t0 = time.time()
|
|
146
|
+
text, err = "", None
|
|
147
|
+
try:
|
|
148
|
+
dockerctl.serve_up(s, wait=False)
|
|
149
|
+
while time.time() - t0 < timeout:
|
|
150
|
+
time.sleep(5)
|
|
151
|
+
text = _docker_logs()
|
|
152
|
+
if _CONC.search(text) and _TOKENS.search(text):
|
|
153
|
+
break
|
|
154
|
+
if _FAILED.search(text) or _container_state() in ("exited", "dead", "absent"):
|
|
155
|
+
err = "the server failed before sizing its KV cache"
|
|
156
|
+
break
|
|
157
|
+
else:
|
|
158
|
+
err = f"no KV cache size after {timeout:.0f}s"
|
|
159
|
+
except dockerctl.DockerError as e:
|
|
160
|
+
err = str(e)
|
|
161
|
+
finally:
|
|
162
|
+
text = text or _docker_logs()
|
|
163
|
+
Path(log).write_text(text)
|
|
164
|
+
dockerctl.serve_down(s)
|
|
165
|
+
# an engine that crashed leaves an exited container behind, whose
|
|
166
|
+
# name would block the next leg's docker run
|
|
167
|
+
dockerctl._run_quiet([dockerctl._docker(), "rm", "-f", dockerctl.CONTAINER_NAME], ok_codes=(0, 1))
|
|
168
|
+
row.update(parse_log(text))
|
|
169
|
+
row["startup_s"] = round(time.time() - t0, 1)
|
|
170
|
+
ok = err is None and row["kv_tokens"] is not None
|
|
171
|
+
rows.append(row)
|
|
172
|
+
cases.append({"label": leg_id, "passed": ok, **({"error": err} if err else {}), "log": str(log)})
|
|
173
|
+
if ok:
|
|
174
|
+
ctx.out(
|
|
175
|
+
f" ✅ {row['kv_tokens']:,} tokens · {row['kv_gib'] or 0:.2f} GiB · "
|
|
176
|
+
f"{row['max_concurrency']:.2f}x at {row['max_model_len']:,} tokens/request"
|
|
177
|
+
)
|
|
178
|
+
else:
|
|
179
|
+
ctx.out(f" ❌ {err or 'KV cache size not found in the log'} — {log}")
|
|
180
|
+
return {"rows": rows, "cases": cases, "params": {"legs": legs, "model": model, "e2e_pp": E2E_PP, "e2e_tg": E2E_TG}}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""Benchmark legs: what one ncu-profiled series runs, and how to read it.
|
|
2
|
+
|
|
3
|
+
A *leg* is one kernel configuration swept over the L axis:
|
|
4
|
+
|
|
5
|
+
* ``mjolnir`` — the Mjolnir kernel at the harness default (fp8 Q, e2m3 KV
|
|
6
|
+
on head-major HND pages, 2 CTAs/SM, evict-first, the
|
|
7
|
+
2-stage fp6 ring) — the kernel's best measured config;
|
|
8
|
+
* ``mjolnir-fp8`` — the Mjolnir kernel on fp8 KV on HND pages (the
|
|
9
|
+
`NVFP4_MJOLNIR` config; the default serves e2m3);
|
|
10
|
+
* ``fi`` — FlashInfer tensor-core decode as vLLM runs it (fp8 KV,
|
|
11
|
+
NHD pages) — the strongest stock baseline on Thor;
|
|
12
|
+
* ``fi-hnd`` — FlashInfer on the same data in HND pages (fair layout check);
|
|
13
|
+
* ``fa2`` — FlashAttention 2 as stock vLLM 0.31.0 runs it on sm_110
|
|
14
|
+
(bf16 KV — FA2 has no fp8 KV path, so 2x the bytes);
|
|
15
|
+
* T5 variants — the Mjolnir harness with extra flags (ab / ring / ...).
|
|
16
|
+
|
|
17
|
+
Every leg profiles only its own kernels (``filter``) and splits them into
|
|
18
|
+
the main decode kernel(s) and the split-KV merge (``main`` / ``merge``
|
|
19
|
+
substrings of the ncu kernel name).
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import re
|
|
25
|
+
import shlex
|
|
26
|
+
from dataclasses import dataclass, field
|
|
27
|
+
|
|
28
|
+
# Bits per K/V element of each KV format (nvfp4 adds one e4m3 scale per 16).
|
|
29
|
+
KV_BITS = {"fp8": 8.0, "e3m2": 6.0, "e2m3": 6.0, "e2m1": 4.0, "nvfp4": 4.5, "bf16": 16.0}
|
|
30
|
+
|
|
31
|
+
# The harness default the `mjolnir` leg runs (kept in sync with
|
|
32
|
+
# mjolnir.t5_decode: HARNESS_KV_FORMAT_FP8Q / HARNESS_KV_LAYOUT).
|
|
33
|
+
HARNESS_KV_FORMAT = "e2m3"
|
|
34
|
+
HARNESS_KV_LAYOUT = "hnd"
|
|
35
|
+
|
|
36
|
+
FILTERS = {
|
|
37
|
+
"fi": "regex:BatchPrefillWithPagedKVCache|BatchDecode|MergeStates",
|
|
38
|
+
"fa2": "regex:flash_fwd",
|
|
39
|
+
"t5": "regex:MixedInputFusedMultiHeadAttentionDecode|reduction",
|
|
40
|
+
}
|
|
41
|
+
MAIN = {
|
|
42
|
+
"fi": ("BatchPrefillWithPagedKVCache", "BatchDecode"),
|
|
43
|
+
"fa2": ("flash_fwd_kernel", "flash_fwd_splitkv_kernel"),
|
|
44
|
+
"t5": ("MixedInputFusedMultiHeadAttentionDecode",),
|
|
45
|
+
}
|
|
46
|
+
MERGE = {
|
|
47
|
+
"fi": ("MergeStates",),
|
|
48
|
+
"fa2": ("combine",),
|
|
49
|
+
"t5": ("reduction",),
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass(frozen=True)
|
|
54
|
+
class Leg:
|
|
55
|
+
id: str # stable series id (file names, chart colors, README lookups)
|
|
56
|
+
label: str # human label
|
|
57
|
+
kind: str # "fi" | "fa2" | "t5"
|
|
58
|
+
role: str = "variant" # "ours" | "baseline" | "variant"
|
|
59
|
+
flags: str = "" # harness flags (t5) — appended after the common args
|
|
60
|
+
env: tuple[tuple[str, str], ...] = field(default_factory=tuple) # driver env (fi / fa2)
|
|
61
|
+
q: str = "fp8" # t5 Q dtype: fp8 | bf16
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def kv_format(self) -> str:
|
|
65
|
+
if self.kind == "fa2":
|
|
66
|
+
return "bf16"
|
|
67
|
+
if self.kind == "fi":
|
|
68
|
+
return "fp8"
|
|
69
|
+
fmt = _last_flag(self.flags, "--kv_format")
|
|
70
|
+
if fmt in (None, "auto"):
|
|
71
|
+
return HARNESS_KV_FORMAT if self.q == "fp8" else "fp8"
|
|
72
|
+
return fmt
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def kv_layout(self) -> str:
|
|
76
|
+
if self.kind == "fi":
|
|
77
|
+
return dict(self.env).get("FI_LAYOUT", "NHD").lower()
|
|
78
|
+
if self.kind == "fa2":
|
|
79
|
+
return "nhd"
|
|
80
|
+
return _last_flag(self.flags, "--kv_layout") or HARNESS_KV_LAYOUT
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def kv_bits(self) -> float:
|
|
84
|
+
return KV_BITS.get(self.kv_format, 8.0)
|
|
85
|
+
|
|
86
|
+
def describe(self) -> str:
|
|
87
|
+
return f"{self.kv_format} KV · {self.kv_layout.upper()}" + (f" · {self.flags}" if self.flags.strip() else "")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _last_flag(flags: str, name: str) -> str | None:
|
|
91
|
+
"""Value of the last ``--name value`` in a flag string (argparse: last wins)."""
|
|
92
|
+
out = None
|
|
93
|
+
toks = shlex.split(flags or "")
|
|
94
|
+
for i, t in enumerate(toks):
|
|
95
|
+
if t == name and i + 1 < len(toks):
|
|
96
|
+
out = toks[i + 1]
|
|
97
|
+
elif t.startswith(name + "="):
|
|
98
|
+
out = t.split("=", 1)[1]
|
|
99
|
+
return out
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
# ── the canonical legs ──────────────────────────────────────────────────────
|
|
103
|
+
MJOLNIR = Leg("mjolnir", "Mjolnir (e2m3 KV, HND)", "t5", role="ours")
|
|
104
|
+
MJOLNIR_FP8 = Leg("mjolnir-fp8", "Mjolnir (fp8 KV, HND)", "t5", role="ours", flags="--kv_format fp8 --kv_layout hnd")
|
|
105
|
+
FI = Leg("fi", "FlashInfer (fp8 KV, NHD)", "fi", role="baseline", env=(("FI_LAYOUT", "NHD"),))
|
|
106
|
+
FI_HND = Leg("fi-hnd", "FlashInfer (fp8 KV, HND)", "fi", role="baseline", env=(("FI_LAYOUT", "HND"),))
|
|
107
|
+
FA2 = Leg("fa2", "FlashAttention 2 (bf16 KV)", "fa2", role="baseline")
|
|
108
|
+
|
|
109
|
+
CANONICAL = {leg.id: leg for leg in (MJOLNIR, MJOLNIR_FP8, FI, FI_HND, FA2)}
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def t5_variant(name: str, flags: str, q: str = "fp8") -> Leg:
|
|
113
|
+
return Leg(f"t5:{name}", name, "t5", role="variant", flags=flags, q=q)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def parse_variants(spec: str) -> list[Leg]:
|
|
117
|
+
"""``"name:flags|name:flags"`` (the verify.sh AB_VARIANTS syntax) → legs."""
|
|
118
|
+
out = []
|
|
119
|
+
for part in (spec or "").split("|"):
|
|
120
|
+
part = part.strip()
|
|
121
|
+
if not part:
|
|
122
|
+
continue
|
|
123
|
+
name, _, flags = part.partition(":")
|
|
124
|
+
out.append(t5_variant(name.strip(), flags.strip()))
|
|
125
|
+
return out
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def legs_from_ids(ids: str | list[str]) -> list[Leg]:
|
|
129
|
+
if isinstance(ids, str):
|
|
130
|
+
ids = [x for x in re.split(r"[,\s]+", ids) if x]
|
|
131
|
+
unknown = [i for i in ids if i not in CANONICAL]
|
|
132
|
+
if unknown:
|
|
133
|
+
raise ValueError(f"unknown leg(s) {unknown}; known: {', '.join(CANONICAL)}")
|
|
134
|
+
return [CANONICAL[i] for i in ids]
|