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,363 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>Federated averaging — flashruntime docs</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root { color-scheme: dark; }
|
|
9
|
+
* { box-sizing: border-box; margin: 0; }
|
|
10
|
+
body { font: 14px/1.65 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; background: #0d1117; color: #c9d1d9; }
|
|
11
|
+
a { color: oklch(0.80 0.16 200); text-decoration: none; }
|
|
12
|
+
a:hover { text-decoration: underline; }
|
|
13
|
+
.layout { display: flex; align-items: flex-start; min-height: 100vh; }
|
|
14
|
+
|
|
15
|
+
/* sidebar — PyTorch-docs-like: sections from _nav.yml, current page active */
|
|
16
|
+
.sidebar { flex: 0 0 264px; position: sticky; top: 0; height: 100vh; overflow-y: auto;
|
|
17
|
+
border-right: 1px solid #21262d; background: #0d1117; padding: 20px 16px; }
|
|
18
|
+
.brand { color: #e6edf3; font-size: 15px; letter-spacing: .04em; }
|
|
19
|
+
.brand small { display: block; margin-top: 2px; color: #8b949e; font-size: 10px;
|
|
20
|
+
text-transform: uppercase; letter-spacing: .14em; }
|
|
21
|
+
.search { position: relative; margin: 16px 0; }
|
|
22
|
+
#q { width: 100%; background: #010409; border: 1px solid #21262d; border-radius: 6px;
|
|
23
|
+
color: #c9d1d9; font: 12px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; padding: 7px 9px; }
|
|
24
|
+
#q:focus { outline: 1px solid oklch(0.80 0.16 200); }
|
|
25
|
+
#results { position: absolute; left: 0; right: 0; top: 112%; z-index: 5; display: none;
|
|
26
|
+
background: #161b22; border: 1px solid #21262d; border-radius: 6px; overflow: hidden; }
|
|
27
|
+
#results.open { display: block; }
|
|
28
|
+
#results a { display: block; padding: 7px 9px; border-bottom: 1px solid #21262d; color: #c9d1d9; }
|
|
29
|
+
#results a:last-child { border-bottom: 0; }
|
|
30
|
+
#results a:hover { background: #010409; text-decoration: none; }
|
|
31
|
+
#results a b { color: #e6edf3; font-weight: 600; }
|
|
32
|
+
#results a span { display: block; margin-top: 2px; color: #8b949e; font-size: 11px; }
|
|
33
|
+
#results .nohit { padding: 7px 9px; color: #8b949e; }
|
|
34
|
+
.nav-section { margin: 16px 0 6px; color: #8b949e; font-size: 10px; font-weight: 600;
|
|
35
|
+
text-transform: uppercase; letter-spacing: .14em; }
|
|
36
|
+
.nav-link { display: block; padding: 4px 8px; border-radius: 5px; color: #c9d1d9; font-size: 13px; }
|
|
37
|
+
.nav-link:hover { background: #161b22; text-decoration: none; }
|
|
38
|
+
.nav-link.active { background: #161b22; color: #e6edf3; box-shadow: inset 2px 0 0 oklch(0.80 0.16 200); }
|
|
39
|
+
|
|
40
|
+
/* content */
|
|
41
|
+
.content { flex: 1 1 auto; max-width: 840px; min-width: 0; padding: 34px 40px 80px; }
|
|
42
|
+
.content h1 { color: #e6edf3; font-size: 26px; margin: 0 0 16px; }
|
|
43
|
+
.content h2 { color: #e6edf3; font-size: 18px; margin: 30px 0 10px;
|
|
44
|
+
padding-top: 10px; border-top: 1px solid #21262d; }
|
|
45
|
+
.content h3 { color: #e6edf3; font-size: 15px; margin: 22px 0 8px; }
|
|
46
|
+
.content p, .content li { color: #c9d1d9; }
|
|
47
|
+
.content ul, .content ol { padding-left: 22px; margin: 10px 0; }
|
|
48
|
+
.content li { margin: 4px 0; }
|
|
49
|
+
.content blockquote { margin: 14px 0; padding: 2px 14px; border-left: 3px solid oklch(0.80 0.18 60);
|
|
50
|
+
background: #161b22; border-radius: 0 6px 6px 0; color: #8b949e; }
|
|
51
|
+
.content :not(pre) > code { background: #010409; border: 1px solid #21262d;
|
|
52
|
+
border-radius: 4px; padding: 1px 5px; font-size: 12.5px; }
|
|
53
|
+
.content table { border-collapse: collapse; margin: 14px 0; display: block; overflow-x: auto; }
|
|
54
|
+
.content th, .content td { border: 1px solid #21262d; padding: 6px 10px; text-align: left; }
|
|
55
|
+
.content th { color: #e6edf3; background: #161b22; }
|
|
56
|
+
|
|
57
|
+
/* code blocks + copy button */
|
|
58
|
+
.codeblock { position: relative; margin: 14px 0; }
|
|
59
|
+
.codeblock pre { background: #010409; border: 1px solid #21262d; border-radius: 8px;
|
|
60
|
+
padding: 14px 16px; overflow-x: auto; }
|
|
61
|
+
.codeblock pre code { padding: 0; border: 0; background: none; font-size: 12.5px; color: #c9d1d9; }
|
|
62
|
+
.copybtn { position: absolute; top: 8px; right: 8px; padding: 3px 8px; cursor: pointer;
|
|
63
|
+
background: #161b22; border: 1px solid #21262d; border-radius: 5px;
|
|
64
|
+
color: #8b949e; font: 11px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
|
|
65
|
+
.copybtn:hover { color: #e6edf3; border-color: oklch(0.80 0.16 200); }
|
|
66
|
+
|
|
67
|
+
@media (max-width: 800px) {
|
|
68
|
+
.layout { flex-direction: column; }
|
|
69
|
+
.sidebar { position: static; height: auto; width: 100%; flex-basis: auto;
|
|
70
|
+
border-right: 0; border-bottom: 1px solid #21262d; }
|
|
71
|
+
.content { padding: 24px 18px 60px; }
|
|
72
|
+
}
|
|
73
|
+
</style>
|
|
74
|
+
</head>
|
|
75
|
+
<body>
|
|
76
|
+
<div class="layout">
|
|
77
|
+
<aside class="sidebar">
|
|
78
|
+
<a class="brand" href="../index.html">flashruntime<small>documentation</small></a>
|
|
79
|
+
<div class="search">
|
|
80
|
+
<input id="q" type="search" placeholder="Search docs (press /)" autocomplete="off" spellcheck="false">
|
|
81
|
+
<div id="results"></div>
|
|
82
|
+
</div>
|
|
83
|
+
<nav><div class="nav-section">Getting started</div>
|
|
84
|
+
<a class="nav-link" href="../index.html">FlashRuntime</a>
|
|
85
|
+
<a class="nav-link" href="../get-started.html">Get started</a>
|
|
86
|
+
<div class="nav-section">Tutorials</div>
|
|
87
|
+
<a class="nav-link" href="../tutorials/convnet.html">Tutorial: make a ConvNet fault-tolerant</a>
|
|
88
|
+
<a class="nav-link" href="../tutorials/sklearn-sweeps.html">Tutorial: parallel scikit-learn sweeps</a>
|
|
89
|
+
<a class="nav-link" href="../tutorials/fault-tolerance.html">Tutorial: automatic recovery, explained</a>
|
|
90
|
+
<div class="nav-section">Guides</div>
|
|
91
|
+
<a class="nav-link" href="pytorch.html">PyTorch guide</a>
|
|
92
|
+
<a class="nav-link" href="sklearn.html">scikit-learn guide</a>
|
|
93
|
+
<a class="nav-link" href="huggingface.html">Hugging Face guide</a>
|
|
94
|
+
<a class="nav-link" href="jobspec-and-isolation.html">JobSpec & isolation guide</a>
|
|
95
|
+
<a class="nav-link active" href="federated-averaging.html">Federated averaging</a>
|
|
96
|
+
<div class="nav-section">Concepts</div>
|
|
97
|
+
<a class="nav-link" href="../concepts/architecture.html">Architecture</a>
|
|
98
|
+
<div class="nav-section">Reference</div>
|
|
99
|
+
<a class="nav-link" href="../reference/sdk.html">Reference: SDK (`flashruntime`)</a>
|
|
100
|
+
<a class="nav-link" href="../reference/integrations.html">Reference: integrations (`flashruntime.integrations`)</a>
|
|
101
|
+
<a class="nav-link" href="../reference/torch-helper.html">Reference: torch helper (`flashruntime.torch`)</a>
|
|
102
|
+
<a class="nav-link" href="../reference/cli.html">Reference: CLI (`flashruntime`)</a>
|
|
103
|
+
<div class="nav-section">Benchmarks</div>
|
|
104
|
+
<a class="nav-link" href="../benchmarks.html">Benchmarks</a></nav>
|
|
105
|
+
</aside>
|
|
106
|
+
<main class="content"><h1 id="federated-averaging">Federated averaging</h1>
|
|
107
|
+
<p>Federated averaging (FedAvg) is FlashRuntime's answer to a question the
|
|
108
|
+
<a href="pytorch.html">PyTorch guide</a> and the
|
|
109
|
+
<a href="jobspec-and-isolation.html">JobSpec & isolation guide</a> both run into: what do
|
|
110
|
+
you do when the machines that want to help train a model <strong>cannot talk to
|
|
111
|
+
each other</strong>? Volunteer nodes run their task containers with <code>--network
|
|
112
|
+
none</code> (see the repo's <code>docs/guides/donate-a-machine.md</code>) — no LAN, no
|
|
113
|
+
internet, no way for one container to find another. Coordinated
|
|
114
|
+
multi-process training (DDP, FSDP) needs the opposite: every rank must
|
|
115
|
+
rendezvous with every other rank over a process group before the first
|
|
116
|
+
<code>all_reduce</code>. On a volunteer pool those two requirements are irreconcilable,
|
|
117
|
+
so FlashRuntime does not attempt coordinated training there at all.</p>
|
|
118
|
+
<p>FedAvg sidesteps the rendezvous problem by never requiring it. Instead of
|
|
119
|
+
ranks synchronizing gradients mid-step, <strong>rounds</strong> synchronize whole models
|
|
120
|
+
between steps:</p>
|
|
121
|
+
<ol>
|
|
122
|
+
<li>The driver broadcasts the current weights as a plain <code>artifact://</code> blob.</li>
|
|
123
|
+
<li>Each participating node downloads the weights, trains <strong>independently</strong>
|
|
124
|
+
for a fixed number of local steps on its own data shard, and uploads a
|
|
125
|
+
weight <strong>delta</strong> (not the new weights — see below).</li>
|
|
126
|
+
<li>Once enough deltas have committed, the driver averages them, applies the
|
|
127
|
+
result to the broadcast weights, and starts the next round.</li>
|
|
128
|
+
</ol>
|
|
129
|
+
<p>No node ever needs to see another node's IP address, let alone open a
|
|
130
|
+
connection to it. Every cross-node interaction is a <code>PUT</code>/<code>GET</code> against the
|
|
131
|
+
coordinator's artifact store, which is exactly the same shape of traffic a
|
|
132
|
+
volunteer node already does to pull its task inputs and push its results.
|
|
133
|
+
That is why this is a <strong>round loop implemented as a driver chaining
|
|
134
|
+
ordinary lease jobs</strong> (<code>flashml_workloads/fedavg_driver.py</code>,
|
|
135
|
+
<code>flashml_workloads/fedavg_worker.py</code>) — the same "pipelines are jobs chained
|
|
136
|
+
by a driver, not a new execution mode" pattern as the sharded-k-means POC —
|
|
137
|
+
rather than a new backend.</p>
|
|
138
|
+
<h2 id="why-a-delta-not-the-new-weights">Why a delta, not the new weights</h2>
|
|
139
|
+
<p>Each worker's task uploads <code>delta.json</code> (the change it made to the weights
|
|
140
|
+
it started from) alongside <code>metrics.json</code>. The driver averages <strong>deltas</strong>,
|
|
141
|
+
not raw weight snapshots, because a delta is a direction that stays
|
|
142
|
+
meaningful even if the weights it was computed against are no longer the
|
|
143
|
+
newest ones — the exact situation a straggling volunteer produces when it
|
|
144
|
+
finally reports in after the round has moved on. Averaging final weights
|
|
145
|
+
directly would require every worker to have started from the <em>same</em>
|
|
146
|
+
snapshot; averaging deltas only requires knowing what each worker started
|
|
147
|
+
from, which the driver already does.</p>
|
|
148
|
+
<h2 id="the-quorum-rule-and-why-late-deltas-are-discarded">The quorum rule, and why late deltas are discarded</h2>
|
|
149
|
+
<p><code>kmeans_driver</code> (the other job-chaining driver in this codebase) requires
|
|
150
|
+
<strong>every</strong> dispatched shard to report before it aggregates. FedAvg
|
|
151
|
+
deliberately does not: <code>run_fedavg(..., min_participants=N)</code> aggregates as
|
|
152
|
+
soon as <code>N</code> of the round's shards have committed, not when all of them have.</p>
|
|
153
|
+
<p>This is not a shortcut — it is the correct policy for volunteer compute.
|
|
154
|
+
Machines that donate spare cycles are unequal and unreliable by
|
|
155
|
+
construction: laptops close, Wi-Fi drops, a slow machine might still be on
|
|
156
|
+
local step 3 when a fast one has already finished. Requiring all of them
|
|
157
|
+
before a round can proceed would let a single closed laptop stall every
|
|
158
|
+
other participant's contribution indefinitely. Quorum aggregation lets the
|
|
159
|
+
round move on as soon as it has a statistically meaningful sample.</p>
|
|
160
|
+
<p>The corollary is what makes quorum aggregation <em>safe</em> rather than merely
|
|
161
|
+
convenient: once the driver has read the quorum's deltas and applied them,
|
|
162
|
+
<strong>any delta that commits afterward for that round is discarded</strong>, never
|
|
163
|
+
folded into a later round. <code>run_fedavg</code> freezes the participant set at the
|
|
164
|
+
moment quorum is reached and never re-reads that job's artifacts again
|
|
165
|
+
(<code>fedavg_driver.py</code>, <code>run_fedavg</code>). A late delta was computed against
|
|
166
|
+
weights that no longer exist by the time it arrives — the model has already
|
|
167
|
+
moved past them — and applying it on top of a newer round's weights would
|
|
168
|
+
not be "one more contribution," it would silently corrupt the average with
|
|
169
|
+
a step that was never actually taken from the current state. Discarding is
|
|
170
|
+
the honest behavior; a driver that tried to be more "inclusive" here would
|
|
171
|
+
be quietly wrong instead.</p>
|
|
172
|
+
<p><code>tests/test_fedavg_convergence.py::test_round_completes_on_quorum_when_a_node_never_reports</code>
|
|
173
|
+
pins exactly this: three shards are dispatched but the test's agent pool is
|
|
174
|
+
capped to exactly two successful claims and then stops claiming, so the
|
|
175
|
+
third shard is never bound to any node and sits PENDING for the life of the
|
|
176
|
+
test. The round still aggregates on the two that committed — with an exact
|
|
177
|
+
<code>participants == 2</code> assertion — rather than hanging until the deadline
|
|
178
|
+
waiting for the shard nobody was ever going to serve. (The cap on claims,
|
|
179
|
+
not the node count, is what makes the third shard genuinely abandoned:
|
|
180
|
+
either registered node can claim either shard, so without the cap both
|
|
181
|
+
nodes could sequentially serve all three before the driver's poll notices
|
|
182
|
+
quorum.)</p>
|
|
183
|
+
<h2 id="what-counts-as-a-participant-and-what-the-driver-refuses">What counts as a participant, and what the driver refuses</h2>
|
|
184
|
+
<p>Everything a volunteer node produces — the delta, the sample count, the
|
|
185
|
+
metrics file, the filenames — is attacker-controlled input. Result
|
|
186
|
+
<em>verification</em> (catching a node that lies about a delta it honestly
|
|
187
|
+
computed) is a later milestone, but input validation and containment are
|
|
188
|
+
not deferred:</p>
|
|
189
|
+
<ul>
|
|
190
|
+
<li><strong>A participant is an accepted commit, not an uploaded file.</strong> The driver
|
|
191
|
+
counts only keys that exactly match the round's dispatched task set
|
|
192
|
+
(<code>jobs/{job_id}/shard-{i:03d}/metrics.json</code> for <code>i < num_shards</code>), and
|
|
193
|
+
cross-checks them against the tasks the coordinator reports <code>COMPLETED</code>
|
|
194
|
+
(<code>GET /v1alpha1/jobs/{id}/tasks</code>). Both halves are load-bearing: the agent
|
|
195
|
+
uploads a task's output tree recursively, so a nested <code>out/a/metrics.json</code>
|
|
196
|
+
would otherwise mint a second participant from one lease; and uploads
|
|
197
|
+
happen <em>before</em> the commit is offered, so an attempt the coordinator
|
|
198
|
+
rejected (lost lease, sha256 mismatch) would otherwise still be averaged
|
|
199
|
+
in.</li>
|
|
200
|
+
<li><strong>Sample counts must be positive.</strong> Validating only the total is not
|
|
201
|
+
enough — <code>(delta=-999, n=-999)</code> plus <code>(delta=1.0, n=1000)</code> totals a
|
|
202
|
+
healthy 1 sample but yields a weight of <code>999001.0</code> where the honest step
|
|
203
|
+
is <code>1.0</code>. A sample-weighted mean is only a convex combination when every
|
|
204
|
+
count is positive.</li>
|
|
205
|
+
<li><strong>NaN and Inf are rejected, not averaged.</strong> Python's <code>json</code> both emits and
|
|
206
|
+
parses <code>NaN</code>/<code>Infinity</code>, and NaN is absorbing: one non-finite value turns
|
|
207
|
+
every weight NaN, and every later round then trains from NaN while the run
|
|
208
|
+
still reports success. This one needs no attacker — a learning rate that
|
|
209
|
+
diverges on one shard does it. <code>fedavg_weights</code> fails closed on any
|
|
210
|
+
non-finite value entering the reduce or leaving <code>apply_delta</code>/<code>subtract</code>,
|
|
211
|
+
naming the parameter and index.</li>
|
|
212
|
+
<li><strong><code>lease_seconds</code> is bounded</strong> (<code>modea.MAX_LEASE_SECONDS</code>, one hour). A
|
|
213
|
+
lease deadline is the only thing that returns an abandoned task to the
|
|
214
|
+
queue, so <code>1e9</code> would pin a shard to a closed laptop for ~31 years and
|
|
215
|
+
<code>inf</code> overflows <code>timedelta</code> inside the coordinator's claim path.</li>
|
|
216
|
+
</ul>
|
|
217
|
+
<p>Artifact <code>PUT</code> is now authenticated and lease-scoped when the coordinator
|
|
218
|
+
sets <code>FLASHML_NODE_TOKENS</code> (the per-machine-token slice): a node token can
|
|
219
|
+
only write under <code>jobs/{job}/{task}/</code> for a task it currently holds a live
|
|
220
|
+
lease on. The round-weights key
|
|
221
|
+
(<code>jobs/{job_id}/round-{round:03d}/weights.json</code>) belongs to no task and no
|
|
222
|
+
node's lease, so a plain node token cannot write it — the driver instead
|
|
223
|
+
authenticates with an <strong>operator token</strong> (<code>FLASHML_OPERATOR_TOKENS</code>), which
|
|
224
|
+
is attributable but deliberately not lease-scoped, exactly because drivers
|
|
225
|
+
are legitimate writers outside any lease (see
|
|
226
|
+
<code>docs/guides/donate-a-machine.md</code>). Result verification is still a separate,
|
|
227
|
+
unbuilt concern: this scoping stops an unrelated node from <em>overwriting</em> the
|
|
228
|
+
round weights, not from a participant lying about the delta it honestly
|
|
229
|
+
computed.</p>
|
|
230
|
+
<h2 id="the-flashmlyaml-shape">The <code>flashml.yaml</code> shape</h2>
|
|
231
|
+
<p>A federated-averaging round is submitted as an ordinary lease-mode job:</p>
|
|
232
|
+
<div class="codeblock"><button class="copybtn" type="button" aria-label="Copy code to clipboard">Copy</button><pre><code class="language-yaml">apiVersion: flashml.dev/v1alpha1
|
|
233
|
+
kind: Job
|
|
234
|
+
metadata:
|
|
235
|
+
name: fedavg-r000
|
|
236
|
+
spec:
|
|
237
|
+
execution:
|
|
238
|
+
backend: leases
|
|
239
|
+
image:
|
|
240
|
+
repository: local/tier1
|
|
241
|
+
tag: dev
|
|
242
|
+
workload:
|
|
243
|
+
type: federated_averaging
|
|
244
|
+
parameters:
|
|
245
|
+
round: 0
|
|
246
|
+
num_shards: 2
|
|
247
|
+
local_steps: 20
|
|
248
|
+
lr: 0.1
|
|
249
|
+
batch_size: 16
|
|
250
|
+
seed: 0
|
|
251
|
+
in_dim: 8
|
|
252
|
+
hidden: 16
|
|
253
|
+
out_dim: 2
|
|
254
|
+
dataset_size: 256
|
|
255
|
+
# weights: artifact://jobs/<prev-job>/round-000/weights.json
|
|
256
|
+
# (omitted on round 0 — each worker seeds its own model from `seed`)
|
|
257
|
+
</code></pre></div>
|
|
258
|
+
<p><code>isolation.tier</code> is left at its default, <code>"standard"</code>, deliberately: unlike
|
|
259
|
+
the <code>argv</code> runner tier for arbitrary bring-your-code jobs, a
|
|
260
|
+
<code>federated_averaging</code> task's payload is a fixed, trusted <code>module</code> execution
|
|
261
|
+
(<code>flashml_workloads.fedavg_worker</code>), so it does not need the sandboxed argv
|
|
262
|
+
path and its <code>argv_capable</code> gate. A node only needs <code>module_capable</code>
|
|
263
|
+
(fail-open — absent counts as capable) to be eligible. <code>run_fedavg</code> builds
|
|
264
|
+
this JobSpec once per round and submits it as a new job
|
|
265
|
+
(<code>flashml_workloads/fedavg_driver.py:_round_body</code>) — the round number is
|
|
266
|
+
the only thing that changes between the driver's own resume points. The
|
|
267
|
+
image and isolation tier are <code>run_fedavg</code> parameters
|
|
268
|
+
(<code>image=</code>, <code>isolation_tier=</code>); the defaults above are this repo's e2e
|
|
269
|
+
fixture image, which only works because <code>SubprocessRunner</code> ignores <code>image</code>
|
|
270
|
+
entirely — a docker-tier volunteer needs a real, pullable reference.</p>
|
|
271
|
+
<h2 id="what-this-proves-and-what-it-does-not">What this proves — and what it does not</h2>
|
|
272
|
+
<p><code>tests/test_fedavg_convergence.py</code> runs this loop against a <strong>real</strong>
|
|
273
|
+
coordinator over real HTTP: real job expansion, real leases, real local
|
|
274
|
+
artifact storage (<code>FLASHML_LOCAL_ARTIFACTS_DIR</code>), and real commit-time
|
|
275
|
+
sha256 validation on every uploaded artifact. Two independent worker
|
|
276
|
+
"agents" (a few lines of <code>urllib</code>, standing in for <code>flashnode work</code> — see
|
|
277
|
+
the test file's docstring for why an in-repo test cannot import <code>flashnode</code>
|
|
278
|
+
directly) pull leases, train, and commit without ever talking to each
|
|
279
|
+
other. The measured per-round mean loss across four rounds with two
|
|
280
|
+
participating nodes:</p>
|
|
281
|
+
<div class="codeblock"><button class="copybtn" type="button" aria-label="Copy code to clipboard">Copy</button><pre><code>round 0 participants 2/2 mean_loss 0.5361
|
|
282
|
+
round 1 participants 2/2 mean_loss 0.3781
|
|
283
|
+
round 2 participants 2/2 mean_loss 0.2548
|
|
284
|
+
round 3 participants 2/2 mean_loss 0.1757
|
|
285
|
+
converged: 0.5361 -> 0.1757 over 4 rounds
|
|
286
|
+
</code></pre></div>
|
|
287
|
+
<p>(<code>scripts/fedavg_local_demo.py</code> reproduces this and exits non-zero if the
|
|
288
|
+
final round's loss is not below the first — a demo that prints numbers
|
|
289
|
+
nobody checks is not evidence.)</p>
|
|
290
|
+
<p>Read that number correctly: <strong>this proves collaborative training, not
|
|
291
|
+
faster training.</strong> Two nodes did not finish training in half the wall-clock
|
|
292
|
+
time of one — they trained <em>sequentially</em> through four rounds, each doing
|
|
293
|
+
its own local steps, and the loss came down because their independently
|
|
294
|
+
computed updates were combined. Nothing here claims a throughput or
|
|
295
|
+
speed-up result; DDP/FSDP make that claim, on a coordinated pool that can
|
|
296
|
+
rendezvous, and that claim is out of scope for volunteer nodes entirely (the
|
|
297
|
+
repo's <code>docs/guides/donate-a-machine.md</code> has the full list of things the
|
|
298
|
+
volunteer pool does not attempt, including "no coordinated multi-process
|
|
299
|
+
training"). What FedAvg proves is that machines which cannot see or trust
|
|
300
|
+
each other — and
|
|
301
|
+
in the volunteer case, cannot even reach each other over the network — can
|
|
302
|
+
still jointly move one model's loss in the right direction, coordinated
|
|
303
|
+
entirely through the coordinator's leases and artifact store.</p></main>
|
|
304
|
+
</div>
|
|
305
|
+
|
|
306
|
+
<script>
|
|
307
|
+
// ---- client-side search: fetch the builder's index, filter as you type -----
|
|
308
|
+
// (<=60 lines, vanilla JS, no external anything — the index is a sibling file.)
|
|
309
|
+
let INDEX = [];
|
|
310
|
+
// Path from THIS page back to the site root. The index stores root-relative
|
|
311
|
+
// URLs (e.g. "tutorials/deep.html"); we fetch it and build result links
|
|
312
|
+
// against ROOT so a page in a subdir resolves them too (see build_docs.py).
|
|
313
|
+
const ROOT = "../";
|
|
314
|
+
const q = document.getElementById("q");
|
|
315
|
+
const results = document.getElementById("results");
|
|
316
|
+
const esc = (s) => String(s).replace(/[&<>"]/g, (c) =>
|
|
317
|
+
({ "&": "&", "<": "<", ">": ">", '"': """ }[c]));
|
|
318
|
+
fetch(ROOT + "search-index.json").then((r) => r.json()).then((d) => { INDEX = d; }).catch(() => {});
|
|
319
|
+
|
|
320
|
+
// A short context window around the first match, so a hit shows WHY it matched.
|
|
321
|
+
function snippet(text, needle) {
|
|
322
|
+
const i = text.toLowerCase().indexOf(needle);
|
|
323
|
+
if (i < 0) return "";
|
|
324
|
+
const start = Math.max(0, i - 32);
|
|
325
|
+
return (start > 0 ? "…" : "") + text.slice(start, i + needle.length + 44).trim() + "…";
|
|
326
|
+
}
|
|
327
|
+
function runSearch() {
|
|
328
|
+
const needle = q.value.trim().toLowerCase();
|
|
329
|
+
if (!needle) { results.className = ""; results.innerHTML = ""; return; }
|
|
330
|
+
const hits = INDEX.map((p) => {
|
|
331
|
+
const inTitle = p.title.toLowerCase().includes(needle);
|
|
332
|
+
const inText = p.text.toLowerCase().includes(needle);
|
|
333
|
+
if (!inTitle && !inText) return null;
|
|
334
|
+
return { url: p.url, title: p.title, snip: inText ? snippet(p.text, needle) : "" };
|
|
335
|
+
}).filter(Boolean).slice(0, 20);
|
|
336
|
+
results.className = "open";
|
|
337
|
+
results.innerHTML = hits.length
|
|
338
|
+
? hits.map((h) => '<a href="' + ROOT + h.url + '"><b>' + esc(h.title) + "</b>" +
|
|
339
|
+
(h.snip ? "<span>" + esc(h.snip) + "</span>" : "") + "</a>").join("")
|
|
340
|
+
: '<div class="nohit">no matches</div>';
|
|
341
|
+
}
|
|
342
|
+
q.addEventListener("input", runSearch);
|
|
343
|
+
q.addEventListener("focus", runSearch);
|
|
344
|
+
document.addEventListener("click", (e) => {
|
|
345
|
+
if (!e.target.closest(".search")) { results.className = ""; } // dismiss on outside click
|
|
346
|
+
});
|
|
347
|
+
document.addEventListener("keydown", (e) => { // "/" focuses search, like PyTorch docs
|
|
348
|
+
if (e.key === "/" && document.activeElement !== q) { e.preventDefault(); q.focus(); }
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
// ---- copy buttons: read the <pre>'s own text into the clipboard ------------
|
|
352
|
+
document.querySelectorAll(".copybtn").forEach((btn) => {
|
|
353
|
+
btn.addEventListener("click", () => {
|
|
354
|
+
const code = btn.parentElement.querySelector("pre").innerText;
|
|
355
|
+
navigator.clipboard.writeText(code).then(() => {
|
|
356
|
+
const was = btn.textContent; btn.textContent = "Copied";
|
|
357
|
+
setTimeout(() => { btn.textContent = was; }, 1200);
|
|
358
|
+
}).catch(() => {});
|
|
359
|
+
});
|
|
360
|
+
});
|
|
361
|
+
</script>
|
|
362
|
+
</body>
|
|
363
|
+
</html>
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>Hugging Face guide — flashruntime docs</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root { color-scheme: dark; }
|
|
9
|
+
* { box-sizing: border-box; margin: 0; }
|
|
10
|
+
body { font: 14px/1.65 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; background: #0d1117; color: #c9d1d9; }
|
|
11
|
+
a { color: oklch(0.80 0.16 200); text-decoration: none; }
|
|
12
|
+
a:hover { text-decoration: underline; }
|
|
13
|
+
.layout { display: flex; align-items: flex-start; min-height: 100vh; }
|
|
14
|
+
|
|
15
|
+
/* sidebar — PyTorch-docs-like: sections from _nav.yml, current page active */
|
|
16
|
+
.sidebar { flex: 0 0 264px; position: sticky; top: 0; height: 100vh; overflow-y: auto;
|
|
17
|
+
border-right: 1px solid #21262d; background: #0d1117; padding: 20px 16px; }
|
|
18
|
+
.brand { color: #e6edf3; font-size: 15px; letter-spacing: .04em; }
|
|
19
|
+
.brand small { display: block; margin-top: 2px; color: #8b949e; font-size: 10px;
|
|
20
|
+
text-transform: uppercase; letter-spacing: .14em; }
|
|
21
|
+
.search { position: relative; margin: 16px 0; }
|
|
22
|
+
#q { width: 100%; background: #010409; border: 1px solid #21262d; border-radius: 6px;
|
|
23
|
+
color: #c9d1d9; font: 12px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; padding: 7px 9px; }
|
|
24
|
+
#q:focus { outline: 1px solid oklch(0.80 0.16 200); }
|
|
25
|
+
#results { position: absolute; left: 0; right: 0; top: 112%; z-index: 5; display: none;
|
|
26
|
+
background: #161b22; border: 1px solid #21262d; border-radius: 6px; overflow: hidden; }
|
|
27
|
+
#results.open { display: block; }
|
|
28
|
+
#results a { display: block; padding: 7px 9px; border-bottom: 1px solid #21262d; color: #c9d1d9; }
|
|
29
|
+
#results a:last-child { border-bottom: 0; }
|
|
30
|
+
#results a:hover { background: #010409; text-decoration: none; }
|
|
31
|
+
#results a b { color: #e6edf3; font-weight: 600; }
|
|
32
|
+
#results a span { display: block; margin-top: 2px; color: #8b949e; font-size: 11px; }
|
|
33
|
+
#results .nohit { padding: 7px 9px; color: #8b949e; }
|
|
34
|
+
.nav-section { margin: 16px 0 6px; color: #8b949e; font-size: 10px; font-weight: 600;
|
|
35
|
+
text-transform: uppercase; letter-spacing: .14em; }
|
|
36
|
+
.nav-link { display: block; padding: 4px 8px; border-radius: 5px; color: #c9d1d9; font-size: 13px; }
|
|
37
|
+
.nav-link:hover { background: #161b22; text-decoration: none; }
|
|
38
|
+
.nav-link.active { background: #161b22; color: #e6edf3; box-shadow: inset 2px 0 0 oklch(0.80 0.16 200); }
|
|
39
|
+
|
|
40
|
+
/* content */
|
|
41
|
+
.content { flex: 1 1 auto; max-width: 840px; min-width: 0; padding: 34px 40px 80px; }
|
|
42
|
+
.content h1 { color: #e6edf3; font-size: 26px; margin: 0 0 16px; }
|
|
43
|
+
.content h2 { color: #e6edf3; font-size: 18px; margin: 30px 0 10px;
|
|
44
|
+
padding-top: 10px; border-top: 1px solid #21262d; }
|
|
45
|
+
.content h3 { color: #e6edf3; font-size: 15px; margin: 22px 0 8px; }
|
|
46
|
+
.content p, .content li { color: #c9d1d9; }
|
|
47
|
+
.content ul, .content ol { padding-left: 22px; margin: 10px 0; }
|
|
48
|
+
.content li { margin: 4px 0; }
|
|
49
|
+
.content blockquote { margin: 14px 0; padding: 2px 14px; border-left: 3px solid oklch(0.80 0.18 60);
|
|
50
|
+
background: #161b22; border-radius: 0 6px 6px 0; color: #8b949e; }
|
|
51
|
+
.content :not(pre) > code { background: #010409; border: 1px solid #21262d;
|
|
52
|
+
border-radius: 4px; padding: 1px 5px; font-size: 12.5px; }
|
|
53
|
+
.content table { border-collapse: collapse; margin: 14px 0; display: block; overflow-x: auto; }
|
|
54
|
+
.content th, .content td { border: 1px solid #21262d; padding: 6px 10px; text-align: left; }
|
|
55
|
+
.content th { color: #e6edf3; background: #161b22; }
|
|
56
|
+
|
|
57
|
+
/* code blocks + copy button */
|
|
58
|
+
.codeblock { position: relative; margin: 14px 0; }
|
|
59
|
+
.codeblock pre { background: #010409; border: 1px solid #21262d; border-radius: 8px;
|
|
60
|
+
padding: 14px 16px; overflow-x: auto; }
|
|
61
|
+
.codeblock pre code { padding: 0; border: 0; background: none; font-size: 12.5px; color: #c9d1d9; }
|
|
62
|
+
.copybtn { position: absolute; top: 8px; right: 8px; padding: 3px 8px; cursor: pointer;
|
|
63
|
+
background: #161b22; border: 1px solid #21262d; border-radius: 5px;
|
|
64
|
+
color: #8b949e; font: 11px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
|
|
65
|
+
.copybtn:hover { color: #e6edf3; border-color: oklch(0.80 0.16 200); }
|
|
66
|
+
|
|
67
|
+
@media (max-width: 800px) {
|
|
68
|
+
.layout { flex-direction: column; }
|
|
69
|
+
.sidebar { position: static; height: auto; width: 100%; flex-basis: auto;
|
|
70
|
+
border-right: 0; border-bottom: 1px solid #21262d; }
|
|
71
|
+
.content { padding: 24px 18px 60px; }
|
|
72
|
+
}
|
|
73
|
+
</style>
|
|
74
|
+
</head>
|
|
75
|
+
<body>
|
|
76
|
+
<div class="layout">
|
|
77
|
+
<aside class="sidebar">
|
|
78
|
+
<a class="brand" href="../index.html">flashruntime<small>documentation</small></a>
|
|
79
|
+
<div class="search">
|
|
80
|
+
<input id="q" type="search" placeholder="Search docs (press /)" autocomplete="off" spellcheck="false">
|
|
81
|
+
<div id="results"></div>
|
|
82
|
+
</div>
|
|
83
|
+
<nav><div class="nav-section">Getting started</div>
|
|
84
|
+
<a class="nav-link" href="../index.html">FlashRuntime</a>
|
|
85
|
+
<a class="nav-link" href="../get-started.html">Get started</a>
|
|
86
|
+
<div class="nav-section">Tutorials</div>
|
|
87
|
+
<a class="nav-link" href="../tutorials/convnet.html">Tutorial: make a ConvNet fault-tolerant</a>
|
|
88
|
+
<a class="nav-link" href="../tutorials/sklearn-sweeps.html">Tutorial: parallel scikit-learn sweeps</a>
|
|
89
|
+
<a class="nav-link" href="../tutorials/fault-tolerance.html">Tutorial: automatic recovery, explained</a>
|
|
90
|
+
<div class="nav-section">Guides</div>
|
|
91
|
+
<a class="nav-link" href="pytorch.html">PyTorch guide</a>
|
|
92
|
+
<a class="nav-link" href="sklearn.html">scikit-learn guide</a>
|
|
93
|
+
<a class="nav-link active" href="huggingface.html">Hugging Face guide</a>
|
|
94
|
+
<a class="nav-link" href="jobspec-and-isolation.html">JobSpec & isolation guide</a>
|
|
95
|
+
<a class="nav-link" href="federated-averaging.html">Federated averaging</a>
|
|
96
|
+
<div class="nav-section">Concepts</div>
|
|
97
|
+
<a class="nav-link" href="../concepts/architecture.html">Architecture</a>
|
|
98
|
+
<div class="nav-section">Reference</div>
|
|
99
|
+
<a class="nav-link" href="../reference/sdk.html">Reference: SDK (`flashruntime`)</a>
|
|
100
|
+
<a class="nav-link" href="../reference/integrations.html">Reference: integrations (`flashruntime.integrations`)</a>
|
|
101
|
+
<a class="nav-link" href="../reference/torch-helper.html">Reference: torch helper (`flashruntime.torch`)</a>
|
|
102
|
+
<a class="nav-link" href="../reference/cli.html">Reference: CLI (`flashruntime`)</a>
|
|
103
|
+
<div class="nav-section">Benchmarks</div>
|
|
104
|
+
<a class="nav-link" href="../benchmarks.html">Benchmarks</a></nav>
|
|
105
|
+
</aside>
|
|
106
|
+
<main class="content"><h1 id="hugging-face-guide">Hugging Face guide</h1>
|
|
107
|
+
<p>FlashRuntime <strong>operates</strong> your Hugging Face job — it never rewrites your
|
|
108
|
+
<code>Trainer</code>. HF Trainer already wraps DDP/FSDP internally when it is launched by
|
|
109
|
+
<code>torchrun</code>, so <em>launching</em> an HF job is just the <a href="pytorch.html">PyTorch path</a>.
|
|
110
|
+
What <code>integrations.huggingface</code> adds is the <strong>callback seam</strong> that commits
|
|
111
|
+
Trainer checkpoints as verified manifests and relays Trainer metrics.</p>
|
|
112
|
+
<p><code>transformers</code> is imported only inside your training process — never in
|
|
113
|
+
FlashRuntime's core.</p>
|
|
114
|
+
<hr />
|
|
115
|
+
<h2 id="launching">Launching</h2>
|
|
116
|
+
<p><code>trainer(script, *, source=".", nproc_per_node=1, script_args="")</code> is a thin
|
|
117
|
+
wrapper over the PyTorch <code>ddp()</code> adapter, so everything in the
|
|
118
|
+
<a href="pytorch.html">PyTorch guide</a> about launching and multi-process applies
|
|
119
|
+
unchanged:</p>
|
|
120
|
+
<div class="codeblock"><button class="copybtn" type="button" aria-label="Copy code to clipboard">Copy</button><pre><code class="language-python">import flashruntime as flash
|
|
121
|
+
from flashruntime.integrations import huggingface as fr_hf
|
|
122
|
+
|
|
123
|
+
run = flash.submit(fr_hf.trainer(
|
|
124
|
+
"train_hf.py",
|
|
125
|
+
source="~/hf-project",
|
|
126
|
+
nproc_per_node=1,
|
|
127
|
+
script_args="--model_name_or_path bert-base-uncased",
|
|
128
|
+
))
|
|
129
|
+
</code></pre></div>
|
|
130
|
+
<hr />
|
|
131
|
+
<h2 id="the-callback-seam">The callback seam</h2>
|
|
132
|
+
<p>Inside your training script, wire the callback and the resume in the usual HF
|
|
133
|
+
way:</p>
|
|
134
|
+
<div class="codeblock"><button class="copybtn" type="button" aria-label="Copy code to clipboard">Copy</button><pre><code class="language-python">from flashruntime.integrations import huggingface as fr_hf
|
|
135
|
+
|
|
136
|
+
trainer.add_callback(fr_hf.flashruntime_callback()) # on_save -> manifest, on_log -> metrics
|
|
137
|
+
|
|
138
|
+
resume = fr_hf.latest_checkpoint(training_args.output_dir) # newest VALID checkpoint dir, or None
|
|
139
|
+
trainer.train(resume_from_checkpoint=resume)
|
|
140
|
+
</code></pre></div>
|
|
141
|
+
<ul>
|
|
142
|
+
<li><strong><code>flashruntime_callback()</code></strong> builds a <code>TrainerCallback</code> whose <code>on_save</code>
|
|
143
|
+
writes a verified manifest for <code>checkpoint-<step>/</code> (rank 0 only) and whose
|
|
144
|
+
<code>on_log</code> relays metrics through <code>flashruntime.torch.log_metrics</code>. The
|
|
145
|
+
<code>transformers</code> import is paid inside this factory, in your process.</li>
|
|
146
|
+
<li><strong><code>latest_checkpoint(output_dir)</code></strong> returns the storage prefix of the newest
|
|
147
|
+
checkpoint dir with a <em>valid</em> manifest (<code>None</code> means fresh start) — pass it
|
|
148
|
+
straight to <code>resume_from_checkpoint</code>.</li>
|
|
149
|
+
</ul>
|
|
150
|
+
<p>So a Trainer run gets the same verified, parts-first / manifest-last checkpoint
|
|
151
|
+
guarantee as a hand-written loop: a half-written checkpoint is never selected
|
|
152
|
+
for resume, because the manifest is written last.</p>
|
|
153
|
+
<hr />
|
|
154
|
+
<h2 id="adding-another-framework">Adding another framework</h2>
|
|
155
|
+
<p><code>integrations.huggingface</code> is the smallest possible adapter: <code>trainer(...)</code>
|
|
156
|
+
delegates straight to <code>ddp(...)</code>, and the only HF-specific code is the callback
|
|
157
|
+
that maps <code>on_save</code>/<code>on_log</code> onto FlashRuntime's manifest and metric contracts.
|
|
158
|
+
That is the extensibility pattern — teach FlashRuntime a new framework by
|
|
159
|
+
writing a small adapter under <code>flashruntime/integrations/</code> that (a) returns a
|
|
160
|
+
<code>CommandWorkload</code> describing what to launch, and (b), if the framework has its
|
|
161
|
+
own callback/hook system, maps those hooks onto <code>write_manifest</code> /
|
|
162
|
+
<code>log_metrics</code>. No core change is needed. See the
|
|
163
|
+
<a href="pytorch.html#adding-another-framework">PyTorch adapter</a> for the base case.</p></main>
|
|
164
|
+
</div>
|
|
165
|
+
|
|
166
|
+
<script>
|
|
167
|
+
// ---- client-side search: fetch the builder's index, filter as you type -----
|
|
168
|
+
// (<=60 lines, vanilla JS, no external anything — the index is a sibling file.)
|
|
169
|
+
let INDEX = [];
|
|
170
|
+
// Path from THIS page back to the site root. The index stores root-relative
|
|
171
|
+
// URLs (e.g. "tutorials/deep.html"); we fetch it and build result links
|
|
172
|
+
// against ROOT so a page in a subdir resolves them too (see build_docs.py).
|
|
173
|
+
const ROOT = "../";
|
|
174
|
+
const q = document.getElementById("q");
|
|
175
|
+
const results = document.getElementById("results");
|
|
176
|
+
const esc = (s) => String(s).replace(/[&<>"]/g, (c) =>
|
|
177
|
+
({ "&": "&", "<": "<", ">": ">", '"': """ }[c]));
|
|
178
|
+
fetch(ROOT + "search-index.json").then((r) => r.json()).then((d) => { INDEX = d; }).catch(() => {});
|
|
179
|
+
|
|
180
|
+
// A short context window around the first match, so a hit shows WHY it matched.
|
|
181
|
+
function snippet(text, needle) {
|
|
182
|
+
const i = text.toLowerCase().indexOf(needle);
|
|
183
|
+
if (i < 0) return "";
|
|
184
|
+
const start = Math.max(0, i - 32);
|
|
185
|
+
return (start > 0 ? "…" : "") + text.slice(start, i + needle.length + 44).trim() + "…";
|
|
186
|
+
}
|
|
187
|
+
function runSearch() {
|
|
188
|
+
const needle = q.value.trim().toLowerCase();
|
|
189
|
+
if (!needle) { results.className = ""; results.innerHTML = ""; return; }
|
|
190
|
+
const hits = INDEX.map((p) => {
|
|
191
|
+
const inTitle = p.title.toLowerCase().includes(needle);
|
|
192
|
+
const inText = p.text.toLowerCase().includes(needle);
|
|
193
|
+
if (!inTitle && !inText) return null;
|
|
194
|
+
return { url: p.url, title: p.title, snip: inText ? snippet(p.text, needle) : "" };
|
|
195
|
+
}).filter(Boolean).slice(0, 20);
|
|
196
|
+
results.className = "open";
|
|
197
|
+
results.innerHTML = hits.length
|
|
198
|
+
? hits.map((h) => '<a href="' + ROOT + h.url + '"><b>' + esc(h.title) + "</b>" +
|
|
199
|
+
(h.snip ? "<span>" + esc(h.snip) + "</span>" : "") + "</a>").join("")
|
|
200
|
+
: '<div class="nohit">no matches</div>';
|
|
201
|
+
}
|
|
202
|
+
q.addEventListener("input", runSearch);
|
|
203
|
+
q.addEventListener("focus", runSearch);
|
|
204
|
+
document.addEventListener("click", (e) => {
|
|
205
|
+
if (!e.target.closest(".search")) { results.className = ""; } // dismiss on outside click
|
|
206
|
+
});
|
|
207
|
+
document.addEventListener("keydown", (e) => { // "/" focuses search, like PyTorch docs
|
|
208
|
+
if (e.key === "/" && document.activeElement !== q) { e.preventDefault(); q.focus(); }
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
// ---- copy buttons: read the <pre>'s own text into the clipboard ------------
|
|
212
|
+
document.querySelectorAll(".copybtn").forEach((btn) => {
|
|
213
|
+
btn.addEventListener("click", () => {
|
|
214
|
+
const code = btn.parentElement.querySelector("pre").innerText;
|
|
215
|
+
navigator.clipboard.writeText(code).then(() => {
|
|
216
|
+
const was = btn.textContent; btn.textContent = "Copied";
|
|
217
|
+
setTimeout(() => { btn.textContent = was; }, 1200);
|
|
218
|
+
}).catch(() => {});
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
</script>
|
|
222
|
+
</body>
|
|
223
|
+
</html>
|