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.
Files changed (80) hide show
  1. mjolnir_cli/__init__.py +8 -0
  2. mjolnir_cli/__main__.py +4 -0
  3. mjolnir_cli/accuracy.py +333 -0
  4. mjolnir_cli/autoresearch.py +539 -0
  5. mjolnir_cli/benchy.py +475 -0
  6. mjolnir_cli/bundled/configs/Qwen/Qwen3.8-27B/NVFP4/config.yaml +78 -0
  7. mjolnir_cli/bundled/configs/Qwen/Qwen3.8-27B/NVFP4_DFLASH2/config.yaml +103 -0
  8. mjolnir_cli/bundled/configs/Qwen/Qwen3.8-27B/NVFP4_DSPARK/config.yaml +102 -0
  9. mjolnir_cli/bundled/configs/Qwen/Qwen3.8-27B/NVFP4_FA2/config.yaml +84 -0
  10. mjolnir_cli/bundled/configs/Qwen/Qwen3.8-27B/NVFP4_FA4hd256/config.yaml +104 -0
  11. mjolnir_cli/bundled/configs/Qwen/Qwen3.8-27B/NVFP4_MJOLNIR/config.yaml +86 -0
  12. mjolnir_cli/bundled/configs/Qwen/Qwen3.8-27B/NVFP4_MJOLNIR_E2M3/config.yaml +93 -0
  13. mjolnir_cli/bundled/configs/Qwen/Qwen3.8-27B/NVFP4_MJOLNIR_E2M3_DFLASH2/config.yaml +123 -0
  14. mjolnir_cli/bundled/configs/Qwen/Qwen3.8-27B/NVFP4_MJOLNIR_E2M3_DSPARK/config.yaml +124 -0
  15. mjolnir_cli/bundled/configs/Qwen/Qwen3.8-27B/NVFP4_MJOLNIR_E2M3_LMH8/.env +2 -0
  16. mjolnir_cli/bundled/configs/Qwen/Qwen3.8-27B/NVFP4_MJOLNIR_E2M3_LMH8/config.yaml +98 -0
  17. mjolnir_cli/bundled/configs/Qwen/Qwen3.8-27B/NVFP4_MJOLNIR_E2M3_LMH8_VT32K/.env +4 -0
  18. mjolnir_cli/bundled/configs/Qwen/Qwen3.8-27B/NVFP4_MJOLNIR_E2M3_LMH8_VT32K/config.yaml +98 -0
  19. mjolnir_cli/bundled/configs/Qwen/Qwen3.8-27B/NVFP4_MJOLNIR_E2M3_LMH8_VT64K/.env +4 -0
  20. mjolnir_cli/bundled/configs/Qwen/Qwen3.8-27B/NVFP4_MJOLNIR_E2M3_LMH8_VT64K/config.yaml +102 -0
  21. mjolnir_cli/bundled/configs/Qwen/Qwen3.8-27B/NVFP4_MJOLNIR_E2M3_VT32K/.env +3 -0
  22. mjolnir_cli/bundled/configs/Qwen/Qwen3.8-27B/NVFP4_MJOLNIR_E2M3_VT32K/config.yaml +98 -0
  23. mjolnir_cli/bundled/configs/Qwen/Qwen3.8-27B/draft_vocab.json +1 -0
  24. mjolnir_cli/bundled/configs/Qwen/Qwen3.8-27B/kv_scales.json +35197 -0
  25. mjolnir_cli/bundled/configs/Qwen/Qwen3.8_chat_template.jinja +456 -0
  26. mjolnir_cli/bundled/docker/litellm/docker-compose.yaml +113 -0
  27. mjolnir_cli/bundled/docker/litellm/litellm.env.example +51 -0
  28. mjolnir_cli/bundled/docker/litellm/litellm_config.template.example.yaml +192 -0
  29. mjolnir_cli/bundled/docker/perses/README.md +89 -0
  30. mjolnir_cli/bundled/docker/perses/dashboards/global-datasource.yaml +17 -0
  31. mjolnir_cli/bundled/docker/perses/dashboards/performance_statistics.yaml +771 -0
  32. mjolnir_cli/bundled/docker/perses/dashboards/project.yaml +13 -0
  33. mjolnir_cli/bundled/docker/perses/dashboards/query_statistics.yaml +399 -0
  34. mjolnir_cli/bundled/docker/perses/docker-compose.yaml +51 -0
  35. mjolnir_cli/bundled/docker/perses/perses.yml +16 -0
  36. mjolnir_cli/bundled/docker/perses/prometheus.template.yaml +20 -0
  37. mjolnir_cli/bundled/scripts/hw/setup-thor.sh +1287 -0
  38. mjolnir_cli/cachectl.py +146 -0
  39. mjolnir_cli/charts.py +1687 -0
  40. mjolnir_cli/cli.py +1480 -0
  41. mjolnir_cli/clidoc.py +170 -0
  42. mjolnir_cli/config.py +346 -0
  43. mjolnir_cli/dockerctl.py +533 -0
  44. mjolnir_cli/drivers/fa2_driver.py +89 -0
  45. mjolnir_cli/drivers/fi_driver.py +134 -0
  46. mjolnir_cli/e2e.py +302 -0
  47. mjolnir_cli/fonts/InterDisplay-Bold.ttf +0 -0
  48. mjolnir_cli/fonts/InterDisplay-Medium.ttf +0 -0
  49. mjolnir_cli/fonts/InterDisplay-Regular.ttf +0 -0
  50. mjolnir_cli/fonts/InterDisplay-SemiBold.ttf +0 -0
  51. mjolnir_cli/fonts/OFL.txt +92 -0
  52. mjolnir_cli/gate.py +233 -0
  53. mjolnir_cli/history.py +159 -0
  54. mjolnir_cli/interactive.py +383 -0
  55. mjolnir_cli/legs.py +109 -0
  56. mjolnir_cli/litellm.py +254 -0
  57. mjolnir_cli/perses.py +214 -0
  58. mjolnir_cli/picker.py +70 -0
  59. mjolnir_cli/readme.py +1020 -0
  60. mjolnir_cli/stack.py +340 -0
  61. mjolnir_cli/tasks.py +122 -0
  62. mjolnir_cli/theme.py +281 -0
  63. mjolnir_cli/thor.py +108 -0
  64. mjolnir_cli/verify/__init__.py +2 -0
  65. mjolnir_cli/verify/cli.py +816 -0
  66. mjolnir_cli/verify/docker.py +334 -0
  67. mjolnir_cli/verify/kvcache.py +180 -0
  68. mjolnir_cli/verify/legs.py +134 -0
  69. mjolnir_cli/verify/ncu.py +141 -0
  70. mjolnir_cli/verify/profile.py +312 -0
  71. mjolnir_cli/verify/report.py +177 -0
  72. mjolnir_cli/verify/results.py +157 -0
  73. mjolnir_cli/verify/spec.py +279 -0
  74. mjolnir_cli/verify/stages.py +817 -0
  75. mjolnir_cli-0.2.0.dist-info/METADATA +87 -0
  76. mjolnir_cli-0.2.0.dist-info/RECORD +80 -0
  77. mjolnir_cli-0.2.0.dist-info/WHEEL +4 -0
  78. mjolnir_cli-0.2.0.dist-info/entry_points.txt +2 -0
  79. mjolnir_cli-0.2.0.dist-info/licenses/LICENSE +202 -0
  80. 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