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,341 @@
|
|
|
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>Reference: SDK (`flashruntime`) — 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" href="../concepts/architecture.html">Architecture</a>
|
|
98
|
+
<div class="nav-section">Reference</div>
|
|
99
|
+
<a class="nav-link active" href="sdk.html">Reference: SDK (`flashruntime`)</a>
|
|
100
|
+
<a class="nav-link" href="integrations.html">Reference: integrations (`flashruntime.integrations`)</a>
|
|
101
|
+
<a class="nav-link" href="torch-helper.html">Reference: torch helper (`flashruntime.torch`)</a>
|
|
102
|
+
<a class="nav-link" href="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="reference-sdk-flashruntime">Reference: SDK (<code>flashruntime</code>)</h1>
|
|
107
|
+
<p>The <code>flashruntime</code> top-level package. The core is pydantic-only — <code>import
|
|
108
|
+
flashruntime</code> pulls in <strong>only pydantic</strong>; the bring-your-own-code helpers
|
|
109
|
+
(<code>submit</code>, <code>CommandWorkload</code>, <code>integrations</code>) resolve lazily so the planner
|
|
110
|
+
stays a minimal import.</p>
|
|
111
|
+
<p>Signatures below are exact. Each entry says, in one line, <em>why it exists</em>.</p>
|
|
112
|
+
<hr />
|
|
113
|
+
<h2 id="running-a-workload">Running a workload</h2>
|
|
114
|
+
<h3 id="flashsubmit"><code>flash.submit</code></h3>
|
|
115
|
+
<div class="codeblock"><button class="copybtn" type="button" aria-label="Copy code to clipboard">Copy</button><pre><code class="language-python">def submit(workload, output_dir=None, wait=True, max_restarts=0, watch=None) -> Run: ...
|
|
116
|
+
</code></pre></div>
|
|
117
|
+
<p>The local entry point — compiles a <code>CommandWorkload</code> into a launch spec, runs
|
|
118
|
+
it as a real subprocess (once per param set), collects artifacts, and returns a
|
|
119
|
+
<code>Run</code>.</p>
|
|
120
|
+
<ul>
|
|
121
|
+
<li><code>output_dir</code> — where <code>run.json</code> and artifacts land; a temp dir if <code>None</code>.
|
|
122
|
+
Reusing the <strong>same</strong> dir reuses the job id, so a checkpointed script resumes.</li>
|
|
123
|
+
<li><code>wait</code> — <code>True</code> drives the launch loop inline and returns a finished <code>Run</code>;
|
|
124
|
+
<code>False</code> returns immediately and drives it on a daemon thread (watch it live).</li>
|
|
125
|
+
<li><code>max_restarts</code> — the automatic fault-tolerance budget (default <code>0</code> = no
|
|
126
|
+
retry). A FAILED attempt is classified and run against the versioned recovery
|
|
127
|
+
policy; a deterministic app error fails fast, anything else relaunches from
|
|
128
|
+
the job-scoped checkpoint up to this many times.</li>
|
|
129
|
+
<li><code>watch</code> — open the live viewer and record its URL on <code>run.viewer_url</code>;
|
|
130
|
+
<code>None</code> (default) auto-decides (on at an interactive terminal, off in
|
|
131
|
+
pipes/CI).</li>
|
|
132
|
+
</ul>
|
|
133
|
+
<h3 id="run"><code>Run</code></h3>
|
|
134
|
+
<p>The result handle, fully populated by the time a <code>wait=True</code> <code>submit()</code> returns.</p>
|
|
135
|
+
<table>
|
|
136
|
+
<thead>
|
|
137
|
+
<tr>
|
|
138
|
+
<th>Attribute / method</th>
|
|
139
|
+
<th>Meaning</th>
|
|
140
|
+
</tr>
|
|
141
|
+
</thead>
|
|
142
|
+
<tbody>
|
|
143
|
+
<tr>
|
|
144
|
+
<td><code>run.state</code></td>
|
|
145
|
+
<td><code>LaunchState</code> — <code>SUCCEEDED</code> if every task succeeded, else <code>FAILED</code></td>
|
|
146
|
+
</tr>
|
|
147
|
+
<tr>
|
|
148
|
+
<td><code>run.trials</code></td>
|
|
149
|
+
<td>list of parsed <code>metrics.json</code> dicts (one per task; fan-out merges its <code>params</code>)</td>
|
|
150
|
+
</tr>
|
|
151
|
+
<tr>
|
|
152
|
+
<td><code>run.artifacts</code></td>
|
|
153
|
+
<td>list of collected file <code>Path</code>s</td>
|
|
154
|
+
</tr>
|
|
155
|
+
<tr>
|
|
156
|
+
<td><code>run.output_dir</code></td>
|
|
157
|
+
<td>root the run wrote under</td>
|
|
158
|
+
</tr>
|
|
159
|
+
<tr>
|
|
160
|
+
<td><code>run.viewer_url</code></td>
|
|
161
|
+
<td>live-page URL if <code>watch</code> opened one, else <code>None</code></td>
|
|
162
|
+
</tr>
|
|
163
|
+
<tr>
|
|
164
|
+
<td><code>run.events</code></td>
|
|
165
|
+
<td>snapshot copy of the append-only event log</td>
|
|
166
|
+
</tr>
|
|
167
|
+
<tr>
|
|
168
|
+
<td><code>run.attempts</code></td>
|
|
169
|
+
<td>snapshot copy of the per-launch attempt rows</td>
|
|
170
|
+
</tr>
|
|
171
|
+
<tr>
|
|
172
|
+
<td><code>run.run_json_path</code></td>
|
|
173
|
+
<td>path to the <code>viewer_v1</code> <code>run.json</code> the Run mirrors itself to</td>
|
|
174
|
+
</tr>
|
|
175
|
+
<tr>
|
|
176
|
+
<td><code>run.wait(timeout=None)</code></td>
|
|
177
|
+
<td>block until terminal (event-based, no poll); returns the state</td>
|
|
178
|
+
</tr>
|
|
179
|
+
<tr>
|
|
180
|
+
<td><code>run.logs(tail_lines=200)</code></td>
|
|
181
|
+
<td>captured stdout+stderr (tail)</td>
|
|
182
|
+
</tr>
|
|
183
|
+
<tr>
|
|
184
|
+
<td><code>run.best_trial(metric=None, maximize=None)</code></td>
|
|
185
|
+
<td>best trial by <code>metric</code> (defaults from the workload's <code>OutputSpec</code>); <code>None</code> if none reported it</td>
|
|
186
|
+
</tr>
|
|
187
|
+
</tbody>
|
|
188
|
+
</table>
|
|
189
|
+
<hr />
|
|
190
|
+
<h2 id="describing-a-workload">Describing a workload</h2>
|
|
191
|
+
<h3 id="flashcommandworkload"><code>flash.CommandWorkload</code></h3>
|
|
192
|
+
<div class="codeblock"><button class="copybtn" type="button" aria-label="Copy code to clipboard">Copy</button><pre><code class="language-python">class CommandWorkload(BaseModel):
|
|
193
|
+
command: str | list[str] # shlex-split (NO shell) or an argv list
|
|
194
|
+
source: Source = Source() # where the user's code lives
|
|
195
|
+
image: ImageSpec | None = None # pinned image (required only for the service path)
|
|
196
|
+
env: dict[str, str] = {}
|
|
197
|
+
inputs: dict[str, str] = {} # each value must be an artifact:// URI
|
|
198
|
+
outputs: OutputSpec = OutputSpec()
|
|
199
|
+
resources: Requirements = Requirements() # resource hints (dormant locally; for future providers)
|
|
200
|
+
isolation: IsolationSpec = IsolationSpec()
|
|
201
|
+
mode: str = "auto" # auto | local | independent_tasks | coordinated
|
|
202
|
+
checkpoint: CheckpointPolicy | None = None
|
|
203
|
+
task_params: list[dict] | None = None # {name} placeholders filled per entry (Mode A fan-out)
|
|
204
|
+
</code></pre></div>
|
|
205
|
+
<p>The user-facing description of a "bring your own code" workload — <em>what</em> to run
|
|
206
|
+
and what FlashRuntime should do around it. It never describes <em>how</em> distributed
|
|
207
|
+
math happens; that belongs to your code. <code>command</code> is <code>shlex</code>-split (there is no
|
|
208
|
+
shell — pipes need an explicit <code>command="bash -c '...'"</code>).</p>
|
|
209
|
+
<h3 id="flashsource"><code>flash.Source</code></h3>
|
|
210
|
+
<div class="codeblock"><button class="copybtn" type="button" aria-label="Copy code to clipboard">Copy</button><pre><code class="language-python">class Source(BaseModel):
|
|
211
|
+
path: str = "." # local dir; ~ is expanded
|
|
212
|
+
git_revision: str | None = None # reserved for remote packaging (later slice)
|
|
213
|
+
</code></pre></div>
|
|
214
|
+
<p>Where the user's code lives — a <code>flash.Source</code>, not a bare string.</p>
|
|
215
|
+
<h3 id="flashoutputspec"><code>flash.OutputSpec</code></h3>
|
|
216
|
+
<div class="codeblock"><button class="copybtn" type="button" aria-label="Copy code to clipboard">Copy</button><pre><code class="language-python">class OutputSpec(BaseModel):
|
|
217
|
+
prefix: str = "artifact://jobs/{job_id}/"
|
|
218
|
+
collect: list[str] = ["metrics.json"] # globs resolved against the script's cwd
|
|
219
|
+
primary_metric: str | None = None # the metrics.json key best_trial() ranks by
|
|
220
|
+
maximize: bool = True
|
|
221
|
+
</code></pre></div>
|
|
222
|
+
<p>What to keep after a run, and how to rank trials. <code>metrics.json</code> in <code>collect</code>
|
|
223
|
+
(the default) is what populates <code>run.trials</code>.</p>
|
|
224
|
+
<hr />
|
|
225
|
+
<h2 id="planning-a-job">Planning a job</h2>
|
|
226
|
+
<h3 id="flashplan"><code>flash.plan</code></h3>
|
|
227
|
+
<div class="codeblock"><button class="copybtn" type="button" aria-label="Copy code to clipboard">Copy</button><pre><code class="language-python">def plan(request: PlanRequest) -> PlanReport: ...
|
|
228
|
+
</code></pre></div>
|
|
229
|
+
<p>Deterministic, explainable strategy selection — turns model + hardware +
|
|
230
|
+
objective into a ranked, explained <code>PlanReport</code> (no cluster required). The
|
|
231
|
+
closed-form arithmetic is framework-import-free.</p>
|
|
232
|
+
<h3 id="flashrender"><code>flash.render</code></h3>
|
|
233
|
+
<div class="codeblock"><button class="copybtn" type="button" aria-label="Copy code to clipboard">Copy</button><pre><code class="language-python">def render(report: PlanReport) -> str: ...
|
|
234
|
+
</code></pre></div>
|
|
235
|
+
<p>Renders a <code>PlanReport</code> as human-readable text (the numbers, the chosen plan,
|
|
236
|
+
and the rejected alternatives with their reasons).</p>
|
|
237
|
+
<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
|
|
238
|
+
|
|
239
|
+
report = flash.plan(flash.PlanRequest(
|
|
240
|
+
workload=flash.TransformerFineTune(model="Qwen/Qwen2.5-7B", method="lora"),
|
|
241
|
+
resources=flash.Resources(gpus=4, gpu_type="RTX4090"),
|
|
242
|
+
objective=flash.Objective(mode="balanced", deadline_minutes=240),
|
|
243
|
+
))
|
|
244
|
+
print(flash.render(report))
|
|
245
|
+
</code></pre></div>
|
|
246
|
+
<h3 id="plan-inputs">Plan inputs</h3>
|
|
247
|
+
<div class="codeblock"><button class="copybtn" type="button" aria-label="Copy code to clipboard">Copy</button><pre><code class="language-python">class Resources(BaseModel):
|
|
248
|
+
gpus: int = 0 # 0 = CPU-only
|
|
249
|
+
gpu_type: str | None = None # 'A100-40GB', 'L40S', 'RTX4090', ...
|
|
250
|
+
vram_gb: float | None = None # per-GPU VRAM override
|
|
251
|
+
hosts: int = 1
|
|
252
|
+
cpu_ram_gb: float = 32.0
|
|
253
|
+
cpu_cores: int = 8
|
|
254
|
+
hourly_cost_usd_per_gpu: float | None = None
|
|
255
|
+
|
|
256
|
+
class Objective(BaseModel):
|
|
257
|
+
mode: str = "balanced" # cheapest | fastest | balanced | reliable
|
|
258
|
+
max_cost_usd: float | None = None
|
|
259
|
+
deadline_minutes: float | None = None
|
|
260
|
+
allow_quantization: bool = True
|
|
261
|
+
allow_cpu_offload: bool = True
|
|
262
|
+
allow_nvme_offload: bool = False
|
|
263
|
+
</code></pre></div>
|
|
264
|
+
<p>Workload intents: <code>TransformerFineTune</code>, <code>PyTorchTraining</code>, <code>ClassicalML</code>,
|
|
265
|
+
<code>IndependentTasks</code>. See the planner walkthrough (in the repo under
|
|
266
|
+
<code>docs/planner/</code>) for the estimator arithmetic.</p>
|
|
267
|
+
<h3 id="flashrun-designed-not-yet-built"><code>flash.run</code> — designed, not yet built</h3>
|
|
268
|
+
<div class="codeblock"><button class="copybtn" type="button" aria-label="Copy code to clipboard">Copy</button><pre><code class="language-python">def run(plan, coordinator_url=None): ... # raises NotImplementedError
|
|
269
|
+
</code></pre></div>
|
|
270
|
+
<p>The plan-to-execution bridge. It is designed (the docstring describes the
|
|
271
|
+
intended pipeline) but not implemented, so it raises <code>NotImplementedError</code>
|
|
272
|
+
rather than half-running. Today you <code>flash.plan()</code> and submit the JobSpec
|
|
273
|
+
yourself.</p>
|
|
274
|
+
<hr />
|
|
275
|
+
<h2 id="related-references">Related references</h2>
|
|
276
|
+
<ul>
|
|
277
|
+
<li><a href="integrations.html">Integrations</a> — the <code>fr_torch</code> / <code>fr_sklearn</code> / <code>fr_hf</code>
|
|
278
|
+
adapters that build a <code>CommandWorkload</code> for you.</li>
|
|
279
|
+
<li><a href="torch-helper.html">torch helper</a> — the <code>flashruntime.torch</code> surface (three verbs + read-only accessors).</li>
|
|
280
|
+
<li><a href="cli.html">CLI</a> — the <code>flashruntime</code> command.</li>
|
|
281
|
+
</ul></main>
|
|
282
|
+
</div>
|
|
283
|
+
|
|
284
|
+
<script>
|
|
285
|
+
// ---- client-side search: fetch the builder's index, filter as you type -----
|
|
286
|
+
// (<=60 lines, vanilla JS, no external anything — the index is a sibling file.)
|
|
287
|
+
let INDEX = [];
|
|
288
|
+
// Path from THIS page back to the site root. The index stores root-relative
|
|
289
|
+
// URLs (e.g. "tutorials/deep.html"); we fetch it and build result links
|
|
290
|
+
// against ROOT so a page in a subdir resolves them too (see build_docs.py).
|
|
291
|
+
const ROOT = "../";
|
|
292
|
+
const q = document.getElementById("q");
|
|
293
|
+
const results = document.getElementById("results");
|
|
294
|
+
const esc = (s) => String(s).replace(/[&<>"]/g, (c) =>
|
|
295
|
+
({ "&": "&", "<": "<", ">": ">", '"': """ }[c]));
|
|
296
|
+
fetch(ROOT + "search-index.json").then((r) => r.json()).then((d) => { INDEX = d; }).catch(() => {});
|
|
297
|
+
|
|
298
|
+
// A short context window around the first match, so a hit shows WHY it matched.
|
|
299
|
+
function snippet(text, needle) {
|
|
300
|
+
const i = text.toLowerCase().indexOf(needle);
|
|
301
|
+
if (i < 0) return "";
|
|
302
|
+
const start = Math.max(0, i - 32);
|
|
303
|
+
return (start > 0 ? "…" : "") + text.slice(start, i + needle.length + 44).trim() + "…";
|
|
304
|
+
}
|
|
305
|
+
function runSearch() {
|
|
306
|
+
const needle = q.value.trim().toLowerCase();
|
|
307
|
+
if (!needle) { results.className = ""; results.innerHTML = ""; return; }
|
|
308
|
+
const hits = INDEX.map((p) => {
|
|
309
|
+
const inTitle = p.title.toLowerCase().includes(needle);
|
|
310
|
+
const inText = p.text.toLowerCase().includes(needle);
|
|
311
|
+
if (!inTitle && !inText) return null;
|
|
312
|
+
return { url: p.url, title: p.title, snip: inText ? snippet(p.text, needle) : "" };
|
|
313
|
+
}).filter(Boolean).slice(0, 20);
|
|
314
|
+
results.className = "open";
|
|
315
|
+
results.innerHTML = hits.length
|
|
316
|
+
? hits.map((h) => '<a href="' + ROOT + h.url + '"><b>' + esc(h.title) + "</b>" +
|
|
317
|
+
(h.snip ? "<span>" + esc(h.snip) + "</span>" : "") + "</a>").join("")
|
|
318
|
+
: '<div class="nohit">no matches</div>';
|
|
319
|
+
}
|
|
320
|
+
q.addEventListener("input", runSearch);
|
|
321
|
+
q.addEventListener("focus", runSearch);
|
|
322
|
+
document.addEventListener("click", (e) => {
|
|
323
|
+
if (!e.target.closest(".search")) { results.className = ""; } // dismiss on outside click
|
|
324
|
+
});
|
|
325
|
+
document.addEventListener("keydown", (e) => { // "/" focuses search, like PyTorch docs
|
|
326
|
+
if (e.key === "/" && document.activeElement !== q) { e.preventDefault(); q.focus(); }
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
// ---- copy buttons: read the <pre>'s own text into the clipboard ------------
|
|
330
|
+
document.querySelectorAll(".copybtn").forEach((btn) => {
|
|
331
|
+
btn.addEventListener("click", () => {
|
|
332
|
+
const code = btn.parentElement.querySelector("pre").innerText;
|
|
333
|
+
navigator.clipboard.writeText(code).then(() => {
|
|
334
|
+
const was = btn.textContent; btn.textContent = "Copied";
|
|
335
|
+
setTimeout(() => { btn.textContent = was; }, 1200);
|
|
336
|
+
}).catch(() => {});
|
|
337
|
+
});
|
|
338
|
+
});
|
|
339
|
+
</script>
|
|
340
|
+
</body>
|
|
341
|
+
</html>
|
|
@@ -0,0 +1,244 @@
|
|
|
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>Reference: torch helper (`flashruntime.torch`) — 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" href="../concepts/architecture.html">Architecture</a>
|
|
98
|
+
<div class="nav-section">Reference</div>
|
|
99
|
+
<a class="nav-link" href="sdk.html">Reference: SDK (`flashruntime`)</a>
|
|
100
|
+
<a class="nav-link" href="integrations.html">Reference: integrations (`flashruntime.integrations`)</a>
|
|
101
|
+
<a class="nav-link active" href="torch-helper.html">Reference: torch helper (`flashruntime.torch`)</a>
|
|
102
|
+
<a class="nav-link" href="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="reference-torch-helper-flashruntimetorch">Reference: torch helper (<code>flashruntime.torch</code>)</h1>
|
|
107
|
+
<p>The optional in-training-script helper: one import
|
|
108
|
+
(<code>import flashruntime.torch as ft</code>) makes a PyTorch script both
|
|
109
|
+
launch-anywhere and fault-tolerant. <code>torch</code> is imported <strong>inside</strong> these
|
|
110
|
+
functions only — FlashRuntime's core never depends on it.</p>
|
|
111
|
+
<p>The surface is three verbs plus read-only launch-fact accessors — a
|
|
112
|
+
deliberate guardrail (ADR-0003: do not rebuild Accelerate). The boundary is
|
|
113
|
+
<em>capability</em>, not count: there are no FSDP policies, no autocast, no
|
|
114
|
+
DeepSpeed config here, and there never will be. Signatures are exact; each
|
|
115
|
+
says, in one line, <em>why it exists</em>.</p>
|
|
116
|
+
<div class="codeblock"><button class="copybtn" type="button" aria-label="Copy code to clipboard">Copy</button><pre><code class="language-python">def prepare(model, optimizer=None, dataloader=None): ...
|
|
117
|
+
def checkpoint(model, optimizer=None, *, step, every=None) -> None: ...
|
|
118
|
+
def log_metrics(metrics: dict) -> None: ...
|
|
119
|
+
def start_step() -> int: ...
|
|
120
|
+
def rank() -> int: ...
|
|
121
|
+
def world_size() -> int: ...
|
|
122
|
+
def is_main() -> bool: ...
|
|
123
|
+
def device() -> str: ... # "cpu" or "cuda:N" — where prepare() put the model
|
|
124
|
+
def backend() -> str | None: ... # "gloo"/"nccl", None when single-process
|
|
125
|
+
</code></pre></div>
|
|
126
|
+
<hr />
|
|
127
|
+
<h3 id="preparemodel-optimizernone-dataloadernone"><code>prepare(model, optimizer=None, dataloader=None)</code></h3>
|
|
128
|
+
<p>Wires distributed execution and restores the newest valid checkpoint; returns
|
|
129
|
+
the possibly-wrapped <code>(model, optimizer, dataloader)</code> triple.</p>
|
|
130
|
+
<ul>
|
|
131
|
+
<li>Launched distributed (<code>WORLD_SIZE > 1</code>): initializes torch's own process
|
|
132
|
+
group (<code>nccl</code> on GPU, <code>gloo</code> on CPU), wraps the model in
|
|
133
|
+
<code>DistributedDataParallel</code>, and swaps the DataLoader's sampler for a seed-0
|
|
134
|
+
<code>DistributedSampler</code> (each rank sees a disjoint, deterministic shard).</li>
|
|
135
|
+
<li>On CUDA: selects <code>nccl</code>, moves the model onto this rank's GPU <strong>before</strong> the
|
|
136
|
+
DDP wrap, and binds <code>device_ids</code> / <code>output_device</code> — you no longer call
|
|
137
|
+
<code>model.to(device)</code>.</li>
|
|
138
|
+
<li>Restores the newest <strong>valid</strong> checkpoint manifest if one exists, setting the
|
|
139
|
+
resume step (read it with <code>start_step()</code>).</li>
|
|
140
|
+
<li>Launched as plain <code>python train.py</code>: a no-op passthrough.</li>
|
|
141
|
+
</ul>
|
|
142
|
+
<blockquote>
|
|
143
|
+
<p>The rebuilt DataLoader carries over <code>batch_size</code>, <code>collate_fn</code>,
|
|
144
|
+
<code>num_workers</code>, and <code>drop_last</code>. <strong><code>shuffle</code> and <code>pin_memory</code> are not carried
|
|
145
|
+
over</strong> — the seed-0 <code>DistributedSampler</code> owns shuffling.</p>
|
|
146
|
+
</blockquote>
|
|
147
|
+
<h3 id="checkpointmodel-optimizernone-step-everynone"><code>checkpoint(model, optimizer=None, *, step, every=None)</code></h3>
|
|
148
|
+
<p>rank 0 writes a resumable checkpoint under the <strong>parts-first / manifest-last</strong>
|
|
149
|
+
contract (the manifest is written last, so a half-written checkpoint is never
|
|
150
|
+
<code>latest_valid</code>). <code>every=N</code> no-ops except on multiples of N. Every rank
|
|
151
|
+
synchronizes on a barrier so no one races past a partial write.</p>
|
|
152
|
+
<h3 id="log_metricsmetrics-dict"><code>log_metrics(metrics: dict)</code></h3>
|
|
153
|
+
<p>rank 0 appends one JSON record per call to <code>metrics.jsonl</code> — the streaming
|
|
154
|
+
history the live page draws as a loss curve. <strong>Never raises</strong> (metrics must
|
|
155
|
+
never kill training). Separate from the final <code>metrics.json</code> your script writes
|
|
156
|
+
for <code>run.trials</code>.</p>
|
|
157
|
+
<h3 id="start_step-int"><code>start_step() -> int</code></h3>
|
|
158
|
+
<p>The first step the loop should run: <code>0</code> fresh, <code>>0</code> after a resume (set by
|
|
159
|
+
<code>prepare()</code> when it restores a checkpoint). Start your loop from it.</p>
|
|
160
|
+
<h3 id="rank-world_size-is_main"><code>rank()</code> / <code>world_size()</code> / <code>is_main()</code></h3>
|
|
161
|
+
<p>The small positional helpers, read from the launch environment: <code>rank()</code> is
|
|
162
|
+
this process's <code>RANK</code> (default <code>0</code>), <code>world_size()</code> is <code>WORLD_SIZE</code> (default
|
|
163
|
+
<code>1</code>), and <code>is_main()</code> is <code>rank() == 0</code> — guard rank-0-only work (writing the
|
|
164
|
+
final <code>metrics.json</code>, printing) with it.</p>
|
|
165
|
+
<hr />
|
|
166
|
+
<h2 id="usage-shape">Usage shape</h2>
|
|
167
|
+
<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
|
|
168
|
+
|
|
169
|
+
model, optimizer, loader = ft.prepare(model, optimizer, loader)
|
|
170
|
+
start = ft.start_step()
|
|
171
|
+
|
|
172
|
+
step = start
|
|
173
|
+
while step < total_steps:
|
|
174
|
+
for x, y in loader:
|
|
175
|
+
...
|
|
176
|
+
step += 1
|
|
177
|
+
ft.checkpoint(model, optimizer, step=step, every=100)
|
|
178
|
+
ft.log_metrics({"step": step, "loss": float(loss)})
|
|
179
|
+
|
|
180
|
+
if ft.is_main():
|
|
181
|
+
... # write the final metrics.json
|
|
182
|
+
</code></pre></div>
|
|
183
|
+
<p>The full worked example is the <a href="../tutorials/convnet.html">ConvNet tutorial</a>; the
|
|
184
|
+
launch side is the <a href="../guides/pytorch.html">PyTorch guide</a>.</p></main>
|
|
185
|
+
</div>
|
|
186
|
+
|
|
187
|
+
<script>
|
|
188
|
+
// ---- client-side search: fetch the builder's index, filter as you type -----
|
|
189
|
+
// (<=60 lines, vanilla JS, no external anything — the index is a sibling file.)
|
|
190
|
+
let INDEX = [];
|
|
191
|
+
// Path from THIS page back to the site root. The index stores root-relative
|
|
192
|
+
// URLs (e.g. "tutorials/deep.html"); we fetch it and build result links
|
|
193
|
+
// against ROOT so a page in a subdir resolves them too (see build_docs.py).
|
|
194
|
+
const ROOT = "../";
|
|
195
|
+
const q = document.getElementById("q");
|
|
196
|
+
const results = document.getElementById("results");
|
|
197
|
+
const esc = (s) => String(s).replace(/[&<>"]/g, (c) =>
|
|
198
|
+
({ "&": "&", "<": "<", ">": ">", '"': """ }[c]));
|
|
199
|
+
fetch(ROOT + "search-index.json").then((r) => r.json()).then((d) => { INDEX = d; }).catch(() => {});
|
|
200
|
+
|
|
201
|
+
// A short context window around the first match, so a hit shows WHY it matched.
|
|
202
|
+
function snippet(text, needle) {
|
|
203
|
+
const i = text.toLowerCase().indexOf(needle);
|
|
204
|
+
if (i < 0) return "";
|
|
205
|
+
const start = Math.max(0, i - 32);
|
|
206
|
+
return (start > 0 ? "…" : "") + text.slice(start, i + needle.length + 44).trim() + "…";
|
|
207
|
+
}
|
|
208
|
+
function runSearch() {
|
|
209
|
+
const needle = q.value.trim().toLowerCase();
|
|
210
|
+
if (!needle) { results.className = ""; results.innerHTML = ""; return; }
|
|
211
|
+
const hits = INDEX.map((p) => {
|
|
212
|
+
const inTitle = p.title.toLowerCase().includes(needle);
|
|
213
|
+
const inText = p.text.toLowerCase().includes(needle);
|
|
214
|
+
if (!inTitle && !inText) return null;
|
|
215
|
+
return { url: p.url, title: p.title, snip: inText ? snippet(p.text, needle) : "" };
|
|
216
|
+
}).filter(Boolean).slice(0, 20);
|
|
217
|
+
results.className = "open";
|
|
218
|
+
results.innerHTML = hits.length
|
|
219
|
+
? hits.map((h) => '<a href="' + ROOT + h.url + '"><b>' + esc(h.title) + "</b>" +
|
|
220
|
+
(h.snip ? "<span>" + esc(h.snip) + "</span>" : "") + "</a>").join("")
|
|
221
|
+
: '<div class="nohit">no matches</div>';
|
|
222
|
+
}
|
|
223
|
+
q.addEventListener("input", runSearch);
|
|
224
|
+
q.addEventListener("focus", runSearch);
|
|
225
|
+
document.addEventListener("click", (e) => {
|
|
226
|
+
if (!e.target.closest(".search")) { results.className = ""; } // dismiss on outside click
|
|
227
|
+
});
|
|
228
|
+
document.addEventListener("keydown", (e) => { // "/" focuses search, like PyTorch docs
|
|
229
|
+
if (e.key === "/" && document.activeElement !== q) { e.preventDefault(); q.focus(); }
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
// ---- copy buttons: read the <pre>'s own text into the clipboard ------------
|
|
233
|
+
document.querySelectorAll(".copybtn").forEach((btn) => {
|
|
234
|
+
btn.addEventListener("click", () => {
|
|
235
|
+
const code = btn.parentElement.querySelector("pre").innerText;
|
|
236
|
+
navigator.clipboard.writeText(code).then(() => {
|
|
237
|
+
const was = btn.textContent; btn.textContent = "Copied";
|
|
238
|
+
setTimeout(() => { btn.textContent = was; }, 1200);
|
|
239
|
+
}).catch(() => {});
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
</script>
|
|
243
|
+
</body>
|
|
244
|
+
</html>
|