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,302 @@
|
|
|
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>Architecture — 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="../guides/pytorch.html">PyTorch guide</a>
|
|
92
|
+
<a class="nav-link" href="../guides/sklearn.html">scikit-learn guide</a>
|
|
93
|
+
<a class="nav-link" href="../guides/huggingface.html">Hugging Face guide</a>
|
|
94
|
+
<a class="nav-link" href="../guides/jobspec-and-isolation.html">JobSpec & isolation guide</a>
|
|
95
|
+
<a class="nav-link" href="../guides/federated-averaging.html">Federated averaging</a>
|
|
96
|
+
<div class="nav-section">Concepts</div>
|
|
97
|
+
<a class="nav-link active" href="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="architecture">Architecture</h1>
|
|
107
|
+
<p>FlashRuntime plans, launches, observes, and recovers a distributed ML job. It
|
|
108
|
+
<strong>never</strong> reimplements the distributed math — that always belongs to your
|
|
109
|
+
framework (PyTorch DDP/FSDP, <code>torchrun</code>, Ray, Hugging Face). This page explains
|
|
110
|
+
the shape of the system: the four axes it is built from, and the three
|
|
111
|
+
mechanisms — leases, manifests, recovery — that make "runs to <em>verifiably</em>
|
|
112
|
+
completed on unreliable machines" a real guarantee rather than a slogan.</p>
|
|
113
|
+
<p>The design decisions here are recorded in ADR-0003 (<em>Reliability runtime first;
|
|
114
|
+
planner as an explainable feasibility filter</em>).</p>
|
|
115
|
+
<hr />
|
|
116
|
+
<h2 id="the-four-orthogonal-axes">The four orthogonal axes</h2>
|
|
117
|
+
<p>The central idea: <strong>getting machines, starting processes, configuring
|
|
118
|
+
execution, and integrating user code are four independent concerns.</strong> Keeping
|
|
119
|
+
them orthogonal is what lets the same job run on your laptop, on RunPod, or on a
|
|
120
|
+
community pool without rewriting anything.</p>
|
|
121
|
+
<div class="codeblock"><button class="copybtn" type="button" aria-label="Copy code to clipboard">Copy</button><pre><code> ┌──────────────────────────────────────────────────────────────┐
|
|
122
|
+
│ your training job │
|
|
123
|
+
└──────────────────────────────────────────────────────────────┘
|
|
124
|
+
│ │ │ │
|
|
125
|
+
▼ ▼ ▼ ▼
|
|
126
|
+
┌─────────┐ ┌──────────┐ ┌────────────┐ ┌──────────────┐
|
|
127
|
+
│providers│ │launchers │ │ strategies │ │ recipes │
|
|
128
|
+
│─────────│ │──────────│ │────────────│ │──────────────│
|
|
129
|
+
│ get │ │ start │ │ configure │ │ integrate │
|
|
130
|
+
│machines │ │processes │ │ execution │ │ user code │
|
|
131
|
+
└─────────┘ └──────────┘ └────────────┘ └──────────────┘
|
|
132
|
+
RunPod, torchrun, DDP / FSDP2 / PyTorch, sklearn,
|
|
133
|
+
local, local proc, zero3 offload, Hugging Face
|
|
134
|
+
K8s pools leased node single-GPU (this is the
|
|
135
|
+
workload layer)
|
|
136
|
+
</code></pre></div>
|
|
137
|
+
<p>The rule that keeps them honest: <strong>Hugging Face / PyTorch code lives in the
|
|
138
|
+
recipe (workload) layer — it is never a backend.</strong> The planner emits a
|
|
139
|
+
backend-neutral <code>StrategyPlan</code> and never imports framework code (no <code>import
|
|
140
|
+
transformers</code> / <code>torch.distributed</code> / <code>ray</code> inside the planner). Launching is
|
|
141
|
+
orthogonal to the strategy your code chooses, which is why FlashRuntime can
|
|
142
|
+
launch an FSDP or DeepSpeed script correctly without knowing anything about
|
|
143
|
+
FSDP or DeepSpeed.</p>
|
|
144
|
+
<p>The <code>integrations.*</code> adapters you use from the SDK (<code>fr_torch.ddp</code>,
|
|
145
|
+
<code>fr_sklearn.sweep</code>, <code>fr_hf.trainer</code>) are the recipe axis in practice: each
|
|
146
|
+
returns a <code>CommandWorkload</code> describing <em>what to run</em>, and the other three axes
|
|
147
|
+
handle the rest.</p>
|
|
148
|
+
<hr />
|
|
149
|
+
<h2 id="leases-the-mode-a-reliability-core">Leases — the Mode A reliability core</h2>
|
|
150
|
+
<p>A <strong>lease</strong> is a time-bounded right to run one task. It is the layer no
|
|
151
|
+
existing distributed-ML library provides, so FlashRuntime builds it first
|
|
152
|
+
(Mode A) before coordinated training (Mode B). A node <em>claims</em> a task, sends
|
|
153
|
+
<em>heartbeats</em> to keep the lease alive, and <em>commits</em> the result idempotently; if
|
|
154
|
+
the heartbeats stop, the lease <em>expires</em> and the task <em>requeues</em> — automatic
|
|
155
|
+
recovery with no central decision required.</p>
|
|
156
|
+
<div class="codeblock"><button class="copybtn" type="button" aria-label="Copy code to clipboard">Copy</button><pre><code> task: PENDING
|
|
157
|
+
│ claim (a node takes a time-bounded lease)
|
|
158
|
+
▼
|
|
159
|
+
LEASED ──heartbeat──► LEASED ──heartbeat──► LEASED
|
|
160
|
+
│ │
|
|
161
|
+
│ no heartbeat within TTL │ commit (validated, idempotent)
|
|
162
|
+
▼ ▼
|
|
163
|
+
EXPIRED ──requeue──► PENDING COMPLETED
|
|
164
|
+
</code></pre></div>
|
|
165
|
+
<p>Key properties:</p>
|
|
166
|
+
<ul>
|
|
167
|
+
<li><strong>Status is derived from an append-only ledger of events</strong>, never a
|
|
168
|
+
hand-mutated field. "What is this job's state?" is always answered by
|
|
169
|
+
replaying events, so the answer is reproducible and auditable.</li>
|
|
170
|
+
<li><strong>Commit is idempotent and validated.</strong> A result is accepted only if it
|
|
171
|
+
matches the task's expected commit key (a sha256), so a duplicate or corrupt
|
|
172
|
+
commit cannot poison the job.</li>
|
|
173
|
+
<li><strong>Lease state is durable.</strong> In-flight leases survive a coordinator restart
|
|
174
|
+
(a SQLite-backed store); agents re-register on their own.</li>
|
|
175
|
+
</ul>
|
|
176
|
+
<hr />
|
|
177
|
+
<h2 id="manifests-checkpoint-validity-by-construction">Manifests — checkpoint validity by construction</h2>
|
|
178
|
+
<p>A checkpoint is only useful if you can trust it after a crash. FlashRuntime
|
|
179
|
+
makes validity <strong>structural</strong> with a parts-first / manifest-last commit: the
|
|
180
|
+
checkpoint's parts are written first, then — only after their hashes verify —
|
|
181
|
+
the <strong>manifest</strong> is written last. No manifest, no checkpoint.</p>
|
|
182
|
+
<div class="codeblock"><button class="copybtn" type="button" aria-label="Copy code to clipboard">Copy</button><pre><code> write step-000123/
|
|
183
|
+
├─ model.pt (part) ─┐
|
|
184
|
+
├─ optimizer.pt (part) │ written FIRST
|
|
185
|
+
└─ ... ─┘
|
|
186
|
+
│ hashes verified
|
|
187
|
+
└─ manifest ──────────┘ written LAST ✔ now "latest_valid"
|
|
188
|
+
|
|
189
|
+
crash between parts and manifest ⇒ no manifest ⇒ never selected
|
|
190
|
+
</code></pre></div>
|
|
191
|
+
<p>So a half-written checkpoint — the exact thing a crash tends to produce — can
|
|
192
|
+
never look valid. Recovery restores only a <strong>verified, topology-compatible</strong>
|
|
193
|
+
manifest (the newest whose world size and framework match), which is why a
|
|
194
|
+
resumed run continues correctly instead of loading garbage. <code>ft.checkpoint(...)</code>
|
|
195
|
+
in the <a href="../reference/torch-helper.html">torch helper</a> writes under this contract;
|
|
196
|
+
the Hugging Face callback commits Trainer checkpoints the same way.</p>
|
|
197
|
+
<hr />
|
|
198
|
+
<h2 id="recovery-typed-deterministic-logged">Recovery — typed, deterministic, logged</h2>
|
|
199
|
+
<p>When something fails, recovery is a <strong>pure function of the evidence</strong>, not a
|
|
200
|
+
judgment call. Raw signals are classified into one failure class, and the class
|
|
201
|
+
(with the execution mode) is looked up in a versioned policy table that returns
|
|
202
|
+
a typed action. There is no LLM, no scoring, no learned model — same failure +
|
|
203
|
+
same policy version ⇒ same action, always.</p>
|
|
204
|
+
<div class="codeblock"><button class="copybtn" type="button" aria-label="Copy code to clipboard">Copy</button><pre><code> failure evidence classify() decide(class, mode)
|
|
205
|
+
──────────────── ───────────────────── ──────────────────────────
|
|
206
|
+
exit code, precedence-ordered: table lookup, versioned:
|
|
207
|
+
log tail, ───► systemic > node > ───► worker_crash + coordinated
|
|
208
|
+
heartbeat loss, process > app → restart_group (from ckpt)
|
|
209
|
+
health signals → one FailureClass app_error → fail_job (fast)
|
|
210
|
+
correlated → freeze_automation
|
|
211
|
+
</code></pre></div>
|
|
212
|
+
<p>The design commitments:</p>
|
|
213
|
+
<ul>
|
|
214
|
+
<li><strong>Deterministic application errors are never retried</strong> — fail fast and tell
|
|
215
|
+
the user. Burning capacity re-hitting a bug is the most expensive kind of
|
|
216
|
+
"recovery".</li>
|
|
217
|
+
<li><strong>Blast radius depends on mode.</strong> A <code>worker_crash</code> costs one task retry in
|
|
218
|
+
<code>independent_tasks</code> mode but a whole-group restart in <code>coordinated_training</code>
|
|
219
|
+
(NCCL collective state is not repairable in place).</li>
|
|
220
|
+
<li><strong>Correlated incidents freeze automation.</strong> The policy's most important
|
|
221
|
+
action is knowing when to <em>stop</em> acting — retry storms during a systemic
|
|
222
|
+
incident are how orchestrators destroy trust.</li>
|
|
223
|
+
<li><strong>Every decision is logged</strong> with its failure class and human-readable
|
|
224
|
+
reason, and emitted as <code>FAILURE_CLASSIFIED</code> / <code>RECOVERY_ACTION_SELECTED</code>
|
|
225
|
+
events the live page and ledger both show.</li>
|
|
226
|
+
</ul>
|
|
227
|
+
<p>The <a href="../tutorials/fault-tolerance.html">fault-tolerance tutorial</a> walks one real
|
|
228
|
+
crash through this pipeline end to end.</p>
|
|
229
|
+
<hr />
|
|
230
|
+
<h2 id="how-it-fits-together">How it fits together</h2>
|
|
231
|
+
<div class="codeblock"><button class="copybtn" type="button" aria-label="Copy code to clipboard">Copy</button><pre><code> plan ──► launch ──► observe ──► recover
|
|
232
|
+
(StrategyPlan, (providers + (leases + (classify + decide,
|
|
233
|
+
explained) launchers) heartbeats + typed actions from
|
|
234
|
+
manifests) the policy table)
|
|
235
|
+
</code></pre></div>
|
|
236
|
+
<p>The runtime is the spine; the planner is an explainable feasibility filter that
|
|
237
|
+
sits in front of it (<code>flash.plan()</code>), and the runtime's ledger is the planner's
|
|
238
|
+
dataset. Everything above is usable <strong>without the cloud</strong> — a self-hosted local
|
|
239
|
+
coordinator is a first-class mode, not a demo shim.</p>
|
|
240
|
+
<p>See also: the <a href="../reference/sdk.html">SDK reference</a> for the entry points, and the
|
|
241
|
+
<a href="../guides/jobspec-and-isolation.html">JobSpec & isolation guide</a> for the
|
|
242
|
+
coordinator wire form.</p></main>
|
|
243
|
+
</div>
|
|
244
|
+
|
|
245
|
+
<script>
|
|
246
|
+
// ---- client-side search: fetch the builder's index, filter as you type -----
|
|
247
|
+
// (<=60 lines, vanilla JS, no external anything — the index is a sibling file.)
|
|
248
|
+
let INDEX = [];
|
|
249
|
+
// Path from THIS page back to the site root. The index stores root-relative
|
|
250
|
+
// URLs (e.g. "tutorials/deep.html"); we fetch it and build result links
|
|
251
|
+
// against ROOT so a page in a subdir resolves them too (see build_docs.py).
|
|
252
|
+
const ROOT = "../";
|
|
253
|
+
const q = document.getElementById("q");
|
|
254
|
+
const results = document.getElementById("results");
|
|
255
|
+
const esc = (s) => String(s).replace(/[&<>"]/g, (c) =>
|
|
256
|
+
({ "&": "&", "<": "<", ">": ">", '"': """ }[c]));
|
|
257
|
+
fetch(ROOT + "search-index.json").then((r) => r.json()).then((d) => { INDEX = d; }).catch(() => {});
|
|
258
|
+
|
|
259
|
+
// A short context window around the first match, so a hit shows WHY it matched.
|
|
260
|
+
function snippet(text, needle) {
|
|
261
|
+
const i = text.toLowerCase().indexOf(needle);
|
|
262
|
+
if (i < 0) return "";
|
|
263
|
+
const start = Math.max(0, i - 32);
|
|
264
|
+
return (start > 0 ? "…" : "") + text.slice(start, i + needle.length + 44).trim() + "…";
|
|
265
|
+
}
|
|
266
|
+
function runSearch() {
|
|
267
|
+
const needle = q.value.trim().toLowerCase();
|
|
268
|
+
if (!needle) { results.className = ""; results.innerHTML = ""; return; }
|
|
269
|
+
const hits = INDEX.map((p) => {
|
|
270
|
+
const inTitle = p.title.toLowerCase().includes(needle);
|
|
271
|
+
const inText = p.text.toLowerCase().includes(needle);
|
|
272
|
+
if (!inTitle && !inText) return null;
|
|
273
|
+
return { url: p.url, title: p.title, snip: inText ? snippet(p.text, needle) : "" };
|
|
274
|
+
}).filter(Boolean).slice(0, 20);
|
|
275
|
+
results.className = "open";
|
|
276
|
+
results.innerHTML = hits.length
|
|
277
|
+
? hits.map((h) => '<a href="' + ROOT + h.url + '"><b>' + esc(h.title) + "</b>" +
|
|
278
|
+
(h.snip ? "<span>" + esc(h.snip) + "</span>" : "") + "</a>").join("")
|
|
279
|
+
: '<div class="nohit">no matches</div>';
|
|
280
|
+
}
|
|
281
|
+
q.addEventListener("input", runSearch);
|
|
282
|
+
q.addEventListener("focus", runSearch);
|
|
283
|
+
document.addEventListener("click", (e) => {
|
|
284
|
+
if (!e.target.closest(".search")) { results.className = ""; } // dismiss on outside click
|
|
285
|
+
});
|
|
286
|
+
document.addEventListener("keydown", (e) => { // "/" focuses search, like PyTorch docs
|
|
287
|
+
if (e.key === "/" && document.activeElement !== q) { e.preventDefault(); q.focus(); }
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
// ---- copy buttons: read the <pre>'s own text into the clipboard ------------
|
|
291
|
+
document.querySelectorAll(".copybtn").forEach((btn) => {
|
|
292
|
+
btn.addEventListener("click", () => {
|
|
293
|
+
const code = btn.parentElement.querySelector("pre").innerText;
|
|
294
|
+
navigator.clipboard.writeText(code).then(() => {
|
|
295
|
+
const was = btn.textContent; btn.textContent = "Copied";
|
|
296
|
+
setTimeout(() => { btn.textContent = was; }, 1200);
|
|
297
|
+
}).catch(() => {});
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
</script>
|
|
301
|
+
</body>
|
|
302
|
+
</html>
|
|
@@ -0,0 +1,263 @@
|
|
|
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>Get started — 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 active" 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="guides/pytorch.html">PyTorch guide</a>
|
|
92
|
+
<a class="nav-link" href="guides/sklearn.html">scikit-learn guide</a>
|
|
93
|
+
<a class="nav-link" href="guides/huggingface.html">Hugging Face guide</a>
|
|
94
|
+
<a class="nav-link" href="guides/jobspec-and-isolation.html">JobSpec & isolation guide</a>
|
|
95
|
+
<a class="nav-link" href="guides/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="get-started">Get started</h1>
|
|
107
|
+
<p>This page takes you from an empty environment to a fault-tolerant job and your
|
|
108
|
+
first 2-process DDP run — on CPU, with no cluster and no GPU. For the what and
|
|
109
|
+
why, see the <strong><a href="index.html">overview</a></strong>.</p>
|
|
110
|
+
<h2 id="install">Install</h2>
|
|
111
|
+
<p>The core is deliberately tiny — <code>pip install flashruntime</code> brings <strong>only
|
|
112
|
+
pydantic</strong>, and every core module (planner, leases, checkpoints, recovery, the
|
|
113
|
+
<code>flash.submit()</code> SDK) works with zero infrastructure:</p>
|
|
114
|
+
<div class="codeblock"><button class="copybtn" type="button" aria-label="Copy code to clipboard">Copy</button><pre><code class="language-bash">pip install flashruntime
|
|
115
|
+
</code></pre></div>
|
|
116
|
+
<p>Infrastructure integrations are opt-in extras, never core imports:</p>
|
|
117
|
+
<div class="codeblock"><button class="copybtn" type="button" aria-label="Copy code to clipboard">Copy</button><pre><code class="language-bash">pip install "flashruntime[service]" # the FastAPI coordinator + CLI
|
|
118
|
+
pip install "flashruntime[sklearn]" # numpy + scikit-learn for the sweep examples
|
|
119
|
+
</code></pre></div>
|
|
120
|
+
<p><strong>Torch is not a dependency.</strong> FlashRuntime <em>launches</em> PyTorch; it never
|
|
121
|
+
imports it (the four-axes rule — launching is orthogonal to your framework). To
|
|
122
|
+
run the DDP example below, install PyTorch yourself. A CPU-only build is enough
|
|
123
|
+
— DDP works over the <code>gloo</code> backend with no GPU:</p>
|
|
124
|
+
<div class="codeblock"><button class="copybtn" type="button" aria-label="Copy code to clipboard">Copy</button><pre><code class="language-bash">pip install torch # CPU build is fine; gloo needs no CUDA
|
|
125
|
+
</code></pre></div>
|
|
126
|
+
<h2 id="your-first-run">Your first run</h2>
|
|
127
|
+
<p><code>flash.submit()</code> operates any command. The one convention your script owes
|
|
128
|
+
FlashRuntime is to write a <code>metrics.json</code> (a flat JSON object) into its working
|
|
129
|
+
directory; FlashRuntime collects it and records it as a trial:</p>
|
|
130
|
+
<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
|
|
131
|
+
|
|
132
|
+
run = flash.submit(flash.CommandWorkload(
|
|
133
|
+
command="python -c \"import json; json.dump({'accuracy': 0.91}, open('metrics.json','w'))\"",
|
|
134
|
+
source=flash.Source(path="."),
|
|
135
|
+
))
|
|
136
|
+
|
|
137
|
+
print(run.state.value) # "SUCCEEDED"
|
|
138
|
+
print(run.trials) # [{'accuracy': 0.91}]
|
|
139
|
+
print(run.artifacts) # [PosixPath('.../metrics.json')]
|
|
140
|
+
</code></pre></div>
|
|
141
|
+
<p>Rerunning <code>flash.submit(workload, output_dir=...)</code> against the <strong>same</strong>
|
|
142
|
+
<code>output_dir</code> reuses the job id, so a checkpointed script resumes instead of
|
|
143
|
+
restarting. Pair that with <code>max_restarts=N</code> and a crash mid-run is recovered
|
|
144
|
+
automatically from the last valid checkpoint.</p>
|
|
145
|
+
<h2 id="your-first-ddp-run">Your first DDP run</h2>
|
|
146
|
+
<p>The <code>integrations.pytorch</code> adapter builds the <code>torchrun</code> command for you. If
|
|
147
|
+
your script already calls <code>torch.distributed.init_process_group()</code> and wraps
|
|
148
|
+
its model in <code>DistributedDataParallel</code>, there are <strong>zero code changes</strong> — the
|
|
149
|
+
adapter just launches it:</p>
|
|
150
|
+
<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
|
|
151
|
+
from flashruntime.integrations import pytorch as fr_torch
|
|
152
|
+
|
|
153
|
+
run = flash.submit(fr_torch.ddp(
|
|
154
|
+
"train.py",
|
|
155
|
+
source="examples/user_pytorch_vanilla",
|
|
156
|
+
nproc_per_node=2, # 2 processes on this host — gloo/CPU works
|
|
157
|
+
script_args="--steps 100",
|
|
158
|
+
))
|
|
159
|
+
|
|
160
|
+
print(run.state.value, run.trials)
|
|
161
|
+
</code></pre></div>
|
|
162
|
+
<p><code>ddp(script, *, source=".", nproc_per_node=2, nnodes=1, script_args="",
|
|
163
|
+
env=None)</code> emits <code>torchrun --nproc-per-node=N --nnodes=1 --standalone
|
|
164
|
+
--local-addr=127.0.0.1 <script> <args></code>. The <code>--local-addr=127.0.0.1</code> pins the
|
|
165
|
+
advertised rendezvous address to loopback (otherwise torchrun advertises
|
|
166
|
+
<code>socket.getfqdn()</code>, which on some macOS DNS setups is unresolvable and the run
|
|
167
|
+
hangs before spawning a process). <code>nproc_per_node=2</code> starts two worker
|
|
168
|
+
processes that rendezvous on loopback and hand each rank its <code>RANK</code> /
|
|
169
|
+
<code>WORLD_SIZE</code> / <code>LOCAL_RANK</code> — a real distributed run on a single machine, no
|
|
170
|
+
GPU required.</p>
|
|
171
|
+
<p><code>nnodes > 1</code> raises <code>NotImplementedError</code> today: multi-node rendezvous is a
|
|
172
|
+
launcher concern for a later slice. <code>--standalone</code> is single-node by
|
|
173
|
+
definition.</p>
|
|
174
|
+
<blockquote>
|
|
175
|
+
<p>Want fault-tolerant checkpointing inside a script you <em>are</em> willing to touch?
|
|
176
|
+
<code>import flashruntime.torch as ft</code> gives you <code>ft.prepare(...)</code>,
|
|
177
|
+
<code>ft.checkpoint(...)</code>, and <code>ft.log_metrics(...)</code> — torch's own DDP wrapped
|
|
178
|
+
under the parts-first / manifest-last checkpoint contract, so a killed run
|
|
179
|
+
resumes with its final loss matching an uninterrupted run to 1e-6 (the
|
|
180
|
+
e2e's assertion). It is optional sugar on the same launch-only
|
|
181
|
+
contract, never required.</p>
|
|
182
|
+
</blockquote>
|
|
183
|
+
<p>The same <code>flashruntime.torch</code> path runs unchanged on GPUs — <code>ft.prepare</code>
|
|
184
|
+
places each rank's model on its <code>cuda:N</code>, initializes the <code>nccl</code> backend, and
|
|
185
|
+
restores checkpoints across the CUDA↔CPU boundary. <strong>Validated on real GPUs
|
|
186
|
+
(2×RTX 4090, nccl) — 2026-07-23</strong> (torch 2.7.1+cu128, CUDA 12.8), covering the
|
|
187
|
+
2-process nccl DDP run and a GPU kill-and-resume; see <code>tests/test_gpu_e2e.py</code>.</p>
|
|
188
|
+
<h2 id="watch-it-run">Watch it run</h2>
|
|
189
|
+
<p>Pass <code>watch=True</code> (or just run at an interactive terminal, where it is the
|
|
190
|
+
default) and <code>flash.submit()</code> opens a live run page in your browser and prints
|
|
191
|
+
its URL:</p>
|
|
192
|
+
<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
|
|
193
|
+
|
|
194
|
+
run = flash.submit(
|
|
195
|
+
flash.CommandWorkload(command="python train.py", source=flash.Source(path=".")),
|
|
196
|
+
watch=True,
|
|
197
|
+
)
|
|
198
|
+
print(run.viewer_url) # http://127.0.0.1:<port>
|
|
199
|
+
</code></pre></div>
|
|
200
|
+
<p>The page draws the run's topology, loss curve, verified checkpoints, and every
|
|
201
|
+
recovery decision, refreshing every couple of seconds — served entirely from a
|
|
202
|
+
loopback server with <strong>no external assets</strong>, so it renders with the network
|
|
203
|
+
cut. These docs are served from that same viewer at <code>/docs</code>.</p></main>
|
|
204
|
+
</div>
|
|
205
|
+
|
|
206
|
+
<script>
|
|
207
|
+
// ---- client-side search: fetch the builder's index, filter as you type -----
|
|
208
|
+
// (<=60 lines, vanilla JS, no external anything — the index is a sibling file.)
|
|
209
|
+
let INDEX = [];
|
|
210
|
+
// Path from THIS page back to the site root. The index stores root-relative
|
|
211
|
+
// URLs (e.g. "tutorials/deep.html"); we fetch it and build result links
|
|
212
|
+
// against ROOT so a page in a subdir resolves them too (see build_docs.py).
|
|
213
|
+
const ROOT = "";
|
|
214
|
+
const q = document.getElementById("q");
|
|
215
|
+
const results = document.getElementById("results");
|
|
216
|
+
const esc = (s) => String(s).replace(/[&<>"]/g, (c) =>
|
|
217
|
+
({ "&": "&", "<": "<", ">": ">", '"': """ }[c]));
|
|
218
|
+
fetch(ROOT + "search-index.json").then((r) => r.json()).then((d) => { INDEX = d; }).catch(() => {});
|
|
219
|
+
|
|
220
|
+
// A short context window around the first match, so a hit shows WHY it matched.
|
|
221
|
+
function snippet(text, needle) {
|
|
222
|
+
const i = text.toLowerCase().indexOf(needle);
|
|
223
|
+
if (i < 0) return "";
|
|
224
|
+
const start = Math.max(0, i - 32);
|
|
225
|
+
return (start > 0 ? "…" : "") + text.slice(start, i + needle.length + 44).trim() + "…";
|
|
226
|
+
}
|
|
227
|
+
function runSearch() {
|
|
228
|
+
const needle = q.value.trim().toLowerCase();
|
|
229
|
+
if (!needle) { results.className = ""; results.innerHTML = ""; return; }
|
|
230
|
+
const hits = INDEX.map((p) => {
|
|
231
|
+
const inTitle = p.title.toLowerCase().includes(needle);
|
|
232
|
+
const inText = p.text.toLowerCase().includes(needle);
|
|
233
|
+
if (!inTitle && !inText) return null;
|
|
234
|
+
return { url: p.url, title: p.title, snip: inText ? snippet(p.text, needle) : "" };
|
|
235
|
+
}).filter(Boolean).slice(0, 20);
|
|
236
|
+
results.className = "open";
|
|
237
|
+
results.innerHTML = hits.length
|
|
238
|
+
? hits.map((h) => '<a href="' + ROOT + h.url + '"><b>' + esc(h.title) + "</b>" +
|
|
239
|
+
(h.snip ? "<span>" + esc(h.snip) + "</span>" : "") + "</a>").join("")
|
|
240
|
+
: '<div class="nohit">no matches</div>';
|
|
241
|
+
}
|
|
242
|
+
q.addEventListener("input", runSearch);
|
|
243
|
+
q.addEventListener("focus", runSearch);
|
|
244
|
+
document.addEventListener("click", (e) => {
|
|
245
|
+
if (!e.target.closest(".search")) { results.className = ""; } // dismiss on outside click
|
|
246
|
+
});
|
|
247
|
+
document.addEventListener("keydown", (e) => { // "/" focuses search, like PyTorch docs
|
|
248
|
+
if (e.key === "/" && document.activeElement !== q) { e.preventDefault(); q.focus(); }
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
// ---- copy buttons: read the <pre>'s own text into the clipboard ------------
|
|
252
|
+
document.querySelectorAll(".copybtn").forEach((btn) => {
|
|
253
|
+
btn.addEventListener("click", () => {
|
|
254
|
+
const code = btn.parentElement.querySelector("pre").innerText;
|
|
255
|
+
navigator.clipboard.writeText(code).then(() => {
|
|
256
|
+
const was = btn.textContent; btn.textContent = "Copied";
|
|
257
|
+
setTimeout(() => { btn.textContent = was; }, 1200);
|
|
258
|
+
}).catch(() => {});
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
</script>
|
|
262
|
+
</body>
|
|
263
|
+
</html>
|