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.
Files changed (95) hide show
  1. flashml_workloads/__init__.py +7 -0
  2. flashml_workloads/fedavg_driver.py +569 -0
  3. flashml_workloads/fedavg_weights.py +223 -0
  4. flashml_workloads/fedavg_worker.py +166 -0
  5. flashml_workloads/kmeans_driver.py +134 -0
  6. flashml_workloads/kmeans_shard.py +69 -0
  7. flashml_workloads/sgd_trainer.py +127 -0
  8. flashml_workloads/sharded_kmeans.py +323 -0
  9. flashml_workloads/sklearn_trial.py +89 -0
  10. flashruntime/__init__.py +125 -0
  11. flashruntime/artifacts/__init__.py +25 -0
  12. flashruntime/artifacts/store.py +228 -0
  13. flashruntime/backends/__init__.py +26 -0
  14. flashruntime/backends/base.py +63 -0
  15. flashruntime/backends/kuberay.py +465 -0
  16. flashruntime/checkpoint/__init__.py +20 -0
  17. flashruntime/checkpoint/catalog.py +198 -0
  18. flashruntime/checkpoint/local.py +109 -0
  19. flashruntime/checkpoint/store.py +86 -0
  20. flashruntime/integrations/__init__.py +5 -0
  21. flashruntime/integrations/huggingface.py +59 -0
  22. flashruntime/integrations/pytorch.py +52 -0
  23. flashruntime/integrations/sklearn.py +42 -0
  24. flashruntime/launchers/__init__.py +130 -0
  25. flashruntime/launchers/local.py +126 -0
  26. flashruntime/leases/__init__.py +27 -0
  27. flashruntime/leases/manager.py +365 -0
  28. flashruntime/leases/sqlite_store.py +169 -0
  29. flashruntime/leases/store.py +103 -0
  30. flashruntime/monitor/__init__.py +7 -0
  31. flashruntime/monitor/sampler.py +232 -0
  32. flashruntime/planner/__init__.py +56 -0
  33. flashruntime/planner/candidates.py +597 -0
  34. flashruntime/planner/catalog.py +129 -0
  35. flashruntime/planner/comm.py +95 -0
  36. flashruntime/planner/explain.py +109 -0
  37. flashruntime/planner/memory.py +166 -0
  38. flashruntime/planner/resolve.py +120 -0
  39. flashruntime/planner/selector.py +169 -0
  40. flashruntime/planner/timecost.py +81 -0
  41. flashruntime/profiling/__init__.py +113 -0
  42. flashruntime/protocol/__init__.py +18 -0
  43. flashruntime/protocol/plan_v1alpha1.py +320 -0
  44. flashruntime/protocol/v1alpha1.py +465 -0
  45. flashruntime/providers/__init__.py +138 -0
  46. flashruntime/py.typed +0 -0
  47. flashruntime/recipes/__init__.py +135 -0
  48. flashruntime/recipes/command.py +166 -0
  49. flashruntime/recovery/__init__.py +21 -0
  50. flashruntime/recovery/policy.py +170 -0
  51. flashruntime/recovery/signals.py +135 -0
  52. flashruntime/recovery/taxonomy.py +91 -0
  53. flashruntime/scheduler/__init__.py +170 -0
  54. flashruntime/sdk.py +402 -0
  55. flashruntime/service/__init__.py +3 -0
  56. flashruntime/service/app.py +391 -0
  57. flashruntime/service/auth.py +180 -0
  58. flashruntime/service/checkpoints.py +90 -0
  59. flashruntime/service/cli.py +167 -0
  60. flashruntime/service/dashboard.py +193 -0
  61. flashruntime/service/ledger.py +101 -0
  62. flashruntime/service/modea.py +821 -0
  63. flashruntime/strategies/__init__.py +156 -0
  64. flashruntime/strategies/command.py +56 -0
  65. flashruntime/torch/__init__.py +274 -0
  66. flashruntime/viewer/__init__.py +20 -0
  67. flashruntime/viewer/_docs/benchmarks.html +771 -0
  68. flashruntime/viewer/_docs/concepts/architecture.html +302 -0
  69. flashruntime/viewer/_docs/get-started.html +263 -0
  70. flashruntime/viewer/_docs/guides/federated-averaging.html +363 -0
  71. flashruntime/viewer/_docs/guides/huggingface.html +223 -0
  72. flashruntime/viewer/_docs/guides/jobspec-and-isolation.html +271 -0
  73. flashruntime/viewer/_docs/guides/pytorch.html +313 -0
  74. flashruntime/viewer/_docs/guides/sklearn.html +232 -0
  75. flashruntime/viewer/_docs/index.html +251 -0
  76. flashruntime/viewer/_docs/reference/cli.html +254 -0
  77. flashruntime/viewer/_docs/reference/integrations.html +240 -0
  78. flashruntime/viewer/_docs/reference/sdk.html +341 -0
  79. flashruntime/viewer/_docs/reference/torch-helper.html +244 -0
  80. flashruntime/viewer/_docs/search-index.json +1 -0
  81. flashruntime/viewer/_docs/tutorials/convnet.html +571 -0
  82. flashruntime/viewer/_docs/tutorials/fault-tolerance.html +375 -0
  83. flashruntime/viewer/_docs/tutorials/sklearn-sweeps.html +278 -0
  84. flashruntime/viewer/flowmap.py +307 -0
  85. flashruntime/viewer/page.py +594 -0
  86. flashruntime/viewer/server.py +134 -0
  87. flashruntime/viewer/state.py +250 -0
  88. flashruntime/workloads/__init__.py +6 -0
  89. flashruntime/workloads/command.py +127 -0
  90. flashruntime-0.3.0.dist-info/METADATA +365 -0
  91. flashruntime-0.3.0.dist-info/RECORD +95 -0
  92. flashruntime-0.3.0.dist-info/WHEEL +5 -0
  93. flashruntime-0.3.0.dist-info/entry_points.txt +2 -0
  94. flashruntime-0.3.0.dist-info/licenses/LICENSE +202 -0
  95. 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 &amp; 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 &amp; 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 &amp; 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=&quot;my-sweep&quot;,
122
+ image=ImageSpec(repository=&quot;myrepo/trainer&quot;, tag=&quot;2026.07-a1b2c3&quot;),
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 &gt; 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
+ ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" }[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 &amp; 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
+ &quot;train.py&quot;,
126
+ source=&quot;examples/user_pytorch_vanilla&quot;,
127
+ nproc_per_node=2, # 2 processes on this host — gloo/CPU works, no GPU
128
+ script_args=&quot;--steps 100&quot;,
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 &lt;script&gt; &lt;args&gt;</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 &gt; 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, &gt;0 after a resume
150
+ ...
151
+ ft.checkpoint(model, optimizer, step=step, every=100)
152
+ ft.log_metrics({&quot;step&quot;: step, &quot;loss&quot;: 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 &gt; 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 &gt; 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
+ ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" }[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>