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/theme.py ADDED
@@ -0,0 +1,281 @@
1
+ """The Mjolnir chart design system — one visual language, light AND dark.
2
+
3
+ Every chart is rendered twice (``<name>.png`` light, ``<name>-dark.png``
4
+ dark) and the README switches with ``<picture>`` + ``prefers-color-scheme``,
5
+ so a chart always sits on its own surface on GitHub.
6
+
7
+ Color = identity, and identities are fixed (never assigned by rank):
8
+
9
+ * **Mjolnir** — orange (the hammer). A second Mjolnir series (fp8 KV next
10
+ to the e2m3 headline) keeps the SAME orange with a hollow marker + a direct
11
+ label: identity by hue, variant by marker fill + label (lighter/darker
12
+ tints of one hue fail the normal-vision floor on the dark surface — the
13
+ validator rejected every tint tried, so tints are not used).
14
+ * **FlashInfer** — blue (the strongest stock baseline).
15
+ * **FlashAttention 2** — aqua (stock vLLM 0.31.0's sm_110 default).
16
+
17
+ Those three slots are the validated reference palette's first three steps:
18
+ they pass the all-pairs colorblind (CVD ΔE ≥ 8) and normal-vision (ΔE ≥ 15)
19
+ checks in both modes (scripts/validate_palette.js; light-mode aqua sits
20
+ below 3:1 contrast → FA2 series are always direct-labeled and every chart
21
+ has a data table in the README). Speedup heatmaps use the diverging pair
22
+ blue (FlashInfer faster) ↔ neutral gray (parity) ↔ orange (Mjolnir faster).
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ from dataclasses import dataclass
28
+
29
+
30
+ @dataclass(frozen=True)
31
+ class Mode:
32
+ name: str # "light" | "dark"
33
+ bg: str # figure background
34
+ panel: str # axes background (the chart surface the palette is validated on)
35
+ text: str
36
+ text_dim: str
37
+ grid: str
38
+ mjolnir: str
39
+ flashinfer: str
40
+ fa2: str
41
+ neutral: str # diverging midpoint / "other" marks
42
+ suffix: str # file-name suffix
43
+
44
+
45
+ LIGHT = Mode(
46
+ name="light",
47
+ bg="#ffffff",
48
+ panel="#fcfcfb",
49
+ text="#0b0b0b",
50
+ text_dim="#52514e",
51
+ grid="#e6e5e1",
52
+ mjolnir="#eb6834",
53
+ flashinfer="#2a78d6",
54
+ fa2="#1baf7a",
55
+ neutral="#f0efec",
56
+ suffix="",
57
+ )
58
+
59
+ DARK = Mode(
60
+ name="dark",
61
+ bg="#0d1117", # GitHub dark
62
+ panel="#151b26",
63
+ text="#e6edf3",
64
+ text_dim="#8b949e",
65
+ grid="#21262d",
66
+ mjolnir="#d95926",
67
+ flashinfer="#3987e5",
68
+ fa2="#199e70",
69
+ neutral="#383835",
70
+ suffix="-dark",
71
+ )
72
+
73
+ MODES = (LIGHT, DARK)
74
+
75
+ # Series id -> (color attribute on Mode, marker, filled). Ids match verify
76
+ # legs and e2e configs, so a series has the same look in every chart.
77
+ SERIES_STYLE = {
78
+ "mjolnir": ("mjolnir", "o", True),
79
+ "mjolnir-fp8": ("mjolnir", "s", False),
80
+ "fi": ("flashinfer", "D", True),
81
+ "fi-hnd": ("flashinfer", "d", False),
82
+ "fa2": ("fa2", "^", True),
83
+ # e2e configs
84
+ "NVFP4_MJOLNIR": ("mjolnir", "s", False), # fp8 KV: the hollow Mjolnir variant
85
+ "NVFP4_MJOLNIR_E2M3": ("mjolnir", "o", True),
86
+ "NVFP4": ("flashinfer", "D", True),
87
+ "NVFP4_FA2": ("fa2", "^", True),
88
+ # the fast experiment config: Mjolnir orange, plus marker, dotted fill in bars
89
+ "NVFP4_MJOLNIR_E2M3_LMH8_VT64K": ("mjolnir", "P", True),
90
+ }
91
+ # bar fill pattern per series (identity is never color-only: hollow = fp8 KV,
92
+ # dotted = the fp8 lm_head + draft-vocab experiment)
93
+ SERIES_HATCH = {"NVFP4_MJOLNIR_E2M3_LMH8_VT64K": ".."}
94
+
95
+
96
+ def color(mode: Mode, series_id: str) -> str:
97
+ attr = SERIES_STYLE.get(series_id, ("text_dim", "o", True))[0]
98
+ return getattr(mode, attr)
99
+
100
+
101
+ def marker(series_id: str) -> str:
102
+ return SERIES_STYLE.get(series_id, ("text_dim", "o", True))[1]
103
+
104
+
105
+ def filled(series_id: str) -> bool:
106
+ return SERIES_STYLE.get(series_id, ("text_dim", "o", True))[2]
107
+
108
+
109
+ def hatch(series_id: str) -> str | None:
110
+ if not filled(series_id):
111
+ return "//"
112
+ return SERIES_HATCH.get(series_id)
113
+
114
+
115
+ def line_kw(mode: Mode, series_id: str) -> dict:
116
+ """2px line + >=8px marker; hollow markers carry a variant (same hue)."""
117
+ c = color(mode, series_id)
118
+ return dict(
119
+ color=c,
120
+ lw=2.0,
121
+ marker=marker(series_id),
122
+ ms=8,
123
+ mfc=c if filled(series_id) else mode.panel,
124
+ mec=c,
125
+ mew=2.0,
126
+ zorder=5,
127
+ )
128
+
129
+
130
+ def diverging_cmap(mode: Mode):
131
+ """blue (FlashInfer faster) -> gray (parity) -> orange (Mjolnir faster)."""
132
+ from matplotlib.colors import LinearSegmentedColormap
133
+
134
+ return LinearSegmentedColormap.from_list(f"mjolnir-div-{mode.name}", [mode.flashinfer, mode.neutral, mode.mjolnir])
135
+
136
+
137
+ # status scale for "how much worse than the baseline" (accuracy heatmaps):
138
+ # soft tints in light mode, deep ones in dark mode, so the cell text (mode.text)
139
+ # stays readable on every step; identity colors are never reused for it
140
+ STATUS = {
141
+ "light": ("#bbf7d0", "#fed7aa", "#fecaca"), # on par / worse / much worse
142
+ "dark": ("#14532d", "#7c2d12", "#7f1d1d"),
143
+ }
144
+
145
+
146
+ def status_cmap(mode: Mode, par: float, span: float):
147
+ """0 → ``span`` points worse: green up to ``par``, then to orange at the
148
+ middle of the rest, red at ``span``."""
149
+ from matplotlib.colors import LinearSegmentedColormap
150
+
151
+ good, warn, bad = STATUS[mode.name]
152
+ p = max(0.0, min(0.9, par / span))
153
+ return LinearSegmentedColormap.from_list(
154
+ f"mjolnir-status-{mode.name}", [(0.0, good), (p, good), ((1 + p) / 2, warn), (1.0, bad)]
155
+ )
156
+
157
+
158
+ def apply(fig, axes, mode: Mode) -> None:
159
+ """Surfaces, recessive hairline grid, muted ticks — for one or many axes."""
160
+ fig.patch.set_facecolor(mode.bg)
161
+ for ax in axes if isinstance(axes, (list, tuple)) else [axes]:
162
+ ax.set_facecolor(mode.panel)
163
+ for s in ax.spines.values():
164
+ s.set_visible(False)
165
+ ax.tick_params(colors=mode.text_dim, labelsize=10, which="both", length=0)
166
+ ax.grid(True, color=mode.grid, linewidth=0.8, zorder=0)
167
+ ax.set_axisbelow(True)
168
+ ax.xaxis.label.set_color(mode.text_dim)
169
+ ax.yaxis.label.set_color(mode.text_dim)
170
+
171
+
172
+ def title(ax, mode: Mode, text: str, subtitle: str | None = None, fontsize: int = 16) -> None:
173
+ """Left-aligned bold title + a dim subtitle above the axes. The subtitle
174
+ wraps at the axes' width (at `` · `` joints): one wider than the plot
175
+ would stretch the saved image and leave an empty band on the right."""
176
+ sub_lines = []
177
+ if subtitle:
178
+ fig = ax.figure
179
+ pos = ax.get_position() # the subtitle may run across every panel of the top row
180
+ right = max(a.get_position().x1 for a in fig.axes if abs(a.get_position().y1 - pos.y1) < 0.02)
181
+ width_in = (right - pos.x0) * fig.get_figwidth()
182
+ per_line = max(30, int(width_in * 72 / (10.5 * 0.56))) # DejaVu Sans ≈ 0.56 em per char
183
+ cur = ""
184
+ for part in subtitle.split(" · "):
185
+ if cur and len(cur) + 3 + len(part) > per_line:
186
+ sub_lines.append(cur)
187
+ cur = part
188
+ else:
189
+ cur = f"{cur} · {part}" if cur else part
190
+ sub_lines.append(cur)
191
+ nlines = text.count("\n") + 1
192
+ pad = (26 if subtitle else 10) + (fontsize + 5) * (nlines - 1) + 15 * max(0, len(sub_lines) - 1)
193
+ t = ax.set_title(text, pad=pad)
194
+ t.set_color(mode.text)
195
+ t.set_fontsize(fontsize)
196
+ t.set_fontweight("bold")
197
+ _, y = t.get_position()
198
+ t.set_position((0.0, y))
199
+ t.set_horizontalalignment("left")
200
+ if subtitle:
201
+ ax.text(
202
+ 0,
203
+ 1.018,
204
+ "\n".join(sub_lines),
205
+ transform=ax.transAxes,
206
+ color=mode.text_dim,
207
+ fontsize=10.5,
208
+ ha="left",
209
+ va="bottom",
210
+ linespacing=1.25,
211
+ )
212
+
213
+
214
+ def footer(fig, mode: Mode, text: str, below_in: float | None = None) -> None:
215
+ # below the axes (and their x labels): the bbox-tight save grows to fit.
216
+ # Horizontally the two texts are pinned to the content's edges at save time
217
+ # (``fit_footer``): placed at the figure's edges they would stretch the tight
218
+ # bbox to the whole canvas and leave wide empty margins left and right.
219
+ drop = below_in / fig.get_figheight() if below_in else 0.13
220
+ y = min(ax.get_position().y0 for ax in fig.axes if ax.get_visible()) - drop if fig.axes else 0.0
221
+ left = fig.text(0.01, y, text, ha="left", va="top", color=mode.text_dim, fontsize=8.5)
222
+ right = fig.text(
223
+ 0.995,
224
+ y,
225
+ "MJOLNIR · Jetson AGX Thor · sm_110",
226
+ ha="right",
227
+ va="top",
228
+ color=mode.text_dim,
229
+ fontsize=8,
230
+ family="monospace",
231
+ )
232
+ fig._mjolnir_footer = (left, right)
233
+
234
+
235
+ def fit_footer(fig) -> None:
236
+ """Move the footer to the left / right edge of everything else on the
237
+ figure (call right before a bbox-tight save)."""
238
+ texts = getattr(fig, "_mjolnir_footer", None)
239
+ if not texts:
240
+ return
241
+ for t in texts:
242
+ t.set_in_layout(False)
243
+ renderer = fig.canvas.get_renderer()
244
+ bb = fig.get_tightbbox(renderer) # inches, footer excluded
245
+ w = fig.get_figwidth()
246
+ left, right = texts
247
+ left.set_x(bb.x0 / w)
248
+ right.set_x(bb.x1 / w)
249
+ # a footer wider than the content still widens the bbox, never clips
250
+ for t in texts:
251
+ t.set_in_layout(True)
252
+
253
+
254
+ def wrap(text: str, width: int = 24) -> str:
255
+ """Break a long series label into lines at its natural joints: `` · ``
256
+ (dropped at a break) and `` + `` (kept, leading the next line), so
257
+ "Mjolnir · fp6 e2m3 KV + fp8 lm_head" reads "Mjolnir · fp6 e2m3 KV\n+ fp8 lm_head"
258
+ instead of widening the chart."""
259
+ import re
260
+
261
+ parts = re.split(r"( · | \+ )", text)
262
+ lines, cur = [], parts[0]
263
+ for sep, word in zip(parts[1::2], parts[2::2]):
264
+ if len(cur) + len(sep) + len(word) <= width:
265
+ cur += sep + word
266
+ else:
267
+ lines.append(cur)
268
+ cur = ("+ " if "+" in sep else "") + word
269
+ lines.append(cur)
270
+ return "\n".join(lines)
271
+
272
+
273
+ def legend(ax, mode: Mode, **kw):
274
+ leg = ax.legend(
275
+ facecolor=mode.panel,
276
+ edgecolor=mode.grid,
277
+ labelcolor=mode.text,
278
+ framealpha=1.0,
279
+ **{"fontsize": 10, **kw},
280
+ )
281
+ return leg
mjolnir_cli/thor.py ADDED
@@ -0,0 +1,108 @@
1
+ """``mjolnir hw setup`` — Jetson AGX Thor host pre-configuration.
2
+
3
+ The ported Thor HW setup (``scripts/hw/setup-thor.sh``, from the ~/thor
4
+ scripts) brings a box to the serving baseline the rest of the CLI
5
+ assumes: headless boot target, apt upgrade, Docker with the NVIDIA
6
+ runtime as default, pip, jtop, a 32 GB swap file (zRAM off), the fan
7
+ profiles (``recommended`` + ``max`` installed, default selected — the
8
+ default profile is ``recommended``), trimmed host services, locked max
9
+ clocks, and the MAXN power mode. Every step is idempotent.
10
+
11
+ This module only locates and runs the script: flags map 1:1 to its
12
+ options, and ``--dry-run`` previews the plan without touching the box.
13
+ The script needs sudo — it asks on the first privileged call.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import subprocess
19
+ from pathlib import Path
20
+
21
+ from mjolnir_cli.config import resource_path
22
+
23
+ # The --skip names, in run order (mirror of the script's steps).
24
+ STEPS: tuple[str, ...] = (
25
+ "gui",
26
+ "upgrade",
27
+ "pip",
28
+ "docker",
29
+ "jtop",
30
+ "memory",
31
+ "fan",
32
+ "host",
33
+ "clocks",
34
+ "power",
35
+ )
36
+
37
+ FAN_PROFILES: tuple[str, ...] = ("recommended", "max", "cool", "quiet")
38
+ DEFAULT_FAN_PROFILE = "recommended"
39
+
40
+
41
+ def setup_script() -> Path:
42
+ """The ported setup script (the repo's ``scripts/hw/setup-thor.sh``)."""
43
+ p = resource_path("scripts/hw/setup-thor.sh")
44
+ if not p.is_file():
45
+ raise FileNotFoundError(f"setup script not found: {p}")
46
+ return p
47
+
48
+
49
+ def build_cmd(
50
+ yes: bool = False,
51
+ upgrade: bool = True,
52
+ keep_gui: bool = False,
53
+ fan_profile: str = DEFAULT_FAN_PROFILE,
54
+ swap_size: int = 32,
55
+ skip: str = "",
56
+ reboot: bool = False,
57
+ dry_run: bool = False,
58
+ ) -> list[str]:
59
+ """The ``bash scripts/hw/setup-thor.sh ...`` argv for the given flags."""
60
+ if fan_profile not in FAN_PROFILES:
61
+ raise ValueError(f"bad --fan-profile '{fan_profile}' (want one of: {', '.join(FAN_PROFILES)})")
62
+ names = [s.strip() for s in skip.split(",") if s.strip()]
63
+ bad = sorted(set(names) - set(STEPS))
64
+ if bad:
65
+ raise ValueError(f"unknown --skip step(s): {', '.join(bad)} (valid: {', '.join(STEPS)})")
66
+
67
+ cmd = ["bash", str(setup_script())]
68
+ if yes:
69
+ cmd.append("--yes")
70
+ if not upgrade:
71
+ cmd.append("--no-upgrade")
72
+ if keep_gui:
73
+ cmd.append("--keep-gui")
74
+ cmd += ["--fan-profile", fan_profile]
75
+ cmd += ["--swap-size", str(swap_size)]
76
+ if names:
77
+ cmd += ["--skip", ",".join(names)]
78
+ if reboot:
79
+ cmd.append("--reboot")
80
+ if dry_run:
81
+ cmd.append("--dry-run")
82
+ return cmd
83
+
84
+
85
+ def run_setup(
86
+ *,
87
+ yes: bool = False,
88
+ upgrade: bool = True,
89
+ keep_gui: bool = False,
90
+ fan_profile: str = DEFAULT_FAN_PROFILE,
91
+ swap_size: int = 32,
92
+ skip: str = "",
93
+ reboot: bool = False,
94
+ dry_run: bool = False,
95
+ ) -> int:
96
+ """Run the Thor setup script; return its exit code (0 = done)."""
97
+ return subprocess.call(
98
+ build_cmd(
99
+ yes=yes,
100
+ upgrade=upgrade,
101
+ keep_gui=keep_gui,
102
+ fan_profile=fan_profile,
103
+ swap_size=swap_size,
104
+ skip=skip,
105
+ reboot=reboot,
106
+ dry_run=dry_run,
107
+ )
108
+ )
@@ -0,0 +1,2 @@
1
+ """``mjolnir verify`` — kernel correctness, benchmarks and profiles saved as
2
+ structured raw data (``benchmarks/raw/verify/<stage>/<run_id>.json``)."""