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/benchy.py
ADDED
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
"""End-to-end perf bench — wraps ``llama-benchy`` directly.
|
|
2
|
+
|
|
3
|
+
We drive llama-benchy (the perf engine behind tool-eval-bench) instead of
|
|
4
|
+
tool-eval-bench itself: tool-eval-bench writes llama-benchy's raw JSON to a
|
|
5
|
+
temp file and deletes it, keeping only the per-cell mean. Calling
|
|
6
|
+
llama-benchy directly gives us the full raw data — per-run values, stddev —
|
|
7
|
+
which is what the charts and the history log need.
|
|
8
|
+
|
|
9
|
+
Protocol per ``--repeat`` sweep (all inside one clean-window gate):
|
|
10
|
+
llama-benchy --base-url ... --model ... \
|
|
11
|
+
--pp 2048 --tg 128 --depth 0 4096 8192 --concurrency 1 2 4 \
|
|
12
|
+
--runs 6 --warmup-runs 2 --latency-mode generation \
|
|
13
|
+
--no-cache --exact-tg --format json --save-result benchmarks/raw/<...>/benchy.json
|
|
14
|
+
|
|
15
|
+
* ``--runs N`` — N measured runs per (depth × pp × tg × concurrency) cell
|
|
16
|
+
* ``--warmup-runs``— discarded warmup runs per shape (JIT/warm-cache state)
|
|
17
|
+
* ``--exact-tg`` — pin output length (min_tokens + ignore_eos) so
|
|
18
|
+
EOS-early-stop can't skew the tg t/s variance
|
|
19
|
+
* raw JSON per sweep → benchmarks/raw/<ts>/benchy-r<i>.json (git-trackable)
|
|
20
|
+
* the vLLM server log (container start → bench end) →
|
|
21
|
+
benchmarks/raw/<ts>/vllm-server.log (kernel-dispatch debugging)
|
|
22
|
+
* normalized rows → benchmarks/history.jsonl (one line per run)
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import json
|
|
28
|
+
import shutil
|
|
29
|
+
import subprocess
|
|
30
|
+
import sys
|
|
31
|
+
import time
|
|
32
|
+
from pathlib import Path
|
|
33
|
+
|
|
34
|
+
from mjolnir_cli.config import Settings
|
|
35
|
+
from mjolnir_cli import dockerctl
|
|
36
|
+
from mjolnir_cli.gate import server_load, wait_for_idle
|
|
37
|
+
from mjolnir_cli import history as hist
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class BenchError(RuntimeError):
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def find_benchy() -> list[str]:
|
|
45
|
+
"""Resolve the llama-benchy invocation: installed binary on PATH first,
|
|
46
|
+
then a uv-managed tool, then ``uvx`` (auto-download)."""
|
|
47
|
+
if shutil.which("llama-benchy"):
|
|
48
|
+
return ["llama-benchy"]
|
|
49
|
+
for cand in (Path.home() / ".local" / "bin" / "llama-benchy", Path.home() / ".cargo" / "bin" / "llama-benchy"):
|
|
50
|
+
if cand.exists():
|
|
51
|
+
return [str(cand)]
|
|
52
|
+
if shutil.which("uv"):
|
|
53
|
+
return ["uvx", "llama-benchy"]
|
|
54
|
+
raise BenchError(
|
|
55
|
+
"llama-benchy not found. Install it once:\n uv tool install llama-benchy\n(or: pipx install llama-benchy)"
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def build_command(
|
|
60
|
+
benchy: list[str],
|
|
61
|
+
s: Settings,
|
|
62
|
+
runs: int,
|
|
63
|
+
warmup_runs: int,
|
|
64
|
+
exact_tg: bool,
|
|
65
|
+
depths: list[int],
|
|
66
|
+
concurrency: list[int],
|
|
67
|
+
pp: int,
|
|
68
|
+
tg: int,
|
|
69
|
+
out_json: Path,
|
|
70
|
+
api_key: str | None,
|
|
71
|
+
) -> list[str]:
|
|
72
|
+
cmd = [
|
|
73
|
+
*benchy,
|
|
74
|
+
"--base-url",
|
|
75
|
+
s.base_url,
|
|
76
|
+
"--model",
|
|
77
|
+
s.served_model,
|
|
78
|
+
"--pp",
|
|
79
|
+
str(pp),
|
|
80
|
+
"--tg",
|
|
81
|
+
str(tg),
|
|
82
|
+
"--depth",
|
|
83
|
+
*map(str, depths),
|
|
84
|
+
"--concurrency",
|
|
85
|
+
*map(str, concurrency),
|
|
86
|
+
"--runs",
|
|
87
|
+
str(runs),
|
|
88
|
+
"--latency-mode",
|
|
89
|
+
"generation",
|
|
90
|
+
"--no-cache",
|
|
91
|
+
"--format",
|
|
92
|
+
"json",
|
|
93
|
+
"--save-result",
|
|
94
|
+
str(out_json),
|
|
95
|
+
]
|
|
96
|
+
if exact_tg:
|
|
97
|
+
cmd += ["--exact-tg"]
|
|
98
|
+
if api_key:
|
|
99
|
+
cmd += ["--api-key", api_key]
|
|
100
|
+
return cmd
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
# Metrics that carry per-run ``values`` (pooled across sweeps for aggregation).
|
|
104
|
+
_METRICS = ("tg_tps", "tg_req_tps", "pp_tps", "peak_tps", "ttfr_ms", "ttft_ms", "itl_ms")
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _mean(vals: list[float]) -> float:
|
|
108
|
+
return sum(vals) / len(vals)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _std(vals: list[float]) -> float:
|
|
112
|
+
if len(vals) < 2:
|
|
113
|
+
return 0.0
|
|
114
|
+
m = _mean(vals)
|
|
115
|
+
return (sum((v - m) ** 2 for v in vals) / (len(vals) - 1)) ** 0.5
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _p95(vals: list[float]) -> float:
|
|
119
|
+
"""Linear-interpolated 95th percentile."""
|
|
120
|
+
if not vals:
|
|
121
|
+
return None
|
|
122
|
+
s = sorted(vals)
|
|
123
|
+
if len(s) == 1:
|
|
124
|
+
return s[0]
|
|
125
|
+
k = (len(s) - 1) * 0.95
|
|
126
|
+
lo, hi = int(k), min(int(k) + 1, len(s) - 1)
|
|
127
|
+
return s[lo] + (s[hi] - s[lo]) * (k - lo)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _aggregate_cells(sweep_cells: list[list[dict]]) -> list[dict]:
|
|
131
|
+
"""Pool the per-run samples of every cell across the ``repeat`` sweeps.
|
|
132
|
+
|
|
133
|
+
One logical bench run is a single record: its cells carry the pooled
|
|
134
|
+
per-run ``values`` with ``mean``/``std``/``p95`` recomputed over all
|
|
135
|
+
samples (``repeat × runs``). The history chart then plots one point per
|
|
136
|
+
run instead of one per sweep."""
|
|
137
|
+
from collections import defaultdict
|
|
138
|
+
|
|
139
|
+
groups: dict[tuple, list[dict]] = defaultdict(list)
|
|
140
|
+
for cells in sweep_cells:
|
|
141
|
+
for c in cells:
|
|
142
|
+
groups[(c.get("concurrency"), c.get("context"))].append(c)
|
|
143
|
+
out = []
|
|
144
|
+
for (conc, ctx), clist in groups.items():
|
|
145
|
+
base = {k: v for k, v in clist[0].items() if k not in _METRICS}
|
|
146
|
+
base["concurrency"], base["context"] = conc, ctx
|
|
147
|
+
for metric in _METRICS:
|
|
148
|
+
pooled: list[float] = []
|
|
149
|
+
for c in clist:
|
|
150
|
+
m = c.get(metric)
|
|
151
|
+
if m and m.get("values"):
|
|
152
|
+
pooled.extend(m["values"])
|
|
153
|
+
if pooled:
|
|
154
|
+
base[metric] = {"mean": _mean(pooled), "std": _std(pooled), "p95": _p95(pooled), "values": pooled}
|
|
155
|
+
out.append(base)
|
|
156
|
+
out.sort(key=lambda c: (c.get("context") or 0, c.get("concurrency") or 0))
|
|
157
|
+
return out
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _aggregate_gates(gates: list[dict | None]) -> dict | None:
|
|
161
|
+
"""Collapse the per-sweep gate summaries into one for the run record."""
|
|
162
|
+
if all(g is None for g in gates):
|
|
163
|
+
return None
|
|
164
|
+
opens = [g["open_ts"] for g in gates if g and g.get("open_ts") is not None]
|
|
165
|
+
return {
|
|
166
|
+
"clean": all(g and g.get("clean") for g in gates),
|
|
167
|
+
"confirm": (gates[0] or {}).get("confirm"),
|
|
168
|
+
"repeat": len(gates),
|
|
169
|
+
"open_ts": min(opens) if opens else None,
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def run_perf(
|
|
174
|
+
s: Settings,
|
|
175
|
+
layout,
|
|
176
|
+
runs: int = 6,
|
|
177
|
+
warmup_runs: int = 2,
|
|
178
|
+
exact_tg: bool = True,
|
|
179
|
+
repeat: int = 1,
|
|
180
|
+
depths: list[int] | None = None,
|
|
181
|
+
concurrency: list[int] | None = None,
|
|
182
|
+
pp: int = 2048,
|
|
183
|
+
tg: int = 128,
|
|
184
|
+
gate: bool = True,
|
|
185
|
+
confirm: int = 3,
|
|
186
|
+
gate_timeout_s: float = 3600,
|
|
187
|
+
api_key: str | None = None,
|
|
188
|
+
label: str | None = None,
|
|
189
|
+
) -> list[dict]:
|
|
190
|
+
"""Run ``repeat`` gated perf sweeps (default: one, 6 measured runs/cell);
|
|
191
|
+
persist raw + the vLLM server log + history; print llama-benchy's
|
|
192
|
+
terminal table; return the records."""
|
|
193
|
+
from mjolnir_cli.config import E2E_DEPTHS
|
|
194
|
+
|
|
195
|
+
depths = depths or list(E2E_DEPTHS)
|
|
196
|
+
concurrency = concurrency or [1, 2, 4]
|
|
197
|
+
benchy = find_benchy()
|
|
198
|
+
if label:
|
|
199
|
+
s.label = label
|
|
200
|
+
|
|
201
|
+
layout.raw_dir.mkdir(parents=True, exist_ok=True)
|
|
202
|
+
stamp = time.strftime("%Y%m%dT%H%M%SZ", time.gmtime())
|
|
203
|
+
out_dir = layout.raw_dir / f"perf-{stamp}"
|
|
204
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
205
|
+
|
|
206
|
+
print(f"[mjolnir] perf bench: {repeat} sweep(s), {runs} runs/cell, {warmup_runs} warmups/cell, exact-tg={exact_tg}")
|
|
207
|
+
print(f"[mjolnir] server: {s.base_url} ({s.backend_label})")
|
|
208
|
+
print(f"[mjolnir] raw → {out_dir}")
|
|
209
|
+
|
|
210
|
+
records = []
|
|
211
|
+
sweep_cells: list[list[dict]] = []
|
|
212
|
+
sweep_gates: list[dict | None] = []
|
|
213
|
+
sweep_spec: list[dict] = []
|
|
214
|
+
for i in range(repeat):
|
|
215
|
+
out_json = out_dir / f"benchy-r{i + 1}.json"
|
|
216
|
+
cmd = build_command(benchy, s, runs, warmup_runs, exact_tg, depths, concurrency, pp, tg, out_json, api_key)
|
|
217
|
+
print(f"\n[mjolnir] sweep {i + 1}/{repeat}:\n {' '.join(cmd)}")
|
|
218
|
+
gate_summary = None
|
|
219
|
+
if gate:
|
|
220
|
+
if server_load(s.metrics_url) is None:
|
|
221
|
+
raise BenchError(f"vLLM metrics {s.metrics_url} unreachable — is the server up? (mjolnir serve up)")
|
|
222
|
+
# One-shot clean-window check on the server under test (the
|
|
223
|
+
# --port). Wait until its queue is 0/0 for `confirm` samples,
|
|
224
|
+
# THEN start the bench. We do NOT monitor during the run: the
|
|
225
|
+
# bench's own in-flight requests would register as "dirty".
|
|
226
|
+
print(f"[mjolnir] waiting for a clean window ({confirm}× 0/0, up to {gate_timeout_s:.0f}s) …")
|
|
227
|
+
open_ts = wait_for_idle(s.metrics_url, confirm=confirm, timeout_s=gate_timeout_s)
|
|
228
|
+
if open_ts is None:
|
|
229
|
+
print(
|
|
230
|
+
f"[mjolnir] WARNING: timed out after "
|
|
231
|
+
f"{gate_timeout_s:.0f}s waiting for a clean window — "
|
|
232
|
+
f"sweep {i + 1} skipped",
|
|
233
|
+
file=sys.stderr,
|
|
234
|
+
)
|
|
235
|
+
continue
|
|
236
|
+
gate_summary = {"clean": True, "confirm": confirm, "open_ts": open_ts}
|
|
237
|
+
from mjolnir_cli.gate import server_counters, spec_delta
|
|
238
|
+
|
|
239
|
+
before = server_counters(s.metrics_url)
|
|
240
|
+
rc = subprocess.call(cmd)
|
|
241
|
+
if rc != 0:
|
|
242
|
+
raise BenchError(f"llama-benchy exited {rc} — see raw output")
|
|
243
|
+
spec_stats = spec_delta(before, server_counters(s.metrics_url))
|
|
244
|
+
if spec_stats:
|
|
245
|
+
sweep_spec.append(spec_stats)
|
|
246
|
+
if spec_stats.get("acceptance_length"):
|
|
247
|
+
print(
|
|
248
|
+
f"[mjolnir] speculative decoding: {spec_stats['acceptance_length']:.2f} tokens per step "
|
|
249
|
+
f"(acceptance {spec_stats['acceptance_rate']:.0%}, per position "
|
|
250
|
+
f"{' '.join(f'{p:.2f}' for p in spec_stats['per_pos'])}), "
|
|
251
|
+
f"{spec_stats['preemptions']} preemptions"
|
|
252
|
+
)
|
|
253
|
+
if not out_json.exists():
|
|
254
|
+
raise BenchError(f"llama-benchy produced no JSON at {out_json}")
|
|
255
|
+
|
|
256
|
+
raw = json.loads(out_json.read_text())
|
|
257
|
+
cells = hist.parse_benchy_json(raw)
|
|
258
|
+
sweep_cells.append(cells)
|
|
259
|
+
sweep_gates.append(gate_summary)
|
|
260
|
+
print(f"[mjolnir] sweep {i + 1}/{repeat} done ({len(cells)} cells)")
|
|
261
|
+
|
|
262
|
+
# The vLLM server log for kernel debugging: the WHOLE container log
|
|
263
|
+
# (startup kernel dispatch + every request of the bench), not just the
|
|
264
|
+
# bench window — what the attention backend got picked as is only in
|
|
265
|
+
# the startup lines.
|
|
266
|
+
full_log = out_dir / "vllm-server.log"
|
|
267
|
+
if dockerctl.dump_server_log(None, full_log):
|
|
268
|
+
print(f"[mjolnir] server log (container start → now) → {full_log}")
|
|
269
|
+
else:
|
|
270
|
+
print(
|
|
271
|
+
f"[mjolnir] (vLLM log not captured — container '{dockerctl.CONTAINER_NAME}' not found/running)",
|
|
272
|
+
file=sys.stderr,
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
if not sweep_cells:
|
|
276
|
+
return []
|
|
277
|
+
# One logical run = one history record: pool the per-run samples across
|
|
278
|
+
# all ``repeat`` sweeps so the chart plots a single point per run.
|
|
279
|
+
rec = hist.make_record(
|
|
280
|
+
s,
|
|
281
|
+
_aggregate_gates(sweep_gates),
|
|
282
|
+
runs,
|
|
283
|
+
warmup_runs,
|
|
284
|
+
exact_tg,
|
|
285
|
+
_aggregate_cells(sweep_cells),
|
|
286
|
+
str(out_dir.relative_to(layout.benchmarks_dir)),
|
|
287
|
+
)
|
|
288
|
+
rec["repeat"] = len(sweep_cells)
|
|
289
|
+
if sweep_spec: # pooled over the sweeps: tokens per verify step, the decode-rate multiplier
|
|
290
|
+
tot = {k: sum(x.get(k, 0) for x in sweep_spec) for k in ("drafts", "draft_tokens", "accepted", "preemptions")}
|
|
291
|
+
rec["spec_stats"] = {
|
|
292
|
+
**tot,
|
|
293
|
+
"acceptance_length": round(1 + tot["accepted"] / tot["drafts"], 3) if tot["drafts"] else None,
|
|
294
|
+
"acceptance_rate": round(tot["accepted"] / tot["draft_tokens"], 4) if tot["draft_tokens"] else None,
|
|
295
|
+
# pooled like the totals: accepted at each position over all drafts
|
|
296
|
+
"per_pos": [
|
|
297
|
+
round(sum(col) / tot["drafts"], 4)
|
|
298
|
+
for col in zip(*(x.get("per_pos_accepted") or [] for x in sweep_spec))
|
|
299
|
+
]
|
|
300
|
+
if tot["drafts"]
|
|
301
|
+
else [],
|
|
302
|
+
}
|
|
303
|
+
hist.append_record(layout.history_file, rec)
|
|
304
|
+
records.append(rec)
|
|
305
|
+
print_benchy_table(out_dir, rec)
|
|
306
|
+
return records
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
# ── the llama-benchy terminal table (the one the tool renders natively) ─────
|
|
310
|
+
#
|
|
311
|
+
# Same layout as llama-benchy's own md table: one row per test (pp rows +
|
|
312
|
+
# tg rows per context × concurrency), cells "mean ± std" over the pooled
|
|
313
|
+
# per-run values. Rebuilt from the raw benchy-r*.json files so the terminal
|
|
314
|
+
# shows the run's numbers next to the raw dump.
|
|
315
|
+
|
|
316
|
+
# raw-JSON metric fields, in table order.
|
|
317
|
+
_BENNY_METRICS = (
|
|
318
|
+
"pp_throughput",
|
|
319
|
+
"pp_req_throughput",
|
|
320
|
+
"tg_throughput",
|
|
321
|
+
"tg_req_throughput",
|
|
322
|
+
"peak_throughput",
|
|
323
|
+
"peak_req_throughput",
|
|
324
|
+
"ttfr",
|
|
325
|
+
"est_ppt",
|
|
326
|
+
"e2e_ttft",
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
# Table columns: (header, metric key). Full layout when >1 concurrency;
|
|
330
|
+
# the req-split columns collapse when concurrency is single.
|
|
331
|
+
_COLS_FULL = (
|
|
332
|
+
("t/s (total)", "total"),
|
|
333
|
+
("t/s (req)", "req"),
|
|
334
|
+
("peak t/s", "peak"),
|
|
335
|
+
("peak t/s (req)", "peak_req"),
|
|
336
|
+
("ttfr (ms)", "ttfr"),
|
|
337
|
+
("est_ppt (ms)", "est_ppt"),
|
|
338
|
+
("e2e_ttft (ms)", "e2e_ttft"),
|
|
339
|
+
)
|
|
340
|
+
_COLS_C1 = (
|
|
341
|
+
("t/s", "total"),
|
|
342
|
+
("peak t/s", "peak"),
|
|
343
|
+
("ttfr (ms)", "ttfr"),
|
|
344
|
+
("est_ppt (ms)", "est_ppt"),
|
|
345
|
+
("e2e_ttft (ms)", "e2e_ttft"),
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def _pool_benchy_files(out_dir: Path) -> tuple[str, int, list[dict]]:
|
|
350
|
+
"""Pool the per-run values across every sweep's raw JSON. Returns
|
|
351
|
+
(model, max_concurrency, pooled entries) in producer order: context
|
|
352
|
+
depth outermost (ctx-phase rows first, then the standard run),
|
|
353
|
+
concurrency innermost."""
|
|
354
|
+
groups: dict[tuple, list[dict]] = {}
|
|
355
|
+
model = ""
|
|
356
|
+
max_c = 1
|
|
357
|
+
for f in sorted(out_dir.glob("benchy-r*.json")):
|
|
358
|
+
try:
|
|
359
|
+
raw = json.loads(f.read_text())
|
|
360
|
+
except (json.JSONDecodeError, OSError):
|
|
361
|
+
continue
|
|
362
|
+
model = raw.get("model", model)
|
|
363
|
+
for b in raw.get("benchmarks", []):
|
|
364
|
+
key = (b.get("context_size") or 0, not bool(b.get("is_context_prefill_phase")), b.get("concurrency") or 1)
|
|
365
|
+
groups.setdefault(key, []).append(b)
|
|
366
|
+
max_c = max(max_c, b.get("concurrency") or 1)
|
|
367
|
+
entries = []
|
|
368
|
+
for key in sorted(groups):
|
|
369
|
+
blist = groups[key]
|
|
370
|
+
first = blist[0]
|
|
371
|
+
base = {
|
|
372
|
+
"concurrency": first.get("concurrency"),
|
|
373
|
+
"context_size": first.get("context_size") or 0,
|
|
374
|
+
"prompt_size": first.get("prompt_size"),
|
|
375
|
+
"response_size": first.get("response_size"),
|
|
376
|
+
"is_context_prefill_phase": bool(first.get("is_context_prefill_phase")),
|
|
377
|
+
}
|
|
378
|
+
for name in _BENNY_METRICS:
|
|
379
|
+
vals: list[float] = []
|
|
380
|
+
for b in blist:
|
|
381
|
+
m = b.get(name)
|
|
382
|
+
if isinstance(m, dict) and m.get("values"):
|
|
383
|
+
vals.extend(float(v) for v in m["values"])
|
|
384
|
+
if vals:
|
|
385
|
+
base[name] = {"mean": _mean(vals), "std": _std(vals)}
|
|
386
|
+
entries.append(base)
|
|
387
|
+
return model, max_c, entries
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def _entry_rows(model: str, max_c: int, e: dict) -> list[tuple[str, list]]:
|
|
391
|
+
"""One pooled entry → its pp row + tg row (the ctx variants when it is a
|
|
392
|
+
prefix-caching prefill-phase run). Cells = metric dicts (or None)."""
|
|
393
|
+
ctx = e["context_size"] or 0
|
|
394
|
+
conc = e["concurrency"] or 1
|
|
395
|
+
d = f" @ d{ctx}" if ctx else ""
|
|
396
|
+
c = f" (c{conc})" if max_c > 1 else ""
|
|
397
|
+
if e["is_context_prefill_phase"]:
|
|
398
|
+
pp_lbl, tg_lbl = f"ctx_pp{d}{c}", f"ctx_tg{d}{c}"
|
|
399
|
+
else:
|
|
400
|
+
pp_lbl = f"pp{e['prompt_size']}{d}{c}"
|
|
401
|
+
tg_lbl = f"tg{e['response_size']}{d}{c}"
|
|
402
|
+
pp_vals = {
|
|
403
|
+
"total": e.get("pp_throughput"),
|
|
404
|
+
"req": e.get("pp_req_throughput"),
|
|
405
|
+
"peak": None,
|
|
406
|
+
"peak_req": None,
|
|
407
|
+
"ttfr": e.get("ttfr"),
|
|
408
|
+
"est_ppt": e.get("est_ppt"),
|
|
409
|
+
"e2e_ttft": e.get("e2e_ttft"),
|
|
410
|
+
}
|
|
411
|
+
tg_vals = {
|
|
412
|
+
"total": e.get("tg_throughput"),
|
|
413
|
+
"req": e.get("tg_req_throughput"),
|
|
414
|
+
"peak": e.get("peak_throughput"),
|
|
415
|
+
"peak_req": e.get("peak_req_throughput"),
|
|
416
|
+
"ttfr": None,
|
|
417
|
+
"est_ppt": None,
|
|
418
|
+
"e2e_ttft": None,
|
|
419
|
+
}
|
|
420
|
+
cols = _COLS_FULL if max_c > 1 else _COLS_C1
|
|
421
|
+
out = []
|
|
422
|
+
for lbl, vals in ((pp_lbl, pp_vals), (tg_lbl, tg_vals)):
|
|
423
|
+
cells = [vals[k] for _, k in cols]
|
|
424
|
+
if any(m is not None for m in cells): # skip all-null rows
|
|
425
|
+
out.append((lbl, cells))
|
|
426
|
+
return out
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def _pipe_table(headers: list[str], rows: list[list[str]]) -> str:
|
|
430
|
+
"""A GitHub-pipe table (llama-benchy's md format): first column
|
|
431
|
+
left-aligned, metric columns right-aligned."""
|
|
432
|
+
aligns = ["<"] + [">"] * (len(headers) - 1)
|
|
433
|
+
widths = (
|
|
434
|
+
[max([len(h)] + [len(r[i]) for r in rows]) for i, h in enumerate(headers)]
|
|
435
|
+
if rows
|
|
436
|
+
else [len(h) for h in headers]
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
def data_row(cells: list[str]) -> str:
|
|
440
|
+
return (
|
|
441
|
+
"| " + " | ".join(c.rjust(w) if a == ">" else c.ljust(w) for c, w, a in zip(cells, widths, aligns)) + " |"
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
def sep_cell(a: str, w: int) -> str:
|
|
445
|
+
return ":" + "-" * (w - 1) if a == "<" else "-" * (w - 1) + ":"
|
|
446
|
+
|
|
447
|
+
sep = "|" + "|".join(sep_cell(a, w) for w, a in zip(widths, aligns)) + "|"
|
|
448
|
+
return "\n".join([data_row(headers), sep, *map(data_row, rows)])
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def print_benchy_table(out_dir: Path, rec: dict) -> None:
|
|
452
|
+
"""Print llama-benchy's terminal table for the run (pooled over its
|
|
453
|
+
sweeps), beside the raw JSON dump."""
|
|
454
|
+
model, max_c, entries = _pool_benchy_files(out_dir)
|
|
455
|
+
model = model or rec.get("model", "")
|
|
456
|
+
rows = [
|
|
457
|
+
[model] + [lbl] + [f"{m['mean']:.2f} ± {m['std']:.2f}" if m else "" for m in cells]
|
|
458
|
+
for e in entries
|
|
459
|
+
for lbl, cells in _entry_rows(model, max_c, e)
|
|
460
|
+
]
|
|
461
|
+
cols = _COLS_FULL if max_c > 1 else _COLS_C1
|
|
462
|
+
headers = ["model", "test"] + [h for h, _ in cols]
|
|
463
|
+
n = rec.get("repeat", 1) * rec["runs"]
|
|
464
|
+
print(
|
|
465
|
+
f"\n{'=' * 100}\n llama-benchy — {rec.get('backend')} "
|
|
466
|
+
f"({rec.get('image')})\n pooled over {rec.get('repeat', 1)} "
|
|
467
|
+
f"sweep(s) × {rec['runs']} runs = {n} samples/cell\n"
|
|
468
|
+
f"{'=' * 100}"
|
|
469
|
+
)
|
|
470
|
+
if rows:
|
|
471
|
+
print(_pipe_table(headers, rows))
|
|
472
|
+
else:
|
|
473
|
+
print(" (no results)")
|
|
474
|
+
print(f"{'=' * 100}")
|
|
475
|
+
print(f" raw: {out_dir} (+1 row to history.jsonl)")
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
model: QUASAR-QAT/Qwen3.8-27B-QUASAR-NVFP4
|
|
2
|
+
served-model-name: Qwen/Qwen3.8-27B
|
|
3
|
+
|
|
4
|
+
dtype: bfloat16
|
|
5
|
+
|
|
6
|
+
performance-mode: throughput
|
|
7
|
+
optimization-level: 3
|
|
8
|
+
compilation-config:
|
|
9
|
+
cudagraph_num_of_warmups: 5
|
|
10
|
+
custom_ops:
|
|
11
|
+
- +rotary_embedding
|
|
12
|
+
pass_config:
|
|
13
|
+
eliminate_noops: true
|
|
14
|
+
fuse_allreduce_rms: false
|
|
15
|
+
|
|
16
|
+
disable-custom-all-reduce: true
|
|
17
|
+
trust-remote-code: true
|
|
18
|
+
enforce-eager: false
|
|
19
|
+
|
|
20
|
+
max-num-seqs: 8
|
|
21
|
+
max-model-len: 200K
|
|
22
|
+
max-num-batched-tokens: 16K
|
|
23
|
+
gpu-memory-utilization: 0.80
|
|
24
|
+
|
|
25
|
+
enable-auto-tool-choice: true
|
|
26
|
+
tool-call-parser: qwen3_coder
|
|
27
|
+
reasoning-parser: qwen3
|
|
28
|
+
|
|
29
|
+
# ── Attention / linear backends ─────────────────────────────────────────────
|
|
30
|
+
attention-backend: flashinfer # TRITON_ATTN
|
|
31
|
+
enable-flashinfer-autotune: true
|
|
32
|
+
mm-encoder-attn-backend: TORCH_SDPA # SM110 ViT PTX-crash workaround (vLLM issue #38411)
|
|
33
|
+
linear-backend: auto # do NOT hardcode flashinfer-cutlass — that's the RTX5090/SM120 GEMM path, not SM110
|
|
34
|
+
|
|
35
|
+
# ── KV Cache ─────────────────────────────────────────────────────────────
|
|
36
|
+
kv-cache-dtype: fp8 # NVFP4 KV isn't available on SM110 for this arch; FP8 matches the checkpoint's own design (NVFP4 weights, FP8 KV)
|
|
37
|
+
kv-cache-dtype-skip-layers: sliding_window
|
|
38
|
+
mamba-cache-mode: align
|
|
39
|
+
mamba-cache-dtype: bfloat16
|
|
40
|
+
mamba-ssm-cache-dtype: bfloat16
|
|
41
|
+
|
|
42
|
+
# ── Multimodal ─────────────────────────────────────────────────────────────
|
|
43
|
+
language-model-only: false
|
|
44
|
+
mm-encoder-tp-mode: data
|
|
45
|
+
mm-processor-cache-type: shm
|
|
46
|
+
limit-mm-per-prompt:
|
|
47
|
+
image: 4
|
|
48
|
+
video: 0
|
|
49
|
+
|
|
50
|
+
# ── Caching & Scheduling ─────────────────────────────────────────────────
|
|
51
|
+
enable-prefix-caching: true
|
|
52
|
+
enable-chunked-prefill: true
|
|
53
|
+
async-scheduling: true
|
|
54
|
+
|
|
55
|
+
# ── Speculative Decoding — MTP (built-in head), K=3 ─────────────────────
|
|
56
|
+
speculative-config:
|
|
57
|
+
method: mtp
|
|
58
|
+
num_speculative_tokens: 3
|
|
59
|
+
|
|
60
|
+
# ── Generation defaults (Qwen3.8 card's documented Thinking-mode preset) ────
|
|
61
|
+
# NOTE: the verified Thor launch args do not set this server-side (sampling is
|
|
62
|
+
# left to the client/proxy). Included here as the model card's recommendation
|
|
63
|
+
# if you want it pinned at the server instead.
|
|
64
|
+
override-generation-config:
|
|
65
|
+
temperature: 1.0
|
|
66
|
+
top_p: 0.95
|
|
67
|
+
top_k: 20
|
|
68
|
+
min_p: 0.0
|
|
69
|
+
presence_penalty: 0.0
|
|
70
|
+
repetition_penalty: 1.0
|
|
71
|
+
|
|
72
|
+
# ── Chat Template ──────────────────────────────────────────────────────────
|
|
73
|
+
default-chat-template-kwargs:
|
|
74
|
+
reasoning_effort: medium
|
|
75
|
+
preserve_thinking: true
|
|
76
|
+
enable_thinking: true
|
|
77
|
+
|
|
78
|
+
chat-template: /configs/Qwen/Qwen3.8_chat_template.jinja
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# NVFP4_DFLASH2 — EXPERIMENT (opt-in e2e leg, official image): NVFP4 with the DFlash2 drafter
|
|
2
|
+
# instead of the built-in MTP head. Stock vLLM 0.31.0: FlashInfer, fp8 KV — what
|
|
3
|
+
# DFlash2 gives WITHOUT Mjolnir. Measure it next to the headline pair:
|
|
4
|
+
# mjolnir bench e2e --optional (all optional legs)
|
|
5
|
+
# mjolnir bench e2e --only official --official-config NVFP4_DFLASH2
|
|
6
|
+
# Untested on Thor before its first run: watch the startup log for the drafter's
|
|
7
|
+
# attention backend and the acceptance rate (`mjolnir verify spec` reads it).
|
|
8
|
+
model: QUASAR-QAT/Qwen3.8-27B-QUASAR-NVFP4
|
|
9
|
+
served-model-name: Qwen/Qwen3.8-27B
|
|
10
|
+
|
|
11
|
+
dtype: bfloat16
|
|
12
|
+
|
|
13
|
+
performance-mode: throughput
|
|
14
|
+
optimization-level: 3
|
|
15
|
+
compilation-config:
|
|
16
|
+
cudagraph_num_of_warmups: 5
|
|
17
|
+
custom_ops:
|
|
18
|
+
- +rotary_embedding
|
|
19
|
+
pass_config:
|
|
20
|
+
eliminate_noops: true
|
|
21
|
+
fuse_allreduce_rms: false
|
|
22
|
+
|
|
23
|
+
disable-custom-all-reduce: true
|
|
24
|
+
trust-remote-code: true
|
|
25
|
+
enforce-eager: false
|
|
26
|
+
|
|
27
|
+
max-num-seqs: 8
|
|
28
|
+
max-model-len: 200K
|
|
29
|
+
max-num-batched-tokens: 16K
|
|
30
|
+
gpu-memory-utilization: 0.80
|
|
31
|
+
|
|
32
|
+
enable-auto-tool-choice: true
|
|
33
|
+
tool-call-parser: qwen3_coder
|
|
34
|
+
reasoning-parser: qwen3
|
|
35
|
+
|
|
36
|
+
# ── Attention / linear backends ─────────────────────────────────────────────
|
|
37
|
+
attention-backend: flashinfer # TRITON_ATTN
|
|
38
|
+
enable-flashinfer-autotune: true
|
|
39
|
+
mm-encoder-attn-backend: TORCH_SDPA # SM110 ViT PTX-crash workaround (vLLM issue #38411)
|
|
40
|
+
linear-backend: auto # do NOT hardcode flashinfer-cutlass — that's the RTX5090/SM120 GEMM path, not SM110
|
|
41
|
+
|
|
42
|
+
# ── KV Cache ─────────────────────────────────────────────────────────────
|
|
43
|
+
kv-cache-dtype: fp8 # NVFP4 KV isn't available on SM110 for this arch; FP8 matches the checkpoint's own design (NVFP4 weights, FP8 KV)
|
|
44
|
+
# The drafter's layers (indices 64-68, after the target's 64) keep bf16 KV:
|
|
45
|
+
# its non-causal block attention then runs on any backend, and the KV it holds
|
|
46
|
+
# is small (5 layers, 8 KV heads).
|
|
47
|
+
kv-cache-dtype-skip-layers: ["sliding_window", "64", "65", "66", "67", "68"]
|
|
48
|
+
mamba-cache-mode: align
|
|
49
|
+
mamba-cache-dtype: bfloat16
|
|
50
|
+
mamba-ssm-cache-dtype: bfloat16
|
|
51
|
+
|
|
52
|
+
# ── Multimodal ─────────────────────────────────────────────────────────────
|
|
53
|
+
language-model-only: false
|
|
54
|
+
mm-encoder-tp-mode: data
|
|
55
|
+
mm-processor-cache-type: shm
|
|
56
|
+
# limit-mm-per-prompt:
|
|
57
|
+
# image: 4
|
|
58
|
+
# video: 0
|
|
59
|
+
|
|
60
|
+
# ── Caching & Scheduling ─────────────────────────────────────────────────
|
|
61
|
+
enable-prefix-caching: true
|
|
62
|
+
enable-chunked-prefill: true
|
|
63
|
+
async-scheduling: true
|
|
64
|
+
|
|
65
|
+
# ── Speculative Decoding — DFlash2 drafter, K=7 ─────────────────────────
|
|
66
|
+
# The DFlash2 block-diffusion drafter (5 Qwen3 layers, 2048-token sliding
|
|
67
|
+
# window, non-causal block attention, a grouped local convolution and a
|
|
68
|
+
# top-16 candidate selector) — architecture DFlash2DraftModel, native in vLLM
|
|
69
|
+
# 0.31.0 (vllm-project/vllm#52816) under method "dflash".
|
|
70
|
+
# num_speculative_tokens 7 — the checkpoint's block size is 8 (1 anchor + 7)
|
|
71
|
+
# the drafter shares the TARGET's lm_head: keep it unquantized — do not stack
|
|
72
|
+
# the fp8 lm_head / draft-vocab env knobs (LMH8, VT*) on this config
|
|
73
|
+
# DFlash2 lives on the V2 model runner (vLLM 0.31's default); if the log says
|
|
74
|
+
# "Model Runner V2 does not yet support …", the drafter silently degrades to
|
|
75
|
+
# DFlash1 drafting — check the acceptance rate before trusting a number
|
|
76
|
+
# z-lab/Qwen3.8-27B-DFlash2 mirrors the same checkpoint.
|
|
77
|
+
speculative-config:
|
|
78
|
+
method: dflash
|
|
79
|
+
model: incoai/Qwen3.8-27B-DFlash2
|
|
80
|
+
num_speculative_tokens: 7
|
|
81
|
+
# the drafter's attention: FlashInfer, like the target. If it fails to boot on
|
|
82
|
+
# sm_110, try attention_backend: FLASH_ATTN (its layers keep bf16 KV — the skip
|
|
83
|
+
# list in the KV Cache section).
|
|
84
|
+
|
|
85
|
+
# ── Generation defaults (Qwen3.8 card's documented Thinking-mode preset) ────
|
|
86
|
+
# NOTE: the verified Thor launch args do not set this server-side (sampling is
|
|
87
|
+
# left to the client/proxy). Included here as the model card's recommendation
|
|
88
|
+
# if you want it pinned at the server instead.
|
|
89
|
+
override-generation-config:
|
|
90
|
+
temperature: 1.0
|
|
91
|
+
top_p: 0.95
|
|
92
|
+
top_k: 20
|
|
93
|
+
min_p: 0.0
|
|
94
|
+
presence_penalty: 0.0
|
|
95
|
+
repetition_penalty: 1.0
|
|
96
|
+
|
|
97
|
+
# ── Chat Template ──────────────────────────────────────────────────────────
|
|
98
|
+
default-chat-template-kwargs:
|
|
99
|
+
reasoning_effort: medium
|
|
100
|
+
preserve_thinking: true
|
|
101
|
+
enable_thinking: true
|
|
102
|
+
|
|
103
|
+
chat-template: /configs/Qwen/Qwen3.8_chat_template.jinja
|