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,271 @@
|
|
|
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>JobSpec & isolation 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" href="huggingface.html">Hugging Face guide</a>
|
|
94
|
+
<a class="nav-link active" 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="jobspec-isolation-guide">JobSpec & isolation guide</h1>
|
|
107
|
+
<p><code>flash.submit()</code> runs your workload on the <strong>local</strong> machine. To hand a command
|
|
108
|
+
workload to a FlashRuntime <strong>coordinator</strong> — so nodes pull and run it under
|
|
109
|
+
leases, heartbeats, and recovery — you compile it to the versioned wire form (a
|
|
110
|
+
<code>JobSpec</code>) and POST it.</p>
|
|
111
|
+
<p>This guide covers that compile step and the <strong>isolation tier</strong> that decides
|
|
112
|
+
which machines a task is allowed to land on. For the local path, see the
|
|
113
|
+
<a href="pytorch.html">PyTorch</a> and <a href="sklearn.html">scikit-learn</a> guides.</p>
|
|
114
|
+
<hr />
|
|
115
|
+
<h2 id="compile-to-a-jobspec">Compile to a JobSpec</h2>
|
|
116
|
+
<div class="codeblock"><button class="copybtn" type="button" aria-label="Copy code to clipboard">Copy</button><pre><code class="language-python">from flashruntime.workloads.command import to_jobspec
|
|
117
|
+
from flashruntime.protocol.v1alpha1 import ImageSpec
|
|
118
|
+
|
|
119
|
+
jobspec = to_jobspec(
|
|
120
|
+
workload, # a CommandWorkload
|
|
121
|
+
name="my-sweep",
|
|
122
|
+
image=ImageSpec(repository="myrepo/trainer", tag="2026.07-a1b2c3"),
|
|
123
|
+
)
|
|
124
|
+
# POST jobspec.model_dump() to POST /v1alpha1/jobs
|
|
125
|
+
# (or from the CLI: flashruntime submit-spec spec.json)
|
|
126
|
+
</code></pre></div>
|
|
127
|
+
<p><code>to_jobspec(workload, name, image=None)</code> produces a
|
|
128
|
+
<code>JobSpec{execution.backend: "leases", workload.type: "command"}</code>. <strong>A pinned
|
|
129
|
+
image is required</strong> — remote runs must be reproducible, and the schema already
|
|
130
|
+
rejects the tag <code>latest</code>. On the coordinator the <code>command</code> recipe expands the
|
|
131
|
+
job into one <code>TaskSpec</code> per <code>task_params</code> entry (or a single task), each
|
|
132
|
+
carrying an <code>argv</code> payload, its env, its <code>artifact://</code> inputs, and its
|
|
133
|
+
isolation requirement.</p>
|
|
134
|
+
<hr />
|
|
135
|
+
<h2 id="isolation-tiers-fail-closed">Isolation tiers (fail closed)</h2>
|
|
136
|
+
<p>Every command task carries an isolation tier from <code>workload.isolation</code>:</p>
|
|
137
|
+
<table>
|
|
138
|
+
<thead>
|
|
139
|
+
<tr>
|
|
140
|
+
<th>Tier</th>
|
|
141
|
+
<th>Where it runs</th>
|
|
142
|
+
<th>Meaning</th>
|
|
143
|
+
</tr>
|
|
144
|
+
</thead>
|
|
145
|
+
<tbody>
|
|
146
|
+
<tr>
|
|
147
|
+
<td><code>standard</code> (default)</td>
|
|
148
|
+
<td>your own machines, RunPod, trusted pools</td>
|
|
149
|
+
<td>ordinary placement — runs anywhere</td>
|
|
150
|
+
</tr>
|
|
151
|
+
<tr>
|
|
152
|
+
<td><code>sandboxed</code></td>
|
|
153
|
+
<td>community / untrusted machines</td>
|
|
154
|
+
<td>may only be leased to a node advertising <code>sandbox_capable is True</code></td>
|
|
155
|
+
</tr>
|
|
156
|
+
</tbody>
|
|
157
|
+
</table>
|
|
158
|
+
<p>The placement gate (<code>scheduler.IsolationAwarePlacement</code>) is <strong>fail-closed</strong> on
|
|
159
|
+
the security-relevant field, per the schema-security rule:</p>
|
|
160
|
+
<ul>
|
|
161
|
+
<li>A node counts as capable <strong>only</strong> when <code>sandbox_capable is True</code> — a truthy
|
|
162
|
+
stand-in (the string <code>"false"</code>, <code>1</code>, <code>"yes"</code>) does <strong>not</strong> count.</li>
|
|
163
|
+
<li>Any tier that is not <code>None</code> / <code>""</code> / <code>"standard"</code> (including a mistyped
|
|
164
|
+
<code>"Sandboxed"</code>) is treated as requiring capability — no silent downgrade.</li>
|
|
165
|
+
<li>A <code>sandboxed</code> task never falls back to an uncapable node unless the workload
|
|
166
|
+
explicitly sets <code>isolation.allowFallback = True</code>.</li>
|
|
167
|
+
</ul>
|
|
168
|
+
<p>So a <code>sandboxed</code> task will sit unclaimed rather than land on a node that cannot
|
|
169
|
+
isolate it. That is the intended behavior: unsafe placement fails closed.</p>
|
|
170
|
+
<hr />
|
|
171
|
+
<h2 id="what-runs-where-today">What runs where today</h2>
|
|
172
|
+
<blockquote>
|
|
173
|
+
<p><strong>Local SDK path — works now.</strong> <code>flash.submit()</code> runs sklearn sweeps,
|
|
174
|
+
2-process CPU DDP (via <code>gloo</code>), and kill-and-resume from checkpoints on this
|
|
175
|
+
machine. All three are proven by the example e2e tests.</p>
|
|
176
|
+
<p><strong>Service-side command jobs — expansion, placement, and execution all
|
|
177
|
+
work.</strong> POSTing a <code>to_jobspec()</code> workload expands it into leased tasks,
|
|
178
|
+
places them fail-closed by isolation tier, and — with a FlashNode agent
|
|
179
|
+
running <code>--runner argv</code> — executes the <code>argv</code> payload inside a hardened,
|
|
180
|
+
network-isolated container and commits the result. <code>sandboxed</code> tasks are
|
|
181
|
+
only ever placed on a node that advertises both <code>sandbox_capable</code> and
|
|
182
|
+
<code>argv_capable</code>; see the repo's <code>docs/guides/donate-a-machine.md</code> for exactly
|
|
183
|
+
what that container confines (and does not).</p>
|
|
184
|
+
<p><strong>Later slices.</strong> Multi-node DDP (<code>nnodes > 1</code> rendezvous — not available on
|
|
185
|
+
volunteer nodes even later, since <code>--network none</code> rules out rendezvous),
|
|
186
|
+
result verification for untrusted volunteer nodes, remote providers
|
|
187
|
+
(RunPod) with source packaging (<code>git_revision</code>), and
|
|
188
|
+
<code>flash.run(StrategyPlan)</code> wiring are open follow-ups.</p>
|
|
189
|
+
</blockquote>
|
|
190
|
+
<p>For how a leased task recovers when a node disappears, see the
|
|
191
|
+
<a href="../tutorials/fault-tolerance.html">fault-tolerance tutorial</a> and the
|
|
192
|
+
<a href="../concepts/architecture.html">architecture</a> page — the same failure taxonomy
|
|
193
|
+
and policy table drive both the local path and the coordinator.</p>
|
|
194
|
+
<hr />
|
|
195
|
+
<h2 id="adding-another-framework">Adding another framework</h2>
|
|
196
|
+
<p>Isolation and JobSpec compilation are <strong>framework-neutral</strong>: <code>to_jobspec</code>
|
|
197
|
+
serializes any <code>CommandWorkload</code>, whatever built it (<code>fr_torch.ddp</code>,
|
|
198
|
+
<code>fr_sklearn.sweep</code>, <code>fr_hf.trainer</code>, or one you hand-construct). So a new
|
|
199
|
+
framework adapter (see the <a href="pytorch.html#adding-another-framework">PyTorch adapter</a>)
|
|
200
|
+
gets coordinator submission and isolation-aware placement for free — it only
|
|
201
|
+
has to return a <code>CommandWorkload</code>.</p>
|
|
202
|
+
<hr />
|
|
203
|
+
<h2 id="built-in-task-modules">Built-in task modules</h2>
|
|
204
|
+
<p>Besides <code>command</code> workloads, the coordinator ships three allowlisted task
|
|
205
|
+
modules under <code>flashml_workloads/</code> — <code>sklearn_trial</code> (hyperparameter
|
|
206
|
+
trials), <code>kmeans_shard</code>/<code>kmeans_driver</code> (sharded K-means), and
|
|
207
|
+
<code>sgd_trainer</code> (checkpointable SGD with bit-identical resume). They are
|
|
208
|
+
reference workloads for the lease protocol, not a required path: they
|
|
209
|
+
predate <code>command</code> workloads and remain the workspace e2e's proof fixtures.
|
|
210
|
+
Their contract is documented in each module's docstring and in the repo's
|
|
211
|
+
<code>AGENTS.md</code>.</p></main>
|
|
212
|
+
</div>
|
|
213
|
+
|
|
214
|
+
<script>
|
|
215
|
+
// ---- client-side search: fetch the builder's index, filter as you type -----
|
|
216
|
+
// (<=60 lines, vanilla JS, no external anything — the index is a sibling file.)
|
|
217
|
+
let INDEX = [];
|
|
218
|
+
// Path from THIS page back to the site root. The index stores root-relative
|
|
219
|
+
// URLs (e.g. "tutorials/deep.html"); we fetch it and build result links
|
|
220
|
+
// against ROOT so a page in a subdir resolves them too (see build_docs.py).
|
|
221
|
+
const ROOT = "../";
|
|
222
|
+
const q = document.getElementById("q");
|
|
223
|
+
const results = document.getElementById("results");
|
|
224
|
+
const esc = (s) => String(s).replace(/[&<>"]/g, (c) =>
|
|
225
|
+
({ "&": "&", "<": "<", ">": ">", '"': """ }[c]));
|
|
226
|
+
fetch(ROOT + "search-index.json").then((r) => r.json()).then((d) => { INDEX = d; }).catch(() => {});
|
|
227
|
+
|
|
228
|
+
// A short context window around the first match, so a hit shows WHY it matched.
|
|
229
|
+
function snippet(text, needle) {
|
|
230
|
+
const i = text.toLowerCase().indexOf(needle);
|
|
231
|
+
if (i < 0) return "";
|
|
232
|
+
const start = Math.max(0, i - 32);
|
|
233
|
+
return (start > 0 ? "…" : "") + text.slice(start, i + needle.length + 44).trim() + "…";
|
|
234
|
+
}
|
|
235
|
+
function runSearch() {
|
|
236
|
+
const needle = q.value.trim().toLowerCase();
|
|
237
|
+
if (!needle) { results.className = ""; results.innerHTML = ""; return; }
|
|
238
|
+
const hits = INDEX.map((p) => {
|
|
239
|
+
const inTitle = p.title.toLowerCase().includes(needle);
|
|
240
|
+
const inText = p.text.toLowerCase().includes(needle);
|
|
241
|
+
if (!inTitle && !inText) return null;
|
|
242
|
+
return { url: p.url, title: p.title, snip: inText ? snippet(p.text, needle) : "" };
|
|
243
|
+
}).filter(Boolean).slice(0, 20);
|
|
244
|
+
results.className = "open";
|
|
245
|
+
results.innerHTML = hits.length
|
|
246
|
+
? hits.map((h) => '<a href="' + ROOT + h.url + '"><b>' + esc(h.title) + "</b>" +
|
|
247
|
+
(h.snip ? "<span>" + esc(h.snip) + "</span>" : "") + "</a>").join("")
|
|
248
|
+
: '<div class="nohit">no matches</div>';
|
|
249
|
+
}
|
|
250
|
+
q.addEventListener("input", runSearch);
|
|
251
|
+
q.addEventListener("focus", runSearch);
|
|
252
|
+
document.addEventListener("click", (e) => {
|
|
253
|
+
if (!e.target.closest(".search")) { results.className = ""; } // dismiss on outside click
|
|
254
|
+
});
|
|
255
|
+
document.addEventListener("keydown", (e) => { // "/" focuses search, like PyTorch docs
|
|
256
|
+
if (e.key === "/" && document.activeElement !== q) { e.preventDefault(); q.focus(); }
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
// ---- copy buttons: read the <pre>'s own text into the clipboard ------------
|
|
260
|
+
document.querySelectorAll(".copybtn").forEach((btn) => {
|
|
261
|
+
btn.addEventListener("click", () => {
|
|
262
|
+
const code = btn.parentElement.querySelector("pre").innerText;
|
|
263
|
+
navigator.clipboard.writeText(code).then(() => {
|
|
264
|
+
const was = btn.textContent; btn.textContent = "Copied";
|
|
265
|
+
setTimeout(() => { btn.textContent = was; }, 1200);
|
|
266
|
+
}).catch(() => {});
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
</script>
|
|
270
|
+
</body>
|
|
271
|
+
</html>
|
|
@@ -0,0 +1,313 @@
|
|
|
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>PyTorch 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 active" 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" 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="pytorch-guide">PyTorch guide</h1>
|
|
107
|
+
<p>FlashRuntime <strong>operates</strong> your PyTorch job — it never rewrites your model. You
|
|
108
|
+
keep the framework, the model, the loop, and the loss you already have;
|
|
109
|
+
FlashRuntime launches your command, injects the environment it promises, tracks
|
|
110
|
+
metrics, validates checkpoints, retries on failure, and collects artifacts.</p>
|
|
111
|
+
<p>This is ADR-0003's fourth axis in practice: <strong>recipes integrate user code</strong>.
|
|
112
|
+
FlashRuntime plans, launches, observes, and recovers; the distributed math is
|
|
113
|
+
always done by PyTorch (<code>torchrun</code>, DDP). There are two paths, and both operate
|
|
114
|
+
<em>unmodified</em> torch code.</p>
|
|
115
|
+
<p>For a full worked walkthrough, do the <a href="../tutorials/convnet.html">ConvNet tutorial</a>.</p>
|
|
116
|
+
<hr />
|
|
117
|
+
<h2 id="path-1-a-script-that-is-already-ddp-ready">Path 1 — a script that is already DDP-ready</h2>
|
|
118
|
+
<p>If your script already calls <code>dist.init_process_group()</code> and wraps its model in
|
|
119
|
+
<code>DistributedDataParallel</code> itself, there are <strong>zero code changes</strong>. The adapter
|
|
120
|
+
just builds the <code>torchrun</code> command:</p>
|
|
121
|
+
<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
|
|
122
|
+
from flashruntime.integrations import pytorch as fr_torch
|
|
123
|
+
|
|
124
|
+
run = flash.submit(fr_torch.ddp(
|
|
125
|
+
"train.py",
|
|
126
|
+
source="examples/user_pytorch_vanilla",
|
|
127
|
+
nproc_per_node=2, # 2 processes on this host — gloo/CPU works, no GPU
|
|
128
|
+
script_args="--steps 100",
|
|
129
|
+
))
|
|
130
|
+
print(run.state.value, run.trials)
|
|
131
|
+
</code></pre></div>
|
|
132
|
+
<p><code>ddp(script, *, source=".", nproc_per_node=2, nnodes=1, script_args="",
|
|
133
|
+
env=None)</code> emits <code>torchrun --nproc-per-node=N --nnodes=1 --standalone
|
|
134
|
+
--local-addr=127.0.0.1 <script> <args></code>. <code>--local-addr=127.0.0.1</code> pins the
|
|
135
|
+
advertised rendezvous address to loopback (otherwise torchrun advertises
|
|
136
|
+
<code>socket.getfqdn()</code>, which on some macOS DNS setups is unresolvable and the run
|
|
137
|
+
hangs before spawning a process).</p>
|
|
138
|
+
<p><code>nnodes > 1</code> raises <code>NotImplementedError</code> today — multi-node rendezvous is a
|
|
139
|
+
launcher concern for a later slice. <code>--standalone</code> is single-node by
|
|
140
|
+
definition.</p>
|
|
141
|
+
<hr />
|
|
142
|
+
<h2 id="path-2-flashruntimetorch-the-optional-in-script-helper">Path 2 — <code>flashruntime.torch</code>, the optional in-script helper</h2>
|
|
143
|
+
<p>For a script you <em>are</em> willing to touch, one import makes it both
|
|
144
|
+
launch-anywhere and fault-tolerant, without rebuilding any framework machinery.
|
|
145
|
+
The surface is three verbs plus read-only launch-fact accessors:</p>
|
|
146
|
+
<div class="codeblock"><button class="copybtn" type="button" aria-label="Copy code to clipboard">Copy</button><pre><code class="language-python">import flashruntime.torch as ft
|
|
147
|
+
|
|
148
|
+
model, optimizer, loader = ft.prepare(model, optimizer, loader)
|
|
149
|
+
start = ft.start_step() # 0 fresh, >0 after a resume
|
|
150
|
+
...
|
|
151
|
+
ft.checkpoint(model, optimizer, step=step, every=100)
|
|
152
|
+
ft.log_metrics({"step": step, "loss": float(loss)})
|
|
153
|
+
if ft.is_main(): # ft.rank(), ft.world_size() too
|
|
154
|
+
...
|
|
155
|
+
</code></pre></div>
|
|
156
|
+
<ul>
|
|
157
|
+
<li><strong><code>prepare(model, optimizer=None, dataloader=None)</code></strong> — launched distributed
|
|
158
|
+
(<code>WORLD_SIZE > 1</code>) it initializes torch's <em>own</em> process group (<code>nccl</code> on GPU,
|
|
159
|
+
<code>gloo</code> on CPU), wraps the model in <code>DistributedDataParallel</code>, and <strong>swaps the
|
|
160
|
+
DataLoader's sampler for a seed-0 <code>DistributedSampler</code></strong> so each rank sees a
|
|
161
|
+
disjoint, deterministically-shuffled shard. It then restores the newest
|
|
162
|
+
<em>valid</em> checkpoint manifest if one exists, setting the resume step. Launched
|
|
163
|
+
as plain <code>python train.py</code>, it is a no-op passthrough.</li>
|
|
164
|
+
</ul>
|
|
165
|
+
<p><strong>CUDA device placement is wired.</strong> <code>prepare</code> selects <code>nccl</code> when CUDA is
|
|
166
|
+
present, moves your model onto this rank's GPU <em>before</em> the DDP wrap, and
|
|
167
|
+
binds DDP with <code>device_ids</code> / <code>output_device</code> — so a single-GPU box "just
|
|
168
|
+
works" and you no longer call <code>model.to(device)</code> yourself. The everyday e2e
|
|
169
|
+
tests exercise CPU / <code>gloo</code>; real-GPU validation is tracked in the workspace
|
|
170
|
+
progress log. <strong>Multi-node DDP (<code>nnodes > 1</code>) is a later slice.</strong></p>
|
|
171
|
+
<p>One caveat: <code>prepare</code> rebuilds the DataLoader carrying over <code>batch_size</code>,
|
|
172
|
+
<code>collate_fn</code>, <code>num_workers</code>, and <code>drop_last</code> — <strong><code>shuffle</code> and <code>pin_memory</code>
|
|
173
|
+
are not carried over</strong> (the <code>DistributedSampler</code> owns shuffling, at seed 0).</p>
|
|
174
|
+
<ul>
|
|
175
|
+
<li>
|
|
176
|
+
<p><strong><code>checkpoint(model, optimizer=None, *, step, every=None)</code></strong> — rank 0 writes
|
|
177
|
+
a checkpoint under the parts-first / manifest-last contract (the manifest is
|
|
178
|
+
written last, so a half-written checkpoint is never <code>latest_valid</code>). <code>every=N</code>
|
|
179
|
+
no-ops except on multiples of N. Every rank synchronizes on a barrier so no
|
|
180
|
+
one races past a partial write.</p>
|
|
181
|
+
</li>
|
|
182
|
+
<li>
|
|
183
|
+
<p><strong><code>log_metrics(dict)</code></strong> — rank 0 appends one JSON record per call to
|
|
184
|
+
<code>metrics.jsonl</code> (streaming history; the live page's loss curve reads it). It
|
|
185
|
+
never raises — metrics must never kill training. This is <em>separate</em> from the
|
|
186
|
+
final <code>metrics.json</code> your script writes for <code>run.trials</code>.</p>
|
|
187
|
+
</li>
|
|
188
|
+
<li>
|
|
189
|
+
<p><strong><code>start_step()</code> / <code>rank()</code> / <code>world_size()</code> / <code>is_main()</code> / <code>device()</code> /
|
|
190
|
+
<code>backend()</code></strong> — the small read-only helpers (<code>device()</code>/<code>backend()</code> let a
|
|
191
|
+
script report where it actually trained, e.g. into <code>metrics.json</code> — how
|
|
192
|
+
the GPU e2e proves the CUDA/nccl path). Full signatures in the
|
|
193
|
+
<a href="../reference/torch-helper.html">torch helper reference</a>.</p>
|
|
194
|
+
</li>
|
|
195
|
+
</ul>
|
|
196
|
+
<p>The same file, three ways:</p>
|
|
197
|
+
<table>
|
|
198
|
+
<thead>
|
|
199
|
+
<tr>
|
|
200
|
+
<th>Command</th>
|
|
201
|
+
<th>What runs</th>
|
|
202
|
+
</tr>
|
|
203
|
+
</thead>
|
|
204
|
+
<tbody>
|
|
205
|
+
<tr>
|
|
206
|
+
<td><code>python train.py --steps 200</code></td>
|
|
207
|
+
<td>single process, <code>prepare</code> is a passthrough</td>
|
|
208
|
+
</tr>
|
|
209
|
+
<tr>
|
|
210
|
+
<td><code>torchrun --nproc-per-node=2 --standalone train.py</code></td>
|
|
211
|
+
<td>DDP by hand</td>
|
|
212
|
+
</tr>
|
|
213
|
+
<tr>
|
|
214
|
+
<td><code>flash.submit(fr_torch.ddp("train.py", ...))</code></td>
|
|
215
|
+
<td>operated by FlashRuntime</td>
|
|
216
|
+
</tr>
|
|
217
|
+
</tbody>
|
|
218
|
+
</table>
|
|
219
|
+
<h3 id="determinism-bit-exact-resume">Determinism / bit-exact resume</h3>
|
|
220
|
+
<p>Keep the script deterministic on CPU (fixed seeds; the seed-0
|
|
221
|
+
<code>DistributedSampler</code> repeats its order every epoch) and a killed-and-resumed
|
|
222
|
+
run reproduces the uninterrupted result — recovery must not change the math.
|
|
223
|
+
There is one alignment constraint: on resume the <code>for</code> loop restarts the
|
|
224
|
+
dataloader at batch 0, so the resumed step must land on an <strong>epoch boundary</strong> —
|
|
225
|
+
a multiple of batches-per-rank-per-epoch. Keep <code>--checkpoint-every</code> a multiple
|
|
226
|
+
of that.</p>
|
|
227
|
+
<blockquote>
|
|
228
|
+
<p><strong>One <code>output_dir</code> is one workload.</strong> Resume works by reusing the job-scoped
|
|
229
|
+
checkpoint tree under <code>output_dir</code>. Point a <em>different</em> workload at an
|
|
230
|
+
<code>output_dir</code> that already holds another workload's checkpoints and
|
|
231
|
+
<code>prepare()</code> will happily restore those foreign weights — silent wrong
|
|
232
|
+
results, not an error. Use a <strong>fresh <code>output_dir</code> per workload</strong>; reusing one
|
|
233
|
+
for the <em>same</em> workload is exactly how kill-and-resume is meant to work.
|
|
234
|
+
(Fan-out sweeps are safe automatically: each trial gets its own checkpoint
|
|
235
|
+
tree.)</p>
|
|
236
|
+
</blockquote>
|
|
237
|
+
<h3 id="guardrail-adr-0003-do-not-rebuild-accelerate">Guardrail (ADR-0003 — do not rebuild Accelerate)</h3>
|
|
238
|
+
<p><code>flashruntime.torch</code> wraps torch's <em>own</em> DDP and stops. There are no FSDP
|
|
239
|
+
policies, no autocast, no DeepSpeed config in this surface. Users who want those
|
|
240
|
+
use the real framework features directly — the launcher still launches such a
|
|
241
|
+
script correctly, because launching is orthogonal to the strategy your code
|
|
242
|
+
chooses.</p>
|
|
243
|
+
<hr />
|
|
244
|
+
<h2 id="adding-another-framework">Adding another framework</h2>
|
|
245
|
+
<p>The PyTorch adapter is ~50 lines: it builds a <code>torchrun</code> command string and
|
|
246
|
+
returns a <code>CommandWorkload</code>. That is the whole extensibility pattern — a new
|
|
247
|
+
framework adapter is a small function under <code>flashruntime/integrations/</code> that
|
|
248
|
+
returns a <code>CommandWorkload</code>, reusing the same launch/collect/recover machinery.
|
|
249
|
+
<code>integrations.huggingface</code> is literally a thin wrapper over this
|
|
250
|
+
<code>ddp()</code> — see the <a href="huggingface.html">Hugging Face guide</a> — and
|
|
251
|
+
<code>integrations.sklearn</code> is the same shape for the fan-out case. No core change is
|
|
252
|
+
needed to teach FlashRuntime a new framework; you describe <em>what to run</em>, and
|
|
253
|
+
the four axes handle the rest.</p></main>
|
|
254
|
+
</div>
|
|
255
|
+
|
|
256
|
+
<script>
|
|
257
|
+
// ---- client-side search: fetch the builder's index, filter as you type -----
|
|
258
|
+
// (<=60 lines, vanilla JS, no external anything — the index is a sibling file.)
|
|
259
|
+
let INDEX = [];
|
|
260
|
+
// Path from THIS page back to the site root. The index stores root-relative
|
|
261
|
+
// URLs (e.g. "tutorials/deep.html"); we fetch it and build result links
|
|
262
|
+
// against ROOT so a page in a subdir resolves them too (see build_docs.py).
|
|
263
|
+
const ROOT = "../";
|
|
264
|
+
const q = document.getElementById("q");
|
|
265
|
+
const results = document.getElementById("results");
|
|
266
|
+
const esc = (s) => String(s).replace(/[&<>"]/g, (c) =>
|
|
267
|
+
({ "&": "&", "<": "<", ">": ">", '"': """ }[c]));
|
|
268
|
+
fetch(ROOT + "search-index.json").then((r) => r.json()).then((d) => { INDEX = d; }).catch(() => {});
|
|
269
|
+
|
|
270
|
+
// A short context window around the first match, so a hit shows WHY it matched.
|
|
271
|
+
function snippet(text, needle) {
|
|
272
|
+
const i = text.toLowerCase().indexOf(needle);
|
|
273
|
+
if (i < 0) return "";
|
|
274
|
+
const start = Math.max(0, i - 32);
|
|
275
|
+
return (start > 0 ? "…" : "") + text.slice(start, i + needle.length + 44).trim() + "…";
|
|
276
|
+
}
|
|
277
|
+
function runSearch() {
|
|
278
|
+
const needle = q.value.trim().toLowerCase();
|
|
279
|
+
if (!needle) { results.className = ""; results.innerHTML = ""; return; }
|
|
280
|
+
const hits = INDEX.map((p) => {
|
|
281
|
+
const inTitle = p.title.toLowerCase().includes(needle);
|
|
282
|
+
const inText = p.text.toLowerCase().includes(needle);
|
|
283
|
+
if (!inTitle && !inText) return null;
|
|
284
|
+
return { url: p.url, title: p.title, snip: inText ? snippet(p.text, needle) : "" };
|
|
285
|
+
}).filter(Boolean).slice(0, 20);
|
|
286
|
+
results.className = "open";
|
|
287
|
+
results.innerHTML = hits.length
|
|
288
|
+
? hits.map((h) => '<a href="' + ROOT + h.url + '"><b>' + esc(h.title) + "</b>" +
|
|
289
|
+
(h.snip ? "<span>" + esc(h.snip) + "</span>" : "") + "</a>").join("")
|
|
290
|
+
: '<div class="nohit">no matches</div>';
|
|
291
|
+
}
|
|
292
|
+
q.addEventListener("input", runSearch);
|
|
293
|
+
q.addEventListener("focus", runSearch);
|
|
294
|
+
document.addEventListener("click", (e) => {
|
|
295
|
+
if (!e.target.closest(".search")) { results.className = ""; } // dismiss on outside click
|
|
296
|
+
});
|
|
297
|
+
document.addEventListener("keydown", (e) => { // "/" focuses search, like PyTorch docs
|
|
298
|
+
if (e.key === "/" && document.activeElement !== q) { e.preventDefault(); q.focus(); }
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
// ---- copy buttons: read the <pre>'s own text into the clipboard ------------
|
|
302
|
+
document.querySelectorAll(".copybtn").forEach((btn) => {
|
|
303
|
+
btn.addEventListener("click", () => {
|
|
304
|
+
const code = btn.parentElement.querySelector("pre").innerText;
|
|
305
|
+
navigator.clipboard.writeText(code).then(() => {
|
|
306
|
+
const was = btn.textContent; btn.textContent = "Copied";
|
|
307
|
+
setTimeout(() => { btn.textContent = was; }, 1200);
|
|
308
|
+
}).catch(() => {});
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
</script>
|
|
312
|
+
</body>
|
|
313
|
+
</html>
|