flashruntime 0.3.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.
- flashml_workloads/__init__.py +7 -0
- flashml_workloads/fedavg_driver.py +569 -0
- flashml_workloads/fedavg_weights.py +223 -0
- flashml_workloads/fedavg_worker.py +166 -0
- flashml_workloads/kmeans_driver.py +134 -0
- flashml_workloads/kmeans_shard.py +69 -0
- flashml_workloads/sgd_trainer.py +127 -0
- flashml_workloads/sharded_kmeans.py +323 -0
- flashml_workloads/sklearn_trial.py +89 -0
- flashruntime/__init__.py +125 -0
- flashruntime/artifacts/__init__.py +25 -0
- flashruntime/artifacts/store.py +228 -0
- flashruntime/backends/__init__.py +26 -0
- flashruntime/backends/base.py +63 -0
- flashruntime/backends/kuberay.py +465 -0
- flashruntime/checkpoint/__init__.py +20 -0
- flashruntime/checkpoint/catalog.py +198 -0
- flashruntime/checkpoint/local.py +109 -0
- flashruntime/checkpoint/store.py +86 -0
- flashruntime/integrations/__init__.py +5 -0
- flashruntime/integrations/huggingface.py +59 -0
- flashruntime/integrations/pytorch.py +52 -0
- flashruntime/integrations/sklearn.py +42 -0
- flashruntime/launchers/__init__.py +130 -0
- flashruntime/launchers/local.py +126 -0
- flashruntime/leases/__init__.py +27 -0
- flashruntime/leases/manager.py +365 -0
- flashruntime/leases/sqlite_store.py +169 -0
- flashruntime/leases/store.py +103 -0
- flashruntime/monitor/__init__.py +7 -0
- flashruntime/monitor/sampler.py +232 -0
- flashruntime/planner/__init__.py +56 -0
- flashruntime/planner/candidates.py +597 -0
- flashruntime/planner/catalog.py +129 -0
- flashruntime/planner/comm.py +95 -0
- flashruntime/planner/explain.py +109 -0
- flashruntime/planner/memory.py +166 -0
- flashruntime/planner/resolve.py +120 -0
- flashruntime/planner/selector.py +169 -0
- flashruntime/planner/timecost.py +81 -0
- flashruntime/profiling/__init__.py +113 -0
- flashruntime/protocol/__init__.py +18 -0
- flashruntime/protocol/plan_v1alpha1.py +320 -0
- flashruntime/protocol/v1alpha1.py +465 -0
- flashruntime/providers/__init__.py +138 -0
- flashruntime/py.typed +0 -0
- flashruntime/recipes/__init__.py +135 -0
- flashruntime/recipes/command.py +166 -0
- flashruntime/recovery/__init__.py +21 -0
- flashruntime/recovery/policy.py +170 -0
- flashruntime/recovery/signals.py +135 -0
- flashruntime/recovery/taxonomy.py +91 -0
- flashruntime/scheduler/__init__.py +170 -0
- flashruntime/sdk.py +402 -0
- flashruntime/service/__init__.py +3 -0
- flashruntime/service/app.py +391 -0
- flashruntime/service/auth.py +180 -0
- flashruntime/service/checkpoints.py +90 -0
- flashruntime/service/cli.py +167 -0
- flashruntime/service/dashboard.py +193 -0
- flashruntime/service/ledger.py +101 -0
- flashruntime/service/modea.py +821 -0
- flashruntime/strategies/__init__.py +156 -0
- flashruntime/strategies/command.py +56 -0
- flashruntime/torch/__init__.py +274 -0
- flashruntime/viewer/__init__.py +20 -0
- flashruntime/viewer/_docs/benchmarks.html +771 -0
- flashruntime/viewer/_docs/concepts/architecture.html +302 -0
- flashruntime/viewer/_docs/get-started.html +263 -0
- flashruntime/viewer/_docs/guides/federated-averaging.html +363 -0
- flashruntime/viewer/_docs/guides/huggingface.html +223 -0
- flashruntime/viewer/_docs/guides/jobspec-and-isolation.html +271 -0
- flashruntime/viewer/_docs/guides/pytorch.html +313 -0
- flashruntime/viewer/_docs/guides/sklearn.html +232 -0
- flashruntime/viewer/_docs/index.html +251 -0
- flashruntime/viewer/_docs/reference/cli.html +254 -0
- flashruntime/viewer/_docs/reference/integrations.html +240 -0
- flashruntime/viewer/_docs/reference/sdk.html +341 -0
- flashruntime/viewer/_docs/reference/torch-helper.html +244 -0
- flashruntime/viewer/_docs/search-index.json +1 -0
- flashruntime/viewer/_docs/tutorials/convnet.html +571 -0
- flashruntime/viewer/_docs/tutorials/fault-tolerance.html +375 -0
- flashruntime/viewer/_docs/tutorials/sklearn-sweeps.html +278 -0
- flashruntime/viewer/flowmap.py +307 -0
- flashruntime/viewer/page.py +594 -0
- flashruntime/viewer/server.py +134 -0
- flashruntime/viewer/state.py +250 -0
- flashruntime/workloads/__init__.py +6 -0
- flashruntime/workloads/command.py +127 -0
- flashruntime-0.3.0.dist-info/METADATA +365 -0
- flashruntime-0.3.0.dist-info/RECORD +95 -0
- flashruntime-0.3.0.dist-info/WHEEL +5 -0
- flashruntime-0.3.0.dist-info/entry_points.txt +2 -0
- flashruntime-0.3.0.dist-info/licenses/LICENSE +202 -0
- flashruntime-0.3.0.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,594 @@
|
|
|
1
|
+
"""render() — the live run page, one self-contained HTML document.
|
|
2
|
+
|
|
3
|
+
This is the single sanctioned large file of the viewer: a `flash.submit(
|
|
4
|
+
watch=True)` opens it, and it polls `/api/state` (the `state.collect()`
|
|
5
|
+
snapshot) every couple of seconds to draw one run's story — its process map,
|
|
6
|
+
its loss curve, its checkpoints, and its recovery decisions — with ZERO
|
|
7
|
+
external assets. No CDN, no web font fetch, no remote image: the page must
|
|
8
|
+
render with the network cut, because the only server it may ever talk to is
|
|
9
|
+
the loopback `RunViewerServer` that served it. Everything (CSS, JS) is
|
|
10
|
+
inline; the only same-origin links are `/api/state` and `/docs/`.
|
|
11
|
+
|
|
12
|
+
DESIGN CONTRACT (the house style, shared with `service/dashboard.py`):
|
|
13
|
+
the palette lives in `TOKENS` at the top of this module as the SINGLE SOURCE
|
|
14
|
+
OF TRUTH — a dark `#0d1117` field, `#161b22` panels, `#21262d` borders,
|
|
15
|
+
`#c9d1d9`/`#8b949e` text, and five oklch accents (cyan running, green
|
|
16
|
+
succeeded+verified, amber leased+recovering, red failed, violet
|
|
17
|
+
checkpoints). `dashboard.py` imports these same values so the two surfaces
|
|
18
|
+
read as one product. Sections, top to bottom (layout A, spec 2026-07-23):
|
|
19
|
+
header → KPI strip → process flow map (machine → workers → ranks; click a
|
|
20
|
+
node for the slide-in detail panel) → loss + resource charts → checkpoint
|
|
21
|
+
timeline → events feed → collapsible logs. The flow-map/KPI component
|
|
22
|
+
itself lives in `viewer/flowmap.py` — shared with the coordinator
|
|
23
|
+
dashboard.
|
|
24
|
+
|
|
25
|
+
The drawing is plain, commented JS organized by section (`renderFlowmap`,
|
|
26
|
+
`drawLoss`, `drawResources`, `renderCheckpoints`, …) — no framework, no
|
|
27
|
+
build step, no minification. Same readability bar as the Python (spec §2b).
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
import json
|
|
33
|
+
|
|
34
|
+
from flashruntime.viewer.flowmap import FLOWMAP_CSS, FLOWMAP_JS
|
|
35
|
+
|
|
36
|
+
# --------------------------------------------------------------------------
|
|
37
|
+
# Visual tokens — the single source of truth for BOTH this page and the
|
|
38
|
+
# coordinator dashboard (`service/dashboard.py` imports these values). Accents
|
|
39
|
+
# are oklch so they stay perceptually even across hues; the neutrals are the
|
|
40
|
+
# established GitHub-dark family the house style already used.
|
|
41
|
+
# --------------------------------------------------------------------------
|
|
42
|
+
TOKENS: dict[str, str] = {
|
|
43
|
+
"bg": "#0d1117", # page field
|
|
44
|
+
"bg_inset": "#010409", # deepest wells (canvas backdrops, code)
|
|
45
|
+
"panel": "#161b22", # raised panels / hover
|
|
46
|
+
"border": "#21262d", # hairlines
|
|
47
|
+
"text": "#c9d1d9", # body text
|
|
48
|
+
"text_bright": "#e6edf3", # headings
|
|
49
|
+
"muted": "#8b949e", # secondary text / section labels
|
|
50
|
+
"running": "oklch(0.80 0.16 200)", # cyan — RUNNING
|
|
51
|
+
"ok": "oklch(0.76 0.18 145)", # green — SUCCEEDED / hash-verified
|
|
52
|
+
"warn": "oklch(0.80 0.18 60)", # amber — LEASED / RECOVERING / classified
|
|
53
|
+
"fail": "oklch(0.70 0.20 20)", # red — FAILED / invalid
|
|
54
|
+
"ckpt": "oklch(0.65 0.20 290)", # violet — checkpoints
|
|
55
|
+
"font": "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace",
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
# The subset of tokens the JS drawing code needs (canvas fillStyles). Injected
|
|
59
|
+
# as a JS object literal so the colors are never duplicated between CSS and JS.
|
|
60
|
+
_JS_TOKEN_KEYS = ("bg", "bg_inset", "panel", "border", "text", "muted", "running", "ok", "warn", "fail", "ckpt")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# The document itself. CSS uses `%%token%%` placeholders (replaced in render());
|
|
64
|
+
# the JS reads colors from the injected `T` object. Nothing here reaches
|
|
65
|
+
# off-host — every href is same-origin (`/docs/`), and there are no src= assets.
|
|
66
|
+
_TEMPLATE = r"""<!doctype html>
|
|
67
|
+
<html lang="en">
|
|
68
|
+
<head>
|
|
69
|
+
<meta charset="utf-8">
|
|
70
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
71
|
+
<title>flashruntime — live run</title>
|
|
72
|
+
<style>
|
|
73
|
+
:root { color-scheme: dark; }
|
|
74
|
+
* { box-sizing: border-box; margin: 0; }
|
|
75
|
+
body { font: 13px/1.5 %%font%%; background: %%bg%%; color: %%text%%; padding: 20px; }
|
|
76
|
+
a { color: %%running%%; text-decoration: none; }
|
|
77
|
+
a:hover { text-decoration: underline; }
|
|
78
|
+
|
|
79
|
+
/* header ------------------------------------------------------------- */
|
|
80
|
+
header { display: flex; align-items: baseline; gap: 16px; flex-wrap: wrap;
|
|
81
|
+
border-bottom: 1px solid %%border%%; padding-bottom: 14px; }
|
|
82
|
+
header .title { font-size: 15px; color: %%text_bright%%; }
|
|
83
|
+
header .cmd { color: %%muted%%; word-break: break-all; }
|
|
84
|
+
header .spacer { flex: 1 1 auto; }
|
|
85
|
+
.meta { display: flex; gap: 18px; align-items: center; margin-top: 4px;
|
|
86
|
+
flex-wrap: wrap; color: %%muted%%; }
|
|
87
|
+
.meta b { color: %%text%%; font-weight: 600; }
|
|
88
|
+
.badge { display: inline-block; padding: 2px 10px; border-radius: 999px;
|
|
89
|
+
border: 1px solid %%border%%; font-size: 12px; }
|
|
90
|
+
|
|
91
|
+
/* section scaffolding ------------------------------------------------ */
|
|
92
|
+
section { margin-top: 22px; }
|
|
93
|
+
h2 { font-size: 11px; text-transform: uppercase; letter-spacing: .1em;
|
|
94
|
+
color: %%muted%%; margin-bottom: 8px; font-weight: 600; }
|
|
95
|
+
.panel { background: %%panel%%; border: 1px solid %%border%%; border-radius: 8px;
|
|
96
|
+
padding: 12px; }
|
|
97
|
+
canvas { display: block; width: 100%; background: %%bg_inset%%; border-radius: 6px; }
|
|
98
|
+
#loss { height: 200px; }
|
|
99
|
+
#resources { height: 160px; }
|
|
100
|
+
.charts { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
|
|
101
|
+
@media (max-width: 900px) { .charts { grid-template-columns: 1fr; } }
|
|
102
|
+
%%FLOWMAP_CSS%%
|
|
103
|
+
|
|
104
|
+
/* checkpoint timeline ------------------------------------------------ */
|
|
105
|
+
#checkpoints { display: flex; gap: 10px; overflow-x: auto; padding: 6px 2px; }
|
|
106
|
+
#checkpoints .empty { color: %%muted%%; }
|
|
107
|
+
.ckpt { flex: 0 0 auto; border: 1px solid %%border%%; border-left: 3px solid %%ckpt%%;
|
|
108
|
+
border-radius: 6px; padding: 8px 10px; background: %%bg%%; min-width: 96px; }
|
|
109
|
+
.ckpt .step { color: %%ckpt%%; font-weight: 600; }
|
|
110
|
+
.ckpt .b { display: inline-block; margin-top: 4px; font-size: 11px;
|
|
111
|
+
padding: 1px 6px; border-radius: 4px; }
|
|
112
|
+
.ckpt .verified { color: %%ok%%; border: 1px solid %%ok%%; }
|
|
113
|
+
.ckpt .invalid { color: %%fail%%; border: 1px solid %%fail%%; }
|
|
114
|
+
.ckpt .latest { color: %%text_bright%%; }
|
|
115
|
+
.ckpt .sub { color: %%muted%%; font-size: 11px; margin-top: 2px; }
|
|
116
|
+
|
|
117
|
+
/* events feed -------------------------------------------------------- */
|
|
118
|
+
#events { max-height: 300px; overflow-y: auto; }
|
|
119
|
+
#events .empty { color: %%muted%%; }
|
|
120
|
+
.ev { display: flex; gap: 8px; padding: 3px 0; border-bottom: 1px solid %%border%%; }
|
|
121
|
+
.ev:last-child { border-bottom: 0; }
|
|
122
|
+
.ev .t { color: %%muted%%; white-space: nowrap; }
|
|
123
|
+
.ev .ty { white-space: nowrap; }
|
|
124
|
+
.ev .msg { color: %%text%%; word-break: break-word; }
|
|
125
|
+
.ty-classified { color: %%warn%%; }
|
|
126
|
+
.ty-recovery { color: %%running%%; }
|
|
127
|
+
.ty-other { color: %%muted%%; }
|
|
128
|
+
|
|
129
|
+
/* logs --------------------------------------------------------------- */
|
|
130
|
+
details summary { cursor: pointer; color: %%muted%%; }
|
|
131
|
+
pre#logbody { margin-top: 8px; padding: 10px; background: %%bg_inset%%;
|
|
132
|
+
border-radius: 6px; max-height: 320px; overflow: auto;
|
|
133
|
+
color: %%text%%; white-space: pre-wrap; word-break: break-word; }
|
|
134
|
+
|
|
135
|
+
#err { display: none; margin-top: 12px; padding: 10px; border-radius: 6px;
|
|
136
|
+
border: 1px solid %%fail%%; color: %%fail%%; }
|
|
137
|
+
</style>
|
|
138
|
+
</head>
|
|
139
|
+
<body>
|
|
140
|
+
|
|
141
|
+
<header>
|
|
142
|
+
<div>
|
|
143
|
+
<div class="title">flashruntime <span id="hstate" class="badge">…</span></div>
|
|
144
|
+
<div class="cmd" id="hcmd">connecting…</div>
|
|
145
|
+
<div class="meta">
|
|
146
|
+
<span>mode <b id="hmode">—</b></span>
|
|
147
|
+
<span>restarts <b id="hrestarts">—</b></span>
|
|
148
|
+
<span>attempts <b id="hattempts">—</b></span>
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
151
|
+
<div class="spacer"></div>
|
|
152
|
+
<div><a href="/docs/">Docs ↗</a></div>
|
|
153
|
+
</header>
|
|
154
|
+
|
|
155
|
+
<div id="err"></div>
|
|
156
|
+
|
|
157
|
+
<section>
|
|
158
|
+
<h2>Run</h2>
|
|
159
|
+
<div id="kpis"></div>
|
|
160
|
+
</section>
|
|
161
|
+
|
|
162
|
+
<section>
|
|
163
|
+
<h2>Process map</h2>
|
|
164
|
+
<div class="panel" style="position: relative;">
|
|
165
|
+
<div id="flowmap"></div>
|
|
166
|
+
<div id="detail" hidden></div>
|
|
167
|
+
</div>
|
|
168
|
+
</section>
|
|
169
|
+
|
|
170
|
+
<section class="charts">
|
|
171
|
+
<div>
|
|
172
|
+
<h2>Loss</h2>
|
|
173
|
+
<div class="panel"><canvas id="loss"></canvas></div>
|
|
174
|
+
</div>
|
|
175
|
+
<div>
|
|
176
|
+
<h2>Resources</h2>
|
|
177
|
+
<div class="panel"><canvas id="resources"></canvas></div>
|
|
178
|
+
</div>
|
|
179
|
+
</section>
|
|
180
|
+
|
|
181
|
+
<section>
|
|
182
|
+
<h2>Checkpoints</h2>
|
|
183
|
+
<div class="panel"><div id="checkpoints"><span class="empty">no checkpoints yet</span></div></div>
|
|
184
|
+
</section>
|
|
185
|
+
|
|
186
|
+
<section>
|
|
187
|
+
<h2>Events</h2>
|
|
188
|
+
<div class="panel"><div id="events"><span class="empty">no events yet</span></div></div>
|
|
189
|
+
</section>
|
|
190
|
+
|
|
191
|
+
<section>
|
|
192
|
+
<details>
|
|
193
|
+
<summary>Logs (tail)</summary>
|
|
194
|
+
<pre id="logbody">—</pre>
|
|
195
|
+
</details>
|
|
196
|
+
</section>
|
|
197
|
+
|
|
198
|
+
<script>
|
|
199
|
+
// The color tokens, shared with the CSS above and with the dashboard. Kept in
|
|
200
|
+
// one object so a canvas fillStyle and a CSS rule never drift apart.
|
|
201
|
+
const T = %%tokens_json%%;
|
|
202
|
+
const POLL_MS = 2000; // /api/state cadence — see poll()
|
|
203
|
+
let STATE = null; // latest snapshot; every draw reads from this
|
|
204
|
+
|
|
205
|
+
const $ = (id) => document.getElementById(id);
|
|
206
|
+
const esc = (s) => String(s == null ? "" : s).replace(/[&<>"]/g, (c) =>
|
|
207
|
+
({ "&": "&", "<": "<", ">": ">", '"': """ }[c]));
|
|
208
|
+
const clockTime = (ts) => (typeof ts === "number" ? new Date(ts * 1000).toLocaleTimeString() : "");
|
|
209
|
+
|
|
210
|
+
%%FLOWMAP_JS%%
|
|
211
|
+
|
|
212
|
+
// Size a canvas for the device pixel ratio so lines are crisp on retina and
|
|
213
|
+
// the drawing coordinate system stays in CSS pixels. Returns {ctx, w, h}.
|
|
214
|
+
function fitCanvas(canvas) {
|
|
215
|
+
const dpr = window.devicePixelRatio || 1;
|
|
216
|
+
const rect = canvas.getBoundingClientRect();
|
|
217
|
+
const w = Math.max(1, Math.floor(rect.width));
|
|
218
|
+
const h = Math.max(1, Math.floor(rect.height));
|
|
219
|
+
if (canvas.width !== w * dpr || canvas.height !== h * dpr) {
|
|
220
|
+
canvas.width = w * dpr;
|
|
221
|
+
canvas.height = h * dpr;
|
|
222
|
+
}
|
|
223
|
+
const ctx = canvas.getContext("2d");
|
|
224
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0); // 1 unit == 1 CSS pixel
|
|
225
|
+
return { ctx, w, h };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ---- header --------------------------------------------------------------
|
|
229
|
+
function renderHeader(s) {
|
|
230
|
+
const cmd = (s.workload && s.workload.command) || [];
|
|
231
|
+
$("hcmd").textContent = Array.isArray(cmd) ? cmd.join(" ") : String(cmd);
|
|
232
|
+
$("hmode").textContent = (s.workload && s.workload.mode) || "—";
|
|
233
|
+
const badge = $("hstate");
|
|
234
|
+
badge.textContent = s.state || "—";
|
|
235
|
+
badge.style.color = fmStateColor(s.state);
|
|
236
|
+
badge.style.borderColor = fmStateColor(s.state);
|
|
237
|
+
const attempts = s.attempts || [];
|
|
238
|
+
// "restarts used" = attempts whose id carries an -rN recovery suffix.
|
|
239
|
+
const used = attempts.filter((a) => /-r\d+$/.test(a.attempt_id || "")).length;
|
|
240
|
+
$("hrestarts").textContent = used + " / " + (s.max_restarts == null ? "?" : s.max_restarts);
|
|
241
|
+
$("hattempts").textContent = String(attempts.length);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function fontFamily() {
|
|
245
|
+
return "ui-monospace, SFMono-Regular, Menlo, monospace";
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// ---- loss canvas ---------------------------------------------------------
|
|
249
|
+
// Pick the loss series out of the metrics tail (prefer a "loss" key, else the
|
|
250
|
+
// first numeric field that isn't a step counter), merge across attempts, and
|
|
251
|
+
// draw an autoscaled curve with the last value labeled.
|
|
252
|
+
function lossKey(rec) {
|
|
253
|
+
if (typeof rec.loss === "number") return "loss";
|
|
254
|
+
for (const k of Object.keys(rec)) {
|
|
255
|
+
if (k === "step" || k === "epoch" || k === "index") continue;
|
|
256
|
+
if (typeof rec[k] === "number" && isFinite(rec[k])) return k;
|
|
257
|
+
}
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function collectLoss(s) {
|
|
262
|
+
const pts = [];
|
|
263
|
+
let key = "loss";
|
|
264
|
+
(s.attempts || []).forEach((a) => {
|
|
265
|
+
(a.metrics || []).forEach((m) => {
|
|
266
|
+
const k = lossKey(m);
|
|
267
|
+
if (k == null) return;
|
|
268
|
+
key = k;
|
|
269
|
+
const y = m[k];
|
|
270
|
+
if (typeof y !== "number" || !isFinite(y)) return;
|
|
271
|
+
const x = typeof m.step === "number" ? m.step : pts.length;
|
|
272
|
+
pts.push({ x, y });
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
pts.sort((p, q) => p.x - q.x);
|
|
276
|
+
return { pts, key };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function drawLoss() {
|
|
280
|
+
const canvas = $("loss");
|
|
281
|
+
const { ctx, w, h } = fitCanvas(canvas);
|
|
282
|
+
ctx.clearRect(0, 0, w, h);
|
|
283
|
+
if (!STATE || STATE.error) return;
|
|
284
|
+
|
|
285
|
+
const { pts, key } = collectLoss(STATE);
|
|
286
|
+
if (pts.length === 0) {
|
|
287
|
+
ctx.fillStyle = T.muted;
|
|
288
|
+
ctx.font = "11px " + fontFamily();
|
|
289
|
+
ctx.textAlign = "center";
|
|
290
|
+
ctx.fillText("no metrics yet", w / 2, h / 2);
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const padL = 8, padR = 64, padT = 14, padB = 18;
|
|
295
|
+
const plotW = w - padL - padR, plotH = h - padT - padB;
|
|
296
|
+
let minX = pts[0].x, maxX = pts[0].x, minY = pts[0].y, maxY = pts[0].y;
|
|
297
|
+
for (const p of pts) {
|
|
298
|
+
minX = Math.min(minX, p.x); maxX = Math.max(maxX, p.x);
|
|
299
|
+
minY = Math.min(minY, p.y); maxY = Math.max(maxY, p.y);
|
|
300
|
+
}
|
|
301
|
+
// pad the y-bounds so the curve never touches the frame; guard a flat series
|
|
302
|
+
const span = maxY - minY || Math.abs(maxY) || 1;
|
|
303
|
+
minY -= span * 0.08; maxY += span * 0.08;
|
|
304
|
+
const xRange = maxX - minX || 1;
|
|
305
|
+
const yRange = maxY - minY || 1;
|
|
306
|
+
const sx = (x) => padL + ((x - minX) / xRange) * plotW;
|
|
307
|
+
const sy = (y) => padT + (1 - (y - minY) / yRange) * plotH;
|
|
308
|
+
|
|
309
|
+
// baseline
|
|
310
|
+
ctx.strokeStyle = T.border;
|
|
311
|
+
ctx.lineWidth = 1;
|
|
312
|
+
ctx.beginPath();
|
|
313
|
+
ctx.moveTo(padL, padT + plotH);
|
|
314
|
+
ctx.lineTo(padL + plotW, padT + plotH);
|
|
315
|
+
ctx.stroke();
|
|
316
|
+
|
|
317
|
+
// the curve
|
|
318
|
+
ctx.strokeStyle = T.running;
|
|
319
|
+
ctx.lineWidth = 1.5;
|
|
320
|
+
ctx.beginPath();
|
|
321
|
+
pts.forEach((p, i) => (i ? ctx.lineTo(sx(p.x), sy(p.y)) : ctx.moveTo(sx(p.x), sy(p.y))));
|
|
322
|
+
ctx.stroke();
|
|
323
|
+
|
|
324
|
+
// last value marker + label
|
|
325
|
+
const last = pts[pts.length - 1];
|
|
326
|
+
ctx.fillStyle = T.running;
|
|
327
|
+
ctx.beginPath();
|
|
328
|
+
ctx.arc(sx(last.x), sy(last.y), 3, 0, 2 * Math.PI);
|
|
329
|
+
ctx.fill();
|
|
330
|
+
ctx.fillStyle = T.text;
|
|
331
|
+
ctx.font = "11px " + fontFamily();
|
|
332
|
+
ctx.textAlign = "left";
|
|
333
|
+
const label = key + " " + (Math.abs(last.y) < 1e-3 ? last.y.toExponential(2) : last.y.toFixed(4));
|
|
334
|
+
ctx.fillText(label, Math.min(sx(last.x) + 6, w - padR), sy(last.y) - 4);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// ---- checkpoint timeline -------------------------------------------------
|
|
338
|
+
// Each manifest is a violet marker: its step, a hash-verified / invalid badge
|
|
339
|
+
// (the RE-VERIFIED state from state.collect, not the manifest's own claim),
|
|
340
|
+
// and a note when it is the one recovery would restore (latest_valid).
|
|
341
|
+
function renderCheckpoints(s) {
|
|
342
|
+
const el = $("checkpoints");
|
|
343
|
+
const cks = s.checkpoints || [];
|
|
344
|
+
if (cks.length === 0) {
|
|
345
|
+
el.innerHTML = '<span class="empty">no checkpoints yet</span>';
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
el.innerHTML = cks
|
|
349
|
+
.map((c) => {
|
|
350
|
+
const verified = c.validation === "hash_verified";
|
|
351
|
+
const badge = verified
|
|
352
|
+
? '<span class="b verified">hash-verified</span>'
|
|
353
|
+
: '<span class="b invalid">invalid</span>';
|
|
354
|
+
const latest = c.latest_valid ? ' <span class="latest">★ latest</span>' : "";
|
|
355
|
+
const age = typeof c.age_s === "number" ? Math.round(c.age_s) + "s ago" : "";
|
|
356
|
+
return (
|
|
357
|
+
'<div class="ckpt"><div class="step">step ' + esc(c.step) + "</div>" +
|
|
358
|
+
badge + latest +
|
|
359
|
+
'<div class="sub">' + esc(c.parts) + " parts · " + esc(age) + "</div></div>"
|
|
360
|
+
);
|
|
361
|
+
})
|
|
362
|
+
.join("");
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// ---- events feed ---------------------------------------------------------
|
|
366
|
+
// Newest first. Recovery decisions are highlighted per the contract:
|
|
367
|
+
// FAILURE_CLASSIFIED amber, RECOVERY_ACTION_SELECTED cyan — the message
|
|
368
|
+
// already carries the failure class and the policy's human reason.
|
|
369
|
+
function eventClass(type) {
|
|
370
|
+
if (type === "FAILURE_CLASSIFIED") return "ty-classified";
|
|
371
|
+
if (type === "RECOVERY_ACTION_SELECTED") return "ty-recovery";
|
|
372
|
+
return "ty-other";
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function renderEvents(s) {
|
|
376
|
+
const el = $("events");
|
|
377
|
+
const evs = s.events || [];
|
|
378
|
+
if (evs.length === 0) {
|
|
379
|
+
el.innerHTML = '<span class="empty">no events yet</span>';
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
el.innerHTML = evs
|
|
383
|
+
.slice()
|
|
384
|
+
.reverse()
|
|
385
|
+
.map((e) =>
|
|
386
|
+
'<div class="ev"><span class="t">' + esc(clockTime(e.ts)) + "</span>" +
|
|
387
|
+
'<span class="ty ' + eventClass(e.type) + '">' + esc(e.type) + "</span>" +
|
|
388
|
+
'<span class="msg">' + esc(e.message) + "</span></div>"
|
|
389
|
+
)
|
|
390
|
+
.join("");
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// ---- logs tail -----------------------------------------------------------
|
|
394
|
+
function renderLogs(s) {
|
|
395
|
+
const attempts = s.attempts || [];
|
|
396
|
+
const chunks = attempts
|
|
397
|
+
.filter((a) => a.log_tail)
|
|
398
|
+
.map((a) => "--- " + (a.attempt_id || "") + " (" + (a.state || "") + ") ---\n" + a.log_tail);
|
|
399
|
+
$("logbody").textContent = chunks.length ? chunks.join("\n\n") : "—";
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// ---- KPI tiles + steps/s derivation ---------------------------------------
|
|
403
|
+
// steps/s comes from consecutive rank heartbeats (ts + step) observed across
|
|
404
|
+
// polls — the only honest rate source; without heartbeats the tile shows "—".
|
|
405
|
+
let prevBeat = null; // {ts, step} from the previous poll
|
|
406
|
+
let stepsPerSec = null; // last derived rate
|
|
407
|
+
|
|
408
|
+
function fmtDur(sec) {
|
|
409
|
+
if (typeof sec !== "number" || !isFinite(sec) || sec < 0) return "—";
|
|
410
|
+
const m = Math.floor(sec / 60), r = Math.floor(sec % 60), h = Math.floor(m / 60);
|
|
411
|
+
return h ? h + "h " + (m % 60) + "m" : m + ":" + String(r).padStart(2, "0");
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function newestBeat(s) {
|
|
415
|
+
let best = null;
|
|
416
|
+
(s.attempts || []).forEach((a) => (a.ranks || []).forEach((r) => {
|
|
417
|
+
if (typeof r.step === "number" && typeof r.ts === "number" &&
|
|
418
|
+
(best == null || r.ts > best.ts)) best = r;
|
|
419
|
+
}));
|
|
420
|
+
return best;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function updateRate(s) {
|
|
424
|
+
const beat = newestBeat(s);
|
|
425
|
+
if (!beat) return;
|
|
426
|
+
if (prevBeat && beat.ts > prevBeat.ts && beat.step >= prevBeat.step) {
|
|
427
|
+
const rate = (beat.step - prevBeat.step) / (beat.ts - prevBeat.ts);
|
|
428
|
+
if (isFinite(rate) && rate > 0) stepsPerSec = rate;
|
|
429
|
+
}
|
|
430
|
+
prevBeat = { ts: beat.ts, step: beat.step };
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function buildKpiTiles(s) {
|
|
434
|
+
const m = (s.monitor && s.monitor.machine) || null;
|
|
435
|
+
const beat = newestBeat(s);
|
|
436
|
+
const attempts = s.attempts || [];
|
|
437
|
+
const used = attempts.filter((a) => /-r\d+$/.test(a.attempt_id || "")).length;
|
|
438
|
+
const verified = (s.checkpoints || []).filter((c) => c.validation === "hash_verified").length;
|
|
439
|
+
const end = s.finished_at || Date.now() / 1000;
|
|
440
|
+
const hint = m && m.limited ? "flashruntime[monitor]" : undefined;
|
|
441
|
+
// A derived rate is only honest while it is still current: once the run
|
|
442
|
+
// stalls or finishes, the last-computed stepsPerSec is a fabricated
|
|
443
|
+
// "current" number if we keep showing it. "Live" = RUNNING and the
|
|
444
|
+
// newest heartbeat is recent (~3 poll periods, POLL_MS=2000).
|
|
445
|
+
const rateLive = s.state === "RUNNING" && beat && (Date.now() / 1000 - beat.ts) < 6;
|
|
446
|
+
const tiles = [
|
|
447
|
+
{ label: "state", value: s.state || "—", color: fmStateColor(s.state) },
|
|
448
|
+
{ label: "elapsed", value: fmtDur(end - s.started_at) },
|
|
449
|
+
{ label: "step", value: beat && beat.step != null ? String(beat.step) : "—" },
|
|
450
|
+
{ label: "steps/s", value: rateLive && stepsPerSec != null ? stepsPerSec.toFixed(2) : "—" },
|
|
451
|
+
{ label: "step latency", value: rateLive && stepsPerSec ? Math.round(1000 / stepsPerSec) + " ms" : "—" },
|
|
452
|
+
{ label: "cpu", value: m && m.cpu_percent != null ? Math.round(m.cpu_percent) + "%" : "—", hint: hint },
|
|
453
|
+
{ label: "memory", value: m && m.mem_used != null ? fmFmtBytes(m.mem_used) : "—", hint: hint },
|
|
454
|
+
{ label: "restarts", value: used + " / " + (s.max_restarts == null ? "?" : s.max_restarts) },
|
|
455
|
+
{ label: "ckpts ✓", value: String(verified) },
|
|
456
|
+
];
|
|
457
|
+
if (m && m.gpus && m.gpus.length) {
|
|
458
|
+
tiles.splice(7, 0, { label: "gpu", value: Math.round(m.gpus[0].util_percent) + "%" });
|
|
459
|
+
}
|
|
460
|
+
return tiles;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// ---- resource chart ---------------------------------------------------------
|
|
464
|
+
// cpu% (cyan) + memory% (violet) over the telemetry tail, 0–100 fixed axis.
|
|
465
|
+
function collectResources(s) {
|
|
466
|
+
const samples = [];
|
|
467
|
+
(s.attempts || []).forEach((a) => (a.telemetry || []).forEach((t) => {
|
|
468
|
+
if (t && t.machine && typeof t.ts === "number") samples.push(t);
|
|
469
|
+
}));
|
|
470
|
+
samples.sort((p, q) => p.ts - q.ts);
|
|
471
|
+
return samples.slice(-150);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function drawResources() {
|
|
475
|
+
const canvas = $("resources");
|
|
476
|
+
const { ctx, w, h } = fitCanvas(canvas);
|
|
477
|
+
ctx.clearRect(0, 0, w, h);
|
|
478
|
+
if (!STATE || STATE.error) return;
|
|
479
|
+
const samples = collectResources(STATE);
|
|
480
|
+
const usable = samples.filter((t) => t.machine.cpu_percent != null);
|
|
481
|
+
if (usable.length < 2) {
|
|
482
|
+
ctx.fillStyle = T.muted;
|
|
483
|
+
ctx.font = "11px " + fontFamily();
|
|
484
|
+
ctx.textAlign = "center";
|
|
485
|
+
const msg = samples.length
|
|
486
|
+
? 'cpu/mem need psutil — pip install "flashruntime[monitor]"'
|
|
487
|
+
: "no telemetry yet";
|
|
488
|
+
ctx.fillText(msg, w / 2, h / 2);
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
const padL = 8, padR = 56, padT = 10, padB = 14;
|
|
492
|
+
const plotW = w - padL - padR, plotH = h - padT - padB;
|
|
493
|
+
const t0 = usable[0].ts, t1 = usable[usable.length - 1].ts;
|
|
494
|
+
const sx = (ts) => padL + ((ts - t0) / (t1 - t0 || 1)) * plotW;
|
|
495
|
+
const sy = (pct) => padT + (1 - Math.min(100, Math.max(0, pct)) / 100) * plotH;
|
|
496
|
+
ctx.strokeStyle = T.border;
|
|
497
|
+
ctx.lineWidth = 1;
|
|
498
|
+
ctx.beginPath();
|
|
499
|
+
ctx.moveTo(padL, padT + plotH);
|
|
500
|
+
ctx.lineTo(padL + plotW, padT + plotH);
|
|
501
|
+
ctx.stroke();
|
|
502
|
+
const series = (pick, color) => {
|
|
503
|
+
ctx.strokeStyle = color;
|
|
504
|
+
ctx.lineWidth = 1.5;
|
|
505
|
+
ctx.beginPath();
|
|
506
|
+
let started = false;
|
|
507
|
+
usable.forEach((t) => {
|
|
508
|
+
const v = pick(t);
|
|
509
|
+
if (typeof v !== "number") return;
|
|
510
|
+
started ? ctx.lineTo(sx(t.ts), sy(v)) : ctx.moveTo(sx(t.ts), sy(v));
|
|
511
|
+
started = true;
|
|
512
|
+
});
|
|
513
|
+
ctx.stroke();
|
|
514
|
+
};
|
|
515
|
+
const memPct = (t) => t.machine.mem_total
|
|
516
|
+
? 100 * t.machine.mem_used / t.machine.mem_total : null;
|
|
517
|
+
series((t) => t.machine.cpu_percent, T.running);
|
|
518
|
+
series(memPct, T.ckpt);
|
|
519
|
+
const last = usable[usable.length - 1];
|
|
520
|
+
ctx.font = "11px " + fontFamily();
|
|
521
|
+
ctx.textAlign = "left";
|
|
522
|
+
ctx.fillStyle = T.running;
|
|
523
|
+
ctx.fillText("cpu " + Math.round(last.machine.cpu_percent) + "%",
|
|
524
|
+
padL + plotW + 4, sy(last.machine.cpu_percent));
|
|
525
|
+
const mp = memPct(last);
|
|
526
|
+
if (mp != null) {
|
|
527
|
+
ctx.fillStyle = T.ckpt;
|
|
528
|
+
ctx.fillText("mem " + Math.round(mp) + "%", padL + plotW + 4, sy(mp));
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// ---- everything that redraws on a poll ------------------------------------
|
|
533
|
+
function renderStatic() {
|
|
534
|
+
const s = STATE;
|
|
535
|
+
const err = $("err");
|
|
536
|
+
if (!s) return;
|
|
537
|
+
if (s.error) {
|
|
538
|
+
err.style.display = "block";
|
|
539
|
+
err.textContent = "snapshot error: " + s.error;
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
err.style.display = "none";
|
|
543
|
+
renderHeader(s);
|
|
544
|
+
updateRate(s);
|
|
545
|
+
renderKpiTiles(buildKpiTiles(s));
|
|
546
|
+
renderFlowmap(s);
|
|
547
|
+
renderDetail(s);
|
|
548
|
+
drawLoss();
|
|
549
|
+
drawResources();
|
|
550
|
+
renderCheckpoints(s);
|
|
551
|
+
renderEvents(s);
|
|
552
|
+
renderLogs(s);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// ---- polling -------------------------------------------------------------
|
|
556
|
+
async function poll() {
|
|
557
|
+
// Pause polling when the tab is hidden: this page can sit open for a whole
|
|
558
|
+
// training run, and there is no reason to spend a laptop's battery fetching
|
|
559
|
+
// a snapshot nobody is looking at. A visibilitychange handler resumes it.
|
|
560
|
+
if (document.hidden) return;
|
|
561
|
+
try {
|
|
562
|
+
const r = await fetch("/api/state", { cache: "no-store" });
|
|
563
|
+
STATE = await r.json();
|
|
564
|
+
} catch (e) {
|
|
565
|
+
STATE = { error: String(e) };
|
|
566
|
+
}
|
|
567
|
+
renderStatic();
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
window.addEventListener("resize", () => renderStatic());
|
|
571
|
+
document.addEventListener("visibilitychange", () => { if (!document.hidden) poll(); });
|
|
572
|
+
|
|
573
|
+
poll();
|
|
574
|
+
setInterval(poll, POLL_MS);
|
|
575
|
+
</script>
|
|
576
|
+
</body>
|
|
577
|
+
</html>
|
|
578
|
+
"""
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
def render() -> str:
|
|
582
|
+
"""Return the live run page as one self-contained HTML string.
|
|
583
|
+
|
|
584
|
+
The flow-map component (viewer/flowmap.py) is spliced in first, THEN the
|
|
585
|
+
`%%token%%` placeholders are substituted from `TOKENS` — so the component's
|
|
586
|
+
CSS resolves against the same single-source-of-truth palette. No other
|
|
587
|
+
templating — the page is otherwise a static document.
|
|
588
|
+
"""
|
|
589
|
+
html = _TEMPLATE.replace("%%FLOWMAP_CSS%%", FLOWMAP_CSS).replace("%%FLOWMAP_JS%%", FLOWMAP_JS)
|
|
590
|
+
subs = {f"%%{name}%%": value for name, value in TOKENS.items()}
|
|
591
|
+
subs["%%tokens_json%%"] = json.dumps({k: TOKENS[k] for k in _JS_TOKEN_KEYS})
|
|
592
|
+
for placeholder, value in subs.items():
|
|
593
|
+
html = html.replace(placeholder, value)
|
|
594
|
+
return html
|