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
@@ -0,0 +1,8 @@
1
+ """mjolnir-cli — the ``mjolnir`` command for Jetson AGX Thor (sm_110).
2
+
3
+ Serve the Mjolnir vLLM image, verify and benchmark the Mjolnir decode
4
+ kernel against FlashInfer and FlashAttention 2, and render the
5
+ data-driven README charts — one command surface.
6
+ """
7
+
8
+ __version__ = "0.2.0"
@@ -0,0 +1,4 @@
1
+ from mjolnir_cli.cli import app
2
+
3
+ if __name__ == "__main__":
4
+ app()
@@ -0,0 +1,333 @@
1
+ """``mjolnir bench accuracy`` — does a serving config keep the model's quality?
2
+
3
+ Each leg (a config, optionally image-pinned, like ``bench ab``) is served in
4
+ turn and benchmarked with `tool-eval-bench
5
+ <https://github.com/SeraphimSerapis/tool-eval-bench>`_ over the OpenAI
6
+ endpoint. The default run is the standard 69 tool-calling scenarios plus the
7
+ 28 Hard Mode ones (``tool-eval-bench run --hardmode``); any other
8
+ tool-eval-bench invocation is forwarded verbatim after ``--`` — GSM8K, MMLU
9
+ and IFEval go through the same adapter (``-- --gsm8k --mmlu --ifeval
10
+ --skip-tool-eval``).
11
+
12
+ tool-eval-bench keeps its results in ``./data/benchmarks.sqlite`` and
13
+ ``./runs/`` of the directory it runs in; every leg runs in its own directory
14
+ under ``benchmarks/raw/accuracy/<run>/<leg>/``, so the database holds exactly
15
+ that leg's runs. :func:`summarize_db` reads it back into the committed
16
+ ``accuracy.json`` (``mjolnir.accuracy/1``): per suite the score, per
17
+ tool-call category the share of points, per scenario the points — what the
18
+ docs table and charts are rendered from.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import json
24
+ import os
25
+ import shlex
26
+ import shutil
27
+ import sqlite3
28
+ import subprocess
29
+ import sys
30
+ import time
31
+ from pathlib import Path
32
+ from statistics import mean
33
+
34
+ FORMAT = "mjolnir.accuracy/1"
35
+ TOOL_REPO = "https://github.com/SeraphimSerapis/tool-eval-bench"
36
+ # pinned: a score is only comparable to one produced by the same scenario set
37
+ TOOL_COMMIT = "b60e2f00f6c44352a3680356e7c7948f777c5280"
38
+ TOOL_SPEC = f"tool-eval-bench[hf] @ git+{TOOL_REPO}.git@{TOOL_COMMIT}"
39
+ DEFAULT_ARGS = ("run", "--hardmode", "--seed", "42", "--timeout", "600")
40
+ HARD_CATEGORY = "P" # tool-eval-bench's Hard Mode category
41
+
42
+ # suite -> (label, unit note); order = table / chart order
43
+ SUITES = {
44
+ "tools": "Tool calls",
45
+ "hardmode": "Hard Mode",
46
+ "gsm8k": "GSM8K",
47
+ "mmlu": "MMLU",
48
+ "ifeval": "IFEval",
49
+ "needle": "Needle",
50
+ }
51
+
52
+
53
+ class AccuracyError(RuntimeError):
54
+ pass
55
+
56
+
57
+ def find_tool() -> list[str]:
58
+ """The tool-eval-bench invocation: ``$MJOLNIR_TOOL_EVAL_BENCH`` if set,
59
+ else the pinned commit through ``uvx`` (no install needed), else one on
60
+ PATH."""
61
+ env = os.environ.get("MJOLNIR_TOOL_EVAL_BENCH", "").strip()
62
+ if env:
63
+ return shlex.split(env)
64
+ if shutil.which("uvx"):
65
+ return ["uvx", "--from", TOOL_SPEC, "tool-eval-bench"]
66
+ if shutil.which("tool-eval-bench"):
67
+ return ["tool-eval-bench"]
68
+ raise AccuracyError(f"tool-eval-bench not found — install uv (uvx runs it) or: uv tool install '{TOOL_SPEC}'")
69
+
70
+
71
+ def tool_args(passthrough: list[str] | tuple[str, ...], base_url: str) -> list[str]:
72
+ """The forwarded tool-eval-bench arguments (the default run when none),
73
+ pointed at the leg's server unless they name one."""
74
+ args = list(passthrough) or list(DEFAULT_ARGS)
75
+ if not any(a == "--base-url" or a.startswith("--base-url=") for a in args):
76
+ args += ["--base-url", base_url]
77
+ return args
78
+
79
+
80
+ # ── reading tool-eval-bench's database ──────────────────────────────────────
81
+
82
+
83
+ def _loads(v) -> dict:
84
+ try:
85
+ return json.loads(v) if v else {}
86
+ except (TypeError, json.JSONDecodeError):
87
+ return {}
88
+
89
+
90
+ def _pct(earned: float, mx: float) -> float | None:
91
+ return round(earned / mx * 100, 2) if mx else None
92
+
93
+
94
+ def summarize_db(db: Path) -> dict:
95
+ """tool-eval-bench's SQLite → ``{"suites": {suite: {...}}, "categories":
96
+ {...}, "scenarios": {...}}``. Several completed runs of a kind (``--trials``)
97
+ are averaged; each suite keeps its per-run scores."""
98
+ if not db.exists():
99
+ return {"suites": {}, "categories": {}, "scenarios": {}, "runs": []}
100
+ con = sqlite3.connect(f"file:{db}?mode=ro", uri=True)
101
+ try:
102
+ rows = con.execute(
103
+ "SELECT run_id, created_at, status, run_type, scores_json, config_json FROM scenario_runs "
104
+ "ORDER BY created_at"
105
+ ).fetchall()
106
+ finally:
107
+ con.close()
108
+ per_suite: dict[str, list[dict]] = {}
109
+ cats: dict[str, dict] = {}
110
+ scen: dict[str, list[float]] = {}
111
+ runs = []
112
+ for run_id, created, status, run_type, scores_json, config_json in rows:
113
+ runs.append({"run_id": run_id, "created_at": created, "status": status, "run_type": run_type})
114
+ if status != "completed":
115
+ continue
116
+ sc = _loads(scores_json)
117
+ if run_type == "tool_eval":
118
+ cs = sc.get("category_scores") or []
119
+ std = [c for c in cs if c.get("category") != HARD_CATEGORY]
120
+ hard = [c for c in cs if c.get("category") == HARD_CATEGORY]
121
+ for name, group in (("tools", std), ("hardmode", hard)):
122
+ e, m = sum(c.get("earned", 0) for c in group), sum(c.get("max", 0) for c in group)
123
+ if m:
124
+ per_suite.setdefault(name, []).append(
125
+ {
126
+ "score": _pct(e, m),
127
+ "earned": e,
128
+ "max": m,
129
+ "pass": sum(c.get("pass_count", 0) for c in group),
130
+ "partial": sum(c.get("partial_count", 0) for c in group),
131
+ "fail": sum(c.get("fail_count", 0) for c in group),
132
+ "final_score": sc.get("final_score"),
133
+ "completion_rate": sc.get("completion_rate", 100.0),
134
+ "safety_warnings": len(sc.get("safety_warnings") or []),
135
+ "run_id": run_id,
136
+ }
137
+ )
138
+ for c in cs:
139
+ d = cats.setdefault(c["category"], {"label": c.get("label", c["category"]), "percent": []})
140
+ if c.get("max"):
141
+ d["percent"].append(_pct(c.get("earned", 0), c["max"]))
142
+ for r in sc.get("scenario_results") or []:
143
+ sid = r.get("scenario_id")
144
+ if sid is not None and r.get("points") is not None:
145
+ scen.setdefault(sid, []).append(float(r["points"]))
146
+ else:
147
+ acc = sc.get("accuracy", sc.get("final_score"))
148
+ if acc is None:
149
+ continue
150
+ extra = {
151
+ k: sc[k]
152
+ for k in ("instruction_accuracy", "correct", "total", "errors", "completion_rate", "truncated")
153
+ if k in sc
154
+ }
155
+ per_suite.setdefault(run_type, []).append({"score": round(float(acc), 2), **extra, "run_id": run_id})
156
+ suites = {}
157
+ for name, items in per_suite.items():
158
+ scores = [i["score"] for i in items if i.get("score") is not None]
159
+ if not scores:
160
+ continue
161
+ last = dict(items[-1])
162
+ last.pop("score", None)
163
+ suites[name] = {"score": round(mean(scores), 2), "scores": scores, "n": len(scores), **last}
164
+ return {
165
+ "suites": suites,
166
+ "categories": {
167
+ k: {"label": v["label"], "percent": round(mean(v["percent"]), 2)}
168
+ for k, v in sorted(cats.items())
169
+ if v["percent"]
170
+ },
171
+ "scenarios": {k: round(mean(v), 3) for k, v in sorted(scen.items())},
172
+ "runs": runs,
173
+ }
174
+
175
+
176
+ # ── the run ─────────────────────────────────────────────────────────────────
177
+
178
+
179
+ def leg_id(model: str, image: str, config: str) -> str:
180
+ side = "official" if not _is_mjolnir(image) else "mjolnir"
181
+ return f"{side}-{config}"
182
+
183
+
184
+ def _is_mjolnir(image: str) -> bool:
185
+ from mjolnir_cli import e2e
186
+
187
+ return e2e.is_mjolnir({"image": image})
188
+
189
+
190
+ def kv_label(rec: dict) -> str:
191
+ kv = str(rec.get("kv_cache_dtype") or "auto")
192
+ return {"fp6_e2m3": "fp6 e2m3 KV", "fp8": "fp8 KV", "fp8_e4m3": "fp8 KV", "auto": "bf16 KV"}.get(kv, f"{kv} KV")
193
+
194
+
195
+ def leg_label(rec: dict) -> str:
196
+ """'official vLLM · FlashInfer · fp8 KV', 'Mjolnir · fp6 e2m3 KV + fp8 lm_head + 64K draft vocab', ...
197
+ — every label names the KV-cache format."""
198
+ from mjolnir_cli.config import BACKEND_LABELS
199
+
200
+ cfg = str(rec.get("config", ""))
201
+ kv = kv_label(rec)
202
+ if not _is_mjolnir(str(rec.get("image", ""))):
203
+ return f"official vLLM · {BACKEND_LABELS.get(cfg, cfg)} · {kv}"
204
+ if "MJOLNIR" not in cfg:
205
+ return f"Mjolnir image · {BACKEND_LABELS.get(cfg, cfg)} · {kv}"
206
+ extra = "".join(
207
+ f" + {name}"
208
+ for tag, name in (("LMH8", "fp8 lm_head"), ("VT32K", "32K draft vocab"), ("VT64K", "64K draft vocab"))
209
+ if tag in cfg.split("_")
210
+ )
211
+ return f"Mjolnir · {kv}{extra}"
212
+
213
+
214
+ def run_accuracy(
215
+ s,
216
+ layout,
217
+ legs: list[tuple[str, str, str]],
218
+ passthrough: list[str],
219
+ restart: bool = True,
220
+ ) -> Path:
221
+ """Serve each ``(model, image, config)`` leg (restarted per leg unless
222
+ ``restart`` is off: then the running server is the one leg), run
223
+ tool-eval-bench against it, and write ``accuracy.json``. Returns its path."""
224
+ from mjolnir_cli import dockerctl
225
+ from mjolnir_cli.config import config_field
226
+
227
+ tool = find_tool()
228
+ run_id = time.strftime("%Y%m%d-%H%M%S")
229
+ out_dir = layout.raw_dir / "accuracy" / run_id
230
+ out_dir.mkdir(parents=True, exist_ok=True)
231
+ doc = {
232
+ "format": FORMAT,
233
+ "run_id": run_id,
234
+ "created": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
235
+ "tool": {"repo": TOOL_REPO, "commit": TOOL_COMMIT, "command": tool},
236
+ "args": list(passthrough) or list(DEFAULT_ARGS),
237
+ "legs": [],
238
+ }
239
+ out = out_dir / "accuracy.json"
240
+ for model, image, config in legs:
241
+ s.model, s.image, s.quant = model, image, config
242
+ lid = leg_id(model, image, config)
243
+ leg_dir = out_dir / lid
244
+ leg_dir.mkdir(parents=True, exist_ok=True)
245
+ print(f"\n{'=' * 62}\n accuracy leg: {model}/{config} (image {image})\n{'=' * 62}", flush=True)
246
+ rec = {
247
+ "id": lid,
248
+ "model": model,
249
+ "image": image,
250
+ "config": config,
251
+ "kv_cache_dtype": config_field(s.config_path, "kv-cache-dtype") or "auto",
252
+ "env": dockerctl.config_env(s),
253
+ }
254
+ rec["label"] = leg_label(rec)
255
+ try:
256
+ if restart:
257
+ dockerctl.serve_down(s)
258
+ dockerctl.serve_up(s, wait=True)
259
+ cmd = tool + tool_args(passthrough, s.base_url)
260
+ print(f"[mjolnir] {' '.join(shlex.quote(c) for c in cmd)}\n[mjolnir] in {leg_dir}", flush=True)
261
+ t0 = time.time()
262
+ rc = subprocess.call(cmd, cwd=leg_dir)
263
+ rec["exit_code"] = rc
264
+ rec["wall_s"] = round(time.time() - t0, 1)
265
+ if rc not in (0, 2): # 2: e.g. --fail-on-safety tripped — the scores still stand
266
+ print(f"[mjolnir] tool-eval-bench exited {rc} — keeping what it saved", file=sys.stderr)
267
+ dockerctl.dump_server_log(None, leg_dir / "vllm-server.log")
268
+ finally:
269
+ if restart:
270
+ print(f" leg done: stopping the vLLM server ({image})", flush=True)
271
+ dockerctl.serve_down(s)
272
+ rec.update(summarize_db(leg_dir / "data" / "benchmarks.sqlite"))
273
+ doc["legs"].append(rec)
274
+ out.write_text(json.dumps(doc, indent=1) + "\n")
275
+ return out
276
+
277
+
278
+ # ── reading the committed results ───────────────────────────────────────────
279
+
280
+
281
+ def load_runs(layout) -> list[dict]:
282
+ root = layout.raw_dir / "accuracy"
283
+ docs = []
284
+ for p in sorted(root.glob("*/accuracy.json")):
285
+ try:
286
+ d = json.loads(p.read_text())
287
+ except (OSError, json.JSONDecodeError):
288
+ continue
289
+ if d.get("format") == FORMAT:
290
+ docs.append(d)
291
+ return sorted(docs, key=lambda d: d.get("run_id", ""))
292
+
293
+
294
+ def latest_legs(docs: list[dict]) -> list[dict]:
295
+ """One row per (model, image, config): every suite from the newest run
296
+ that measured it (an A/B run and a later GSM8K run merge into one row).
297
+ Order: first appearance."""
298
+ rows: dict[tuple, dict] = {}
299
+ for d in docs:
300
+ for leg in d.get("legs", []):
301
+ key = (leg.get("model"), leg.get("image"), leg.get("config"))
302
+ row = rows.setdefault(
303
+ key,
304
+ {k: leg.get(k) for k in ("id", "model", "image", "config", "label", "kv_cache_dtype", "env")}
305
+ | {"suites": {}, "categories": {}, "scenarios": {}, "runs": {}},
306
+ )
307
+ row["kv_cache_dtype"] = leg.get("kv_cache_dtype") or row.get("kv_cache_dtype")
308
+ row["label"] = leg_label(row) # recomputed: labels improve after a run is stored
309
+ for name, suite in (leg.get("suites") or {}).items():
310
+ row["suites"][name] = suite
311
+ row["runs"][name] = {"run_id": d.get("run_id"), "args": d.get("args")}
312
+ if (leg.get("suites") or {}).keys() & {"tools", "hardmode"}:
313
+ row["categories"] = leg.get("categories") or row["categories"]
314
+ row["scenarios"] = leg.get("scenarios") or row["scenarios"]
315
+ return [r for r in rows.values() if r["suites"]]
316
+
317
+
318
+ def baseline(rows: list[dict]) -> dict | None:
319
+ """The reference row for deltas: native FlashInfer on the official image
320
+ when measured, else the first row."""
321
+ for r in rows:
322
+ if not _is_mjolnir(str(r.get("image", ""))) and r.get("config") == "NVFP4":
323
+ return r
324
+ return rows[0] if rows else None
325
+
326
+
327
+ def suites_in(rows: list[dict]) -> list[str]:
328
+ seen = {n for r in rows for n in r["suites"]}
329
+ return [n for n in SUITES if n in seen] + sorted(seen - set(SUITES))
330
+
331
+
332
+ def suite_label(name: str) -> str:
333
+ return SUITES.get(name, name.upper())