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/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
|
+
)
|