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,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 &amp; 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 &quot;latest_valid&quot;
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 &gt; node &gt; ───► worker_crash + coordinated
208
+ heartbeat loss, process &gt; 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 &amp; 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
+ ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" }[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 &amp; 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 &quot;flashruntime[service]&quot; # the FastAPI coordinator + CLI
118
+ pip install &quot;flashruntime[sklearn]&quot; # 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=&quot;python -c \&quot;import json; json.dump({'accuracy': 0.91}, open('metrics.json','w'))\&quot;&quot;,
134
+ source=flash.Source(path=&quot;.&quot;),
135
+ ))
136
+
137
+ print(run.state.value) # &quot;SUCCEEDED&quot;
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
+ &quot;train.py&quot;,
155
+ source=&quot;examples/user_pytorch_vanilla&quot;,
156
+ nproc_per_node=2, # 2 processes on this host — gloo/CPU works
157
+ script_args=&quot;--steps 100&quot;,
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 &lt;script&gt; &lt;args&gt;</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 &gt; 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=&quot;python train.py&quot;, source=flash.Source(path=&quot;.&quot;)),
196
+ watch=True,
197
+ )
198
+ print(run.viewer_url) # http://127.0.0.1:&lt;port&gt;
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
+ ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" }[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>