gpu-compute-provider 0.4.2__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 (84) hide show
  1. gpu_compute_provider-0.4.2.dist-info/METADATA +310 -0
  2. gpu_compute_provider-0.4.2.dist-info/RECORD +84 -0
  3. gpu_compute_provider-0.4.2.dist-info/WHEEL +5 -0
  4. gpu_compute_provider-0.4.2.dist-info/entry_points.txt +2 -0
  5. gpu_compute_provider-0.4.2.dist-info/licenses/LICENSE +202 -0
  6. gpu_compute_provider-0.4.2.dist-info/licenses/NOTICE +4 -0
  7. gpu_compute_provider-0.4.2.dist-info/top_level.txt +1 -0
  8. supercomputer/SKILL.md +1565 -0
  9. supercomputer/__init__.py +110 -0
  10. supercomputer/__main__.py +153 -0
  11. supercomputer/_direct_bridge.py +1882 -0
  12. supercomputer/_modal_runner.py +101 -0
  13. supercomputer/_quiet.py +9 -0
  14. supercomputer/autoresearch/SETUP.md +67 -0
  15. supercomputer/autoresearch/program.template.md +159 -0
  16. supercomputer/billing_types.py +298 -0
  17. supercomputer/cli/__init__.py +27 -0
  18. supercomputer/cli/attach.py +251 -0
  19. supercomputer/cli/autoresearch.py +36 -0
  20. supercomputer/cli/common.py +499 -0
  21. supercomputer/cli/config_cmd.py +131 -0
  22. supercomputer/cli/console.py +226 -0
  23. supercomputer/cli/context.py +251 -0
  24. supercomputer/cli/credits.py +283 -0
  25. supercomputer/cli/credits_buy.py +115 -0
  26. supercomputer/cli/credits_render.py +133 -0
  27. supercomputer/cli/exec_cmd.py +100 -0
  28. supercomputer/cli/files.py +321 -0
  29. supercomputer/cli/hardware.py +94 -0
  30. supercomputer/cli/help.py +163 -0
  31. supercomputer/cli/interactive.py +148 -0
  32. supercomputer/cli/job.py +235 -0
  33. supercomputer/cli/job_wait.py +170 -0
  34. supercomputer/cli/main.py +237 -0
  35. supercomputer/cli/migration.py +590 -0
  36. supercomputer/cli/mount.py +150 -0
  37. supercomputer/cli/output.py +45 -0
  38. supercomputer/cli/processes.py +602 -0
  39. supercomputer/cli/registry.py +28 -0
  40. supercomputer/cli/run.py +542 -0
  41. supercomputer/cli/sessions.py +829 -0
  42. supercomputer/cli/setup.py +100 -0
  43. supercomputer/cli/ssh.py +257 -0
  44. supercomputer/cli/stop_outcome.py +158 -0
  45. supercomputer/cli/sweep.py +1059 -0
  46. supercomputer/cli/sweep_harvest.py +206 -0
  47. supercomputer/cli/sweep_watch.py +175 -0
  48. supercomputer/cli/usage.py +69 -0
  49. supercomputer/cli/volume.py +323 -0
  50. supercomputer/cli/wait.py +325 -0
  51. supercomputer/cli_args.py +369 -0
  52. supercomputer/client.py +718 -0
  53. supercomputer/client_billing.py +112 -0
  54. supercomputer/client_projects.py +52 -0
  55. supercomputer/client_sessions.py +111 -0
  56. supercomputer/client_volumes.py +80 -0
  57. supercomputer/config.py +553 -0
  58. supercomputer/direct.py +576 -0
  59. supercomputer/errors.py +153 -0
  60. supercomputer/fileset.py +170 -0
  61. supercomputer/image.py +532 -0
  62. supercomputer/job.py +260 -0
  63. supercomputer/log_cursor.py +66 -0
  64. supercomputer/login.py +236 -0
  65. supercomputer/modal_compat.py +307 -0
  66. supercomputer/modal_objects.py +156 -0
  67. supercomputer/modal_run.py +93 -0
  68. supercomputer/process_input.py +67 -0
  69. supercomputer/providers/__init__.py +5 -0
  70. supercomputer/providers/requests_http.py +68 -0
  71. supercomputer/py.typed +0 -0
  72. supercomputer/session.py +474 -0
  73. supercomputer/setup_cmd.py +255 -0
  74. supercomputer/shell.py +569 -0
  75. supercomputer/sku_names.py +110 -0
  76. supercomputer/sweeps.py +608 -0
  77. supercomputer/tunnel.py +198 -0
  78. supercomputer/tunnel_relay.py +42 -0
  79. supercomputer/types.py +362 -0
  80. supercomputer/v2_connect.py +1117 -0
  81. supercomputer/verified_fetch.py +126 -0
  82. supercomputer/version.py +33 -0
  83. supercomputer/volume_wait.py +131 -0
  84. supercomputer/volumes.py +224 -0
supercomputer/SKILL.md ADDED
@@ -0,0 +1,1565 @@
1
+ ---
2
+ name: supercomputer
3
+ description: >-
4
+ Use supercomputer to provision GPU or CPU hosts, run remote commands and scripts,
5
+ manage persistent sessions, transfer files, and monitor long-running jobs from
6
+ agent workflows.
7
+ ---
8
+
9
+ # supercomputer — GPU compute from the command line
10
+
11
+ supercomputer provisions GPU and CPU hosts, runs code on them, and transfers files — all
12
+ from the terminal. It works with any AI coding agent (Cursor, Claude Code, Codex,
13
+ etc.) so your agent can provision hardware, run experiments, and pull results
14
+ autonomously.
15
+
16
+ Package: `supercomputer` | CLI command: `super` | Python 3.9+
17
+
18
+ ---
19
+
20
+ ## Setup
21
+
22
+ ```bash
23
+ pip install gpu-compute-provider
24
+ super set_token <api_key> # one-time; saves to ~/.supercomputer/token
25
+ super health # verify connectivity
26
+ ```
27
+
28
+ (A human sets up with `super login`, which signs in through the browser and
29
+ mints a key for the machine; agents use `set_token` with a key they were given.)
30
+
31
+ Token resolution order:
32
+
33
+ 1. `api_key=` parameter to `Client()`
34
+ 2. `SUPERCOMPUTER_API_KEY` environment variable
35
+ 3. Saved token in `~/.supercomputer/token`
36
+ 4. `SUPERCOMPUTER_API_KEY` from the nearest `.env` file
37
+
38
+ (For a human at a terminal, plain `super set_token` with no argument prompts
39
+ with hidden input, keeping the key out of shell history.)
40
+
41
+ ---
42
+
43
+ ## Quick Start
44
+
45
+ ### Run a script on a GPU (throwaway session)
46
+
47
+ ```bash
48
+ # Upload a script, run it on an L4, print output, tear down
49
+ super run train.py --sku gpu_1x_l4
50
+
51
+ # Upload a directory, run a specific script, pass arguments
52
+ super run . --script train.py --sku gpu_1x_l4 -- --epochs 50 --lr 1e-4
53
+
54
+ # Run a one-off command (no file upload)
55
+ super exec --sku gpu_1x_l4 nvidia-smi -L
56
+ ```
57
+
58
+ ### Choosing a SKU
59
+
60
+ Request hardware by passing `--sku` to `exec`/`run`/`create`. SKUs follow the
61
+ pattern `gpu_<count>x_<model>` or `cpu_<count>x_<family>`: `gpu_1x_l4`,
62
+ `gpu_8x_h100`, `cpu_2x_epyc`, etc. Run `super skus` (or `super skus --json`)
63
+ for the live list with availability and the rate each SKU was last billed at.
64
+ If a SKU has no available capacity, the session queues until a host is
65
+ provisioned.
66
+
67
+ ---
68
+
69
+ ## CLI Reference
70
+
71
+ ### Global Flags
72
+
73
+
74
+ | Flag | Description |
75
+ | -------- | --------------------------------------------------------------------- |
76
+ | `--json` | Machine-readable JSON output to stdout. Status messages go to stderr. |
77
+
78
+
79
+ Use `--json` in agent workflows so output can be parsed programmatically. Place
80
+ `--json` before your remote command (e.g. `super exec --json -- python app.py`):
81
+ flags after the command — or after a `--` — belong to *your* program, so
82
+ `super exec -- python app.py --json` passes `--json` to `app.py`, not to super.
83
+ Unknown flags are rejected (e.g. a typo'd `--limitt`) rather than silently
84
+ ignored.
85
+
86
+ ### Infrastructure
87
+
88
+
89
+ | Command | Description |
90
+ | ---------------------- | ----------------------------------------- |
91
+ | `super health` | Check API connectivity |
92
+ | `super skus` | List SKUs: availability, `MAX` concurrency, last-billed `$/HR` (`cost_per_hr` in `--json`; spot, so it moves; `-` = not billed in 7 days) |
93
+ | `super set_token <key>` | Save API key |
94
+ | `super credits` | Prepaid balance, auto-refill, card |
95
+ | `super credits buy <usd>` | Buy credit (opens hosted checkout) |
96
+
97
+
98
+ ### Execution
99
+
100
+
101
+ | Command | Description |
102
+ | ----------------------------------------------------------------------------- | ----------------------- |
103
+ | `super run [SID] [--upload PATH]... [--detach] [--retries N] [--wait DUR] -- CMD` | Run a command; upload files first if needed |
104
+ | `super run FILE\|DIR [--script NAME] [-- args...]` | Existing upload-and-run form |
105
+ | `super exec [SID] [flags] -- CMD` | Run command spelling with a 30-second default wait |
106
+ | `super spawn [--sku SKU \| --on SID] [flags] -- CMD` | Start a command detached |
107
+ | `super upload [--on SID] <local> [remote]` / `super download [--on SID] <remote> [local]` | Transfer files (session via `--on` or positional) |
108
+
109
+
110
+ Without a session ID, `run` creates a session and gives its box back when the
111
+ command ends. Its `/workspace` remains saved and downloadable. Use `--on SID`
112
+ or a positional SID to run in an existing session; it stays available for later
113
+ commands. `exec` is the same command as `run`, except its wait defaults to 30s
114
+ instead of forever. `--include` is an alias of `--upload`; old command forms
115
+ still work.
116
+
117
+ Cached images complete in ~2s. Cold builds (first run on a host) take 30–120s.
118
+
119
+ ### Exec Semantics
120
+
121
+ `super exec` is optimized for agents and ML workloads. It starts commands as
122
+ managed processes and waits for a foreground result for a configurable budget.
123
+
124
+ **Working directory.** Commands run from `/workspace` by default — the same
125
+ place `super upload` puts your files. So an uploaded `train.py` runs as
126
+ `python train.py` (relative path, no `cd` and no need to spell out
127
+ `/workspace/train.py`), and commands that write to relative paths also land in
128
+ `/workspace` and persist. Do **not** assume a different default (e.g. `/root`)
129
+ or hardcode `/root/...` paths — that's the single most common cwd mistake. To
130
+ run elsewhere for one command, `cd` inside it: `sh -c 'cd /some/dir && ...'`.
131
+ This applies uniformly to `exec`, `run`, `spawn`, and `shell`.
132
+
133
+ ```bash
134
+ # Default wait budget is 30s
135
+ super exec --on s-abc123 python3 /workspace/train.py
136
+
137
+ # Wait longer before detaching
138
+ super exec --on s-abc123 --wait 10m python3 /workspace/train.py
139
+
140
+ # Positional session alias for --on
141
+ super exec s-abc123 --wait 10m -- python3 /workspace/train.py
142
+
143
+ # Alias for --wait, useful when an agent thinks in timeouts
144
+ super exec --on s-abc123 --timeout 10m python3 /workspace/train.py
145
+ ```
146
+
147
+ If the command exits within `--wait`, `exec` behaves like a normal foreground
148
+ command: stdout/stderr are printed and the CLI exits with the remote return code.
149
+
150
+ If the command is still running when the wait budget expires, `exec` does **not**
151
+ kill it. It detaches, prints a process ID on stderr and exits **75** (exit 0 only
152
+ for `--wait 0`, which detaches immediately):
153
+
154
+ ```bash
155
+ Command is still running; use super logs s-abc123 proc-456 --follow
156
+ ```
157
+
158
+ In `--json` mode, a detached command returns `status: "running"`,
159
+ `returncode: null`, plus `session_id` and `process_id`. Agents should treat that
160
+ (and its exit code 75) as success-with-handle, not as command failure, then block on it with
161
+ `super wait <session_id> <process_id>` (returns the real exit code + tail) or
162
+ tail it with `super logs --follow`.
163
+
164
+ ```json
165
+ {
166
+ "stdout": "",
167
+ "stderr": "",
168
+ "returncode": null,
169
+ "status": "running",
170
+ "session_id": "s-abc123",
171
+ "process_id": "proc-456",
172
+ "message": "Command is still running; use super logs s-abc123 proc-456 --follow"
173
+ }
174
+ ```
175
+
176
+ `exec` returns a bounded stdout payload by default so a noisy command cannot flood
177
+ the caller. If the JSON response has `stdout_truncated: true`, do **not** rerun the
178
+ job just to recover logs. Use the returned `session_id` and `process_id`:
179
+
180
+ ```bash
181
+ # Fetch the durable combined process log captured by the host
182
+ super logs s-abc123 proc-456 --output all
183
+
184
+ # Fetch a specific range or stream
185
+ super logs s-abc123 proc-456 --output head --output-bytes 128k
186
+ super logs s-abc123 proc-456 --stream stderr --output all
187
+ ```
188
+
189
+ > The default stream is **combined** (stdout + stderr) — prefer it. Most ML
190
+ > tooling (tqdm progress bars, HuggingFace/TRL logs) writes to **stderr**, so
191
+ > `--stream stdout` on a training job often looks empty even though the job is
192
+ > producing output. Reach for `--stream stdout`/`stderr` only when you
193
+ > specifically need one split.
194
+
195
+ **Liveness while polling a long job:** `super logs <sid> <pid> --json` includes
196
+ `last_write_ts` (when the durable log was last written) and `session_status`.
197
+ A `status: "running"` process whose `last_write_ts` stops advancing across
198
+ polls is stalled or on a lost host — no need to diff `output_total_bytes`
199
+ between polls. A `status: "lost_host"` (with `process_alive: false`) means the
200
+ session ended without the process's exit ever being recorded: the host was
201
+ lost, the tail is final, don't keep polling.
202
+
203
+ SDK equivalent:
204
+
205
+ ```python
206
+ log = session.logs("proc-456", output_range="all")
207
+ print(log.output)
208
+
209
+ stderr = session.logs("proc-456", stream="stderr", output_range="all")
210
+ ```
211
+
212
+ Persistent-session commands (`--on <sid>`) print a one-line `✓ ready` to
213
+ stderr on every attach; fuller provision/setup narration appears only when
214
+ the session actually has setup work to do. Terminal/problem statuses also
215
+ print to stderr. Parse stdout (or use `--json`) — never stderr.
216
+
217
+ #### Shell Parsing Rules
218
+
219
+ Prefer passing commands as normal argv:
220
+
221
+ ```bash
222
+ super exec --on s-abc123 ls /workspace
223
+ super exec s-abc123 -- ls /workspace
224
+ super exec --on s-abc123 python3 /workspace/train.py --epochs 10
225
+ ```
226
+
227
+ A single quoted string is treated as shell text (it runs via `bash -lc`),
228
+ so both of these work and are equivalent:
229
+
230
+ ```bash
231
+ super exec --on s-abc123 'ls /workspace'
232
+ super exec --on s-abc123 'echo one two && echo piped | tr a-z A-Z'
233
+ ```
234
+
235
+ When you need shell syntax (`;`, `&&`, pipes, redirects, variables), either pass a
236
+ single quoted shell expression or be explicit with `sh -c`:
237
+
238
+ ```bash
239
+ super exec --on s-abc123 'cd /workspace; ls'
240
+ super exec --on s-abc123 -- sh -c 'sleep 45; echo done'
241
+ ```
242
+
243
+ Remember that unquoted shell metacharacters are handled by the local shell before
244
+ `super` sees them:
245
+
246
+ ```bash
247
+ # Runs "sleep 45" remotely, then "echo done" locally
248
+ super exec --on s-abc123 sleep 45; echo done
249
+ ```
250
+
251
+ ### Jobs (`super job`) — run to completion
252
+
253
+ `super job run` runs like `super run`, except for what `--retries` counts. It keeps `/workspace` on the
254
+ session, so you can inspect or download files after the command ends. A new
255
+ session gives its box back when the command ends. `--retries N` re-runs only
256
+ after a lost box; a command's non-zero exit is final. (`super run --retries N`
257
+ instead re-runs a command that exits non-zero, after a short growing pause — except
258
+ exit 126/127 and an OOM kill, which a re-run on the same box cannot change.) With mounted volumes,
259
+ the attached command waits for their commit before returning. Use `--detach`
260
+ to get a handle immediately, then follow its logs or wait for completion
261
+ (`--detach` cannot be combined with `--retries`). For work that may be
262
+ re-run, save checkpoints in `/workspace` and make the command load them on start:
263
+ a retry runs in the same session and `/workspace`. Only when the session itself
264
+ ended (failed or dead) does the retry start a new session with an empty
265
+ `/workspace` (uploads are sent again; stderr says so) — checkpoints that must
266
+ survive that belong on a volume. A job closed on request (`super job kill`,
267
+ `super stop`) is never retried.
268
+
269
+ | Command | Description |
270
+ |---|---|
271
+ | `super job run [flags] -- CMD` | Run on a session, keep `/workspace`, retry up to `--retries N` |
272
+ | `super job ls [--all] [--limit N]` | List jobs (`--all` includes finished ones) |
273
+ | `super job logs <job-id> [<pid>] [--follow]` | No pid: list the job's processes. With a pid: its durable output (`--follow` for live) |
274
+ | `super job wait <job-id> [--timeout DUR]` | Block until the job ends; exits with its command's exit code (`69` host lost, `75` still running at `--timeout`) |
275
+ | `super job kill <job-id>` | Stop the job's session: billing stops at once; `/workspace` is saved by the teardown that follows (`super stop <job-id> --wait` confirms) |
276
+
277
+ `job run` accepts the `run` flags: `--sku SKU`, `--volume NAME[:MOUNT]`,
278
+ `--upload PATH` (also `--include`), `--env K=V`,
279
+ `--secret KEY[=VALUE]`, `--retries N`, `--min-disk-gb N`, and `--detach`.
280
+ After preemption, follow the same job ID with `super job wait` or
281
+ `super job logs`; do not submit a duplicate run. For example:
282
+
283
+ ```bash
284
+ super job run --sku gpu_1x_l4 --upload train.py --retries 2 -- \
285
+ python train.py --out /workspace/model.pt
286
+ super download <session-id> /workspace/model.pt ./model.pt
287
+ ```
288
+
289
+ The SDK's `project.job` remains an ephemeral job: its outputs live in volumes
290
+ and logs. The CLI `super job run` uses a saved workspace, like `super run`.
291
+
292
+ SDK example:
293
+
294
+ ```python
295
+ job = project.job("python train.py --out /results/model.pt",
296
+ sku="gpu_1x_l4",
297
+ volume_mounts=[{"name": "results", "mount_path": "/results"}],
298
+ include=["./src"], retries=1)
299
+ log = job.wait() # ProcessLog: exit_code, output
300
+
301
+ client.list_volume_files("results") # [{path, size, ...}]
302
+ data = client.read_volume_file("results", "model.pt") # bytes, no session
303
+ client.download_volume_file("results", "model.pt", "./model.pt")
304
+ client.delete_volume("results") # when you're done with it
305
+ ```
306
+
307
+ Use `job run` for a command with a retry budget; use a persistent session when
308
+ you want to run several commands against the same `/workspace`.
309
+
310
+ ### Persistent Sessions
311
+
312
+
313
+ | Command | Description |
314
+ | ----------------------------- | ---------------------------------------------------- |
315
+ | `super create [--sku SKU]` | Create a session (cold — $0, no host, until first used) |
316
+ | `super sessions [--all] [--limit N] [--json]` | List sessions and spending; JSON includes `restart_check` after a restart |
317
+ | `super usage [--since 24h]` | What the account has been charged over a window (default 30 days), by resource and session |
318
+ | `super stop <session-id> [--wait]` | Stop a session now: billing stops when the stop is accepted; files are saved by the final sync during teardown. `--wait` blocks until teardown settles and reports a failed final save when the server records one for this stop: it exits `1` naming `close_flush_incomplete` (the final sync did not finish) or `volume_commit_failed`. A state loss recorded before this stop is mentioned, not failed. It also exits `1` if the session ends `failed`/`dead` during the stop, and when `--timeout` expires |
319
+
320
+
321
+ `create` registers a session and returns a session ID without allocating a host —
322
+ it is cold and costs nothing until you use it. The first `exec`/`run`/`upload`/
323
+ `shell`/`ssh` brings it online automatically with its files intact. Use
324
+ `--on <session-id>` with `exec`, `run`, `upload`, or `download` to interact with it
325
+ repeatedly.
326
+
327
+ Sessions resume transparently: there is **no `resume` command**. A session that was
328
+ stopped or reaped while idle comes back online (on a fresh host, with its workspace
329
+ restored from cloud storage) the moment you use it again. `super stop` exists only to halt spend
330
+ immediately — your files are preserved either way. `job run` also keeps its
331
+ `/workspace`; see Jobs.
332
+
333
+ `super stop` returns once the stop is accepted (billing has stopped), which is
334
+ before teardown has saved the files — and teardown can be deferred and retried
335
+ (`stop accepted … its host did not answer the teardown`). Anything that reads
336
+ the saved manifest (`super ls`, `super mount`, `super map --from <sid>`) must
337
+ wait for it: pass **`super stop <sid> --wait`** (`--timeout SECONDS`, default
338
+ 300, `0` waits forever) instead of polling `super sessions` for `state=cold`
339
+ yourself. It exits 1 when the wait expires (the stop is unaffected), when the
340
+ server records a failed final save for this stop (`close_flush_incomplete`,
341
+ `volume_commit_failed`), or when the session ends `failed`/`dead` during the stop.
342
+
343
+ **Keep a client attached until the host is up.** First use claims a host for the
344
+ session; if no client stays attached while it comes online, the host is reclaimed
345
+ ~60s after it's claimed (the session goes back to cold and resumes on next use —
346
+ no data loss, but you paid for a boot that did nothing). To run **N boxes at
347
+ once**, keep one client attached per box for its whole run — e.g. N parallel
348
+ `super exec --on <sid> --wait <long>` (or `spawn` + `super wait`) — rather than
349
+ creating N sessions and attending to them one at a time, which lets the idle
350
+ ones lose their hosts.
351
+
352
+ ### Surviving preemption
353
+
354
+ Sessions run on spot capacity. **Restart is on by default**: on a spot notice,
355
+ supercomputer flushes `/workspace`, gets a new box, restores the files and
356
+ re-runs the command. After an unannounced host death it restores the last
357
+ completed workspace sync and re-runs the command. Your program must save and
358
+ load its own checkpoints to recover progress; unsynced files and in-memory
359
+ state can be lost. See *Make your program resumable* below.
360
+
361
+ For restart, the new box first goes where the session's files are stored.
362
+ `--home-wait DUR` (default `10m`) sets how long to wait for a box there. If none
363
+ is available, supercomputer moves the files elsewhere only when the estimated
364
+ transfer cost fits `--egress-budget USD` (default `$5`). Both settings require
365
+ restart and apply only when the session is created.
366
+
367
+ `--no-restart` (SDK `restart=False`, project `"restart": false`,
368
+ `SUPERCOMPUTER_RESTART=off`, or `super config set restart off`) turns all
369
+ automatic recovery off. After a spot notice the session returns cold on any
370
+ host with `/workspace` kept, processes lost and nothing re-run (SDK:
371
+ `client.describe_session(sid).state_loss_reason` is
372
+ `spot_preempt_checkpoint_disabled`). After host death it ends (`dead`). You
373
+ must start new work yourself.
374
+
375
+ `--checkpoint` is an **EXPERIMENTAL opt-in**: it first tries live migration of
376
+ the running process, including GPU state, and falls back to restart if that
377
+ fails. It is not available on multi-GPU SKUs and requires restart to be on.
378
+ Live migration restores where the sandbox checkpoint is stored and can wait
379
+ there for capacity (`provisioning.reason` `awaiting_restore_capacity`).
380
+ `--no-checkpoint` disables live migration but leaves restart enabled.
381
+
382
+ ```bash
383
+ super run --sku gpu_1x_h100 --upload train.py --upload checkpoint_progress.py --detach -- python -u train.py
384
+ super run --sku gpu_1x_h100 --checkpoint --upload train.py --upload checkpoint_progress.py --detach -- python -u train.py
385
+ super create --sku gpu_1x_l4 --no-restart
386
+ super config set restart off
387
+ super config get restart
388
+ super config unset restart
389
+ super run --home-wait 20m --egress-budget 8 --upload train.py --detach -- python train.py
390
+ ```
391
+
392
+ `SUPERCOMPUTER_MIGRATE_RESTART=never` stops automatic command re-runs during
393
+ restart. It is redundant with `--no-restart` for that session, but remains
394
+ useful for restart-enabled sessions and existing callers.
395
+
396
+ Precedence, strongest first:
397
+
398
+ - `--checkpoint`/`--no-checkpoint` (SDK `checkpoint=True/False`) →
399
+ `SUPERCOMPUTER_CHECKPOINT=on|off` → supercomputer.json `"checkpoint"` →
400
+ `super config set checkpoint on|off` → server default (off).
401
+ - `--restart`/`--no-restart` (SDK `restart=True/False`) →
402
+ `SUPERCOMPUTER_RESTART=on|off` → supercomputer.json `"restart"` →
403
+ `super config set restart on|off` → server default (on).
404
+
405
+ Both settings are fixed when the session is created; `--on SID` uses that
406
+ policy. There is no `auto` or `app` spelling for these settings.
407
+ `super sessions --json` reports `checkpoint` (live migration enabled) and
408
+ `restart` (automatic recovery enabled).
409
+
410
+ Run `super sessions --json` and read each session's `restart_check`, for
411
+ example `{"status":"pass","saved_step":100,"resumed_step":101,
412
+ "at":"2026-09-25T12:00:00Z"}`. Status is `pass`, `fail`, or `no_signal`;
413
+ the step fields may be `null`, and `at` is RFC3339. `pass` means the first `start` after
414
+ the restart is the last pre-restart `save` step plus one (or step `0` or `1` when
415
+ there was no save). `fail` means it started elsewhere. `no_signal` means the
416
+ progress file did not survive the restart, or there was no new `start` while
417
+ the re-run process was running.
418
+
419
+ #### Make your program resumable
420
+
421
+ Save under `/workspace`, not a volume. While a session runs, periodic syncs
422
+ start no sooner than 2 minutes after the preceding sync completes; the time
423
+ until the next sync can be longer. Volumes commit only when the session ends.
424
+ On a preemption notice super stops the program and flushes `/workspace` once
425
+ more before the new box starts. When a box dies with no notice, the resume
426
+ starts from the last completed periodic sync, and
427
+ files the program was still rewriting at that sync (the progress file, the
428
+ newest checkpoint) can be missing from it, so the resume can start from an
429
+ older save or from scratch.
430
+ Run Python unbuffered (`python -u` or `PYTHONUNBUFFERED=1`) so step lines show
431
+ up in `super logs` as they happen. Write a checkpoint
432
+ to a temporary path, flush and `fsync` it, rename it atomically, then update
433
+ `LATEST` and keep the newest two. Load `LATEST` when the program starts. Include
434
+ model weights, optimizer and scheduler state, AMP gradient scaler, Python/NumPy/
435
+ PyTorch RNG state, dataloader position, and global step. Restoring weights alone
436
+ does not resume training from the same step.
437
+
438
+ Append JSON lines to `/workspace/.supercomputer/progress.jsonl`. Write
439
+ `{"event":"start","step":N}` once when the program starts, where `N` is the
440
+ first step it will run. Write `{"event":"save","step":N}` only after the
441
+ checkpoint for step `N` is durably in place, after the atomic rename. Other
442
+ lines, such as `{"step":N,"loss":x}`, may share the file; the restart check
443
+ reads only lines that carry `event`. Use this small helper in each recipe below:
444
+
445
+ ```python
446
+ # /workspace/checkpoint_progress.py
447
+ import json
448
+ import os
449
+ import shutil
450
+ import tempfile
451
+ from pathlib import Path
452
+
453
+ ROOT = Path(os.environ.get("WORKSPACE", "/workspace")) / ".supercomputer" # super run sets WORKSPACE; set it to a local folder to test
454
+ CHECKPOINTS = ROOT / "checkpoints"
455
+ LATEST = ROOT / "LATEST"
456
+ PROGRESS = ROOT / "progress.jsonl"
457
+
458
+ def progress(event, step):
459
+ ROOT.mkdir(parents=True, exist_ok=True)
460
+ with PROGRESS.open("a") as file:
461
+ file.write(json.dumps({"event": event, "step": int(step)}) + "\n")
462
+ file.flush()
463
+ os.fsync(file.fileno())
464
+
465
+ def latest():
466
+ return Path(LATEST.read_text().strip()) if LATEST.exists() else None
467
+
468
+ def _sync_dir(path):
469
+ fd = os.open(path, os.O_RDONLY)
470
+ try:
471
+ os.fsync(fd)
472
+ finally:
473
+ os.close(fd)
474
+
475
+ def save(step, write_checkpoint):
476
+ CHECKPOINTS.mkdir(parents=True, exist_ok=True)
477
+ temp = Path(tempfile.mkdtemp(prefix=".pending-", dir=CHECKPOINTS))
478
+ write_checkpoint(temp)
479
+ for path in temp.rglob("*"):
480
+ if path.is_file():
481
+ with path.open("rb") as file:
482
+ os.fsync(file.fileno())
483
+ for path in sorted((p for p in temp.rglob("*") if p.is_dir()),
484
+ key=lambda p: len(p.parts), reverse=True):
485
+ _sync_dir(path)
486
+ _sync_dir(temp)
487
+ final = CHECKPOINTS / f"step-{step}-{temp.name[len('.pending-'):]}"
488
+ os.replace(temp, final)
489
+ _sync_dir(CHECKPOINTS)
490
+ with tempfile.NamedTemporaryFile(mode="w", dir=ROOT, delete=False) as file:
491
+ file.write(str(final) + "\n")
492
+ file.flush()
493
+ os.fsync(file.fileno())
494
+ latest_temp = file.name
495
+ os.replace(latest_temp, LATEST)
496
+ _sync_dir(ROOT)
497
+ saved = sorted(CHECKPOINTS.glob("step-*"), key=lambda p: p.stat().st_mtime_ns)
498
+ for old in saved[:-2]:
499
+ shutil.rmtree(old)
500
+ progress("save", step)
501
+ ```
502
+
503
+ **Plain PyTorch.** Put this around your existing `model`, `optimizer`,
504
+ `scheduler`, `scaler`, and `loader`. Use a deterministic sampler; the stored
505
+ epoch and batch index let you skip batches already consumed. Set `total_epochs`
506
+ and `save_every` for your run.
507
+
508
+ ```python
509
+ import random
510
+ import numpy as np
511
+ import torch
512
+ from checkpoint_progress import latest, progress, save
513
+
514
+ checkpoint = latest()
515
+ epoch0 = batch0 = step = 0
516
+ if checkpoint:
517
+ state = torch.load(checkpoint / "state.pt", map_location="cpu", weights_only=False)
518
+ model.load_state_dict(state["model"])
519
+ optimizer.load_state_dict(state["optimizer"])
520
+ scheduler.load_state_dict(state["scheduler"])
521
+ scaler.load_state_dict(state["scaler"])
522
+ random.setstate(state["python_rng"])
523
+ np.random.set_state(state["numpy_rng"])
524
+ torch.set_rng_state(state["torch_rng"])
525
+ if torch.cuda.is_available():
526
+ torch.cuda.set_rng_state_all(state["cuda_rng"])
527
+ epoch0, batch0, step = state["epoch"], state["next_batch"], state["step"]
528
+ progress("start", step + 1)
529
+ for epoch in range(epoch0, total_epochs):
530
+ if hasattr(loader.sampler, "set_epoch"):
531
+ loader.sampler.set_epoch(epoch)
532
+ for batch_index, batch in enumerate(loader):
533
+ if epoch == epoch0 and batch_index < batch0:
534
+ continue
535
+ # Replace this line with your forward, backward, optimizer, scaler and scheduler steps.
536
+ train_batch(batch, model, optimizer, scheduler, scaler)
537
+ step += 1
538
+ if step % save_every == 0:
539
+ next_epoch, next_batch = (epoch + 1, 0) if batch_index + 1 == len(loader) else (epoch, batch_index + 1)
540
+ state = dict(model=model.state_dict(), optimizer=optimizer.state_dict(),
541
+ scheduler=scheduler.state_dict(), scaler=scaler.state_dict(),
542
+ python_rng=random.getstate(), numpy_rng=np.random.get_state(),
543
+ torch_rng=torch.get_rng_state(),
544
+ cuda_rng=torch.cuda.get_rng_state_all() if torch.cuda.is_available() else [],
545
+ epoch=next_epoch, next_batch=next_batch, step=step)
546
+ save(step, lambda path: torch.save(state, path / "state.pt"))
547
+ batch0 = 0
548
+ ```
549
+
550
+ **Hugging Face Trainer.** Needs `transformers` and `accelerate`. Add this
551
+ callback to your existing `model` and `train_dataset` setup. `save_steps` sets your save schedule. Trainer writes its
552
+ full checkpoint first; the helper copies it to a temporary directory and
553
+ publishes it after the atomic rename. `resume_from_checkpoint=True` works when
554
+ Trainer's output directory has the latest checkpoint; passing `latest()` uses
555
+ the published copy directly.
556
+
557
+ ```python
558
+ import shutil
559
+ from transformers import Trainer, TrainerCallback, TrainingArguments
560
+ from checkpoint_progress import latest, progress, save
561
+
562
+ class ProgressCallback(TrainerCallback):
563
+ def on_train_begin(self, args, state, control, **kwargs):
564
+ progress("start", state.global_step + 1)
565
+
566
+ def on_save(self, args, state, control, **kwargs):
567
+ source = f"{args.output_dir}/checkpoint-{state.global_step}"
568
+ save(state.global_step, lambda path: shutil.copytree(source, path, dirs_exist_ok=True))
569
+
570
+ args = TrainingArguments(output_dir="/workspace/.supercomputer/trainer",
571
+ save_strategy="steps", save_steps=save_steps)
572
+ trainer = Trainer(model=model, args=args, train_dataset=train_dataset,
573
+ callbacks=[ProgressCallback()])
574
+ trainer.train(resume_from_checkpoint=str(latest()) if latest() else None)
575
+ ```
576
+
577
+ **PyTorch Lightning.** Add these callbacks to your existing `model` and
578
+ `train_loader` setup. Lightning's `ckpt_path="last"` loads the last checkpoint;
579
+ the checkpoint callback copies each completed `last.ckpt` into the atomic store
580
+ and then writes the progress line.
581
+
582
+ ```python
583
+ import shutil
584
+ import lightning as L
585
+ from lightning.pytorch.callbacks import Callback, ModelCheckpoint
586
+ from checkpoint_progress import latest, progress, save
587
+
588
+ class StartProgress(Callback):
589
+ def on_train_start(self, trainer, pl_module):
590
+ progress("start", trainer.global_step + 1)
591
+
592
+ class ProgressCheckpoint(ModelCheckpoint):
593
+ def on_train_epoch_end(self, trainer, pl_module):
594
+ super().on_train_epoch_end(trainer, pl_module)
595
+ source = self.last_model_path
596
+ save(trainer.global_step,
597
+ lambda path: shutil.copy2(source, path / "last.ckpt"))
598
+
599
+ checkpoint = ProgressCheckpoint(dirpath="/workspace/.supercomputer/lightning",
600
+ save_last=True, every_n_epochs=1,
601
+ save_on_train_epoch_end=True)
602
+ if latest():
603
+ checkpoint.last_model_path = str(latest() / "last.ckpt")
604
+ trainer = L.Trainer(callbacks=[StartProgress(), checkpoint])
605
+ trainer.fit(model, train_dataloaders=train_loader,
606
+ ckpt_path="last" if latest() else None)
607
+ ```
608
+
609
+ Start long work with `super run --sku gpu_1x_l4 --detach --upload train.py
610
+ --upload checkpoint_progress.py -- python train.py` so supercomputer can re-run the
611
+ command after a restart. A command typed inside `super shell` is not re-run. The
612
+ host does not keep `--env`/`--secret` values, so a command started with them is
613
+ re-run only by a client still attached to it (not after `--detach`).
614
+
615
+ ### File Transfer
616
+
617
+ | Command | Description | Needs active session? |
618
+ | -------------------------------------- | ------------------------------------------- | ------------------------------- |
619
+ | `super upload <sid> <local> [remote]` | Upload files to a session | Yes — brings a cold session online (a host starts billing) |
620
+ | `super download <sid> <remote> [local]` | Download a file from a session | No — works on active **or** closed |
621
+ | `super ls <sid> [path]` | List a session's workspace manifest | No |
622
+ | `super mount <sid> <mountpoint>` | Mount a session's files (read-only) | No |
623
+
624
+ Only `upload` needs a live host. `download` uses the live host when the
625
+ session is active, for the freshest bytes, and falls back to the saved cloud-storage
626
+ manifest otherwise. `ls` and `mount` are manifest views: while the session runs
627
+ they can lag its files by a sync or two; after `super stop --wait` they are final.
628
+ A closed CLI job also has a saved workspace; use its session ID with `download`,
629
+ `ls`, or `mount`.
630
+
631
+
632
+ Uploaded files land in `/workspace/` on the host. Files **at or under 5 MiB**
633
+ stream inline over the WebSocket; anything larger goes client → cloud storage
634
+ → host via a pre-signed URL minted by the supercomputer API. `super upload` prints which
635
+ files took that second route (and names them again if it fails), so a broken
636
+ large-file upload is attributable to the file, not to the command — one
637
+ oversized results JSON in an otherwise tiny code tree is enough to put the
638
+ batch on the pre-signed path. That path is a single PUT, so each file is
639
+ capped at **5 GiB**; split larger files (e.g. `split -b 4G`) or stage them
640
+ through a volume/import path instead. Downloads work the same way in
641
+ reverse. Uploads keep each file's permission bits (`rwx` for user/group/other;
642
+ setuid/setgid/sticky are dropped), so a `0755` script stays executable;
643
+ `download` does not restore modes locally.
644
+
645
+ **Client version.** Every request states the client's version, and the API
646
+ rejects clients below the deployment's floor with HTTP 426 / `client_too_old`
647
+ (`supercomputer.ClientTooOldError`) naming the upgrade command. If you see that, run
648
+ `pip install -U gpu-compute-provider` — it is a client-age problem, never a credential one.
649
+
650
+ ### Browsing & mounting a session
651
+
652
+ `super ls` and `super mount` read the session's file manifest. The host
653
+ refreshes it after each periodic workspace sync while the session runs (so it
654
+ can lag by a couple of minutes) and writes the final copy at stop. They are
655
+ most useful after a `super stop --wait` (the common "grab my outputs later"
656
+ case).
657
+
658
+ ```bash
659
+ super ls s-abc123 # list files at the root
660
+ super ls s-abc123 data # list files under data/
661
+ super mount s-abc123 ~/mnt # browse in Finder/Explorer (read-only)
662
+ ```
663
+
664
+ A stopped session's **durable logs** appear under a virtual `_logs/` directory
665
+ next to its workspace files — the image build log, the session lifecycle log,
666
+ and the per-process exec logs the host streamed to cloud storage:
667
+
668
+ ```bash
669
+ super ls s-abc123 _logs # build.log, session.log, exec-<pid>.log, ...
670
+ super download s-abc123 _logs/build.log # read a log long after the session is gone
671
+ ```
672
+
673
+ These are the same streams `super logs <sid> <pid>` shows live; `_logs/` just
674
+ makes them browsable and downloadable post-hoc, and they appear under `_logs/`
675
+ in `super mount` too.
676
+
677
+ `mount` streams file bytes **directly from cloud storage** to your machine (the
678
+ API only serves the listing + a redirect), so it stays fast for large
679
+ files. It uses rclone's **NFS mount** (the OS NFS client), so it needs only
680
+ rclone — **no macFUSE, no kernel extension, no recovery-mode approval**:
681
+
682
+ ```bash
683
+ brew install rclone # macOS
684
+ # Linux: sudo apt-get install -y rclone (or curl https://rclone.org/install.sh | sudo bash)
685
+ ```
686
+
687
+ Then `super mount <sid> <dir>` and open `<dir>` in Finder/Explorer. `<dir>`
688
+ must be a path that does **not** exist yet — super creates the mountpoint and
689
+ deletes it on unmount, and refuses an existing folder so it can never touch
690
+ your data. Note: on
691
+ macOS the **Terminal** may be blocked from reading the mount (a privacy/TCC
692
+ quirk) — browse it in **Finder**, or use `super ls` from the CLI. `super ls`
693
+ needs no rclone at all (plain API call). Press Ctrl-C to unmount.
694
+
695
+ ### Volumes
696
+
697
+ Volumes are named, manifest-backed folders for moving data between sessions.
698
+ They are not attached disks and do not reserve separate block storage: file bytes
699
+ stay in the shared content-addressed blob store, while the volume records the
700
+ path → blob manifest.
701
+
702
+ | Command | Description |
703
+ | ----------------------------------------------------------- | ------------------------------------------------ |
704
+ | `super volume create <name>` | Create an empty volume |
705
+ | `super volume list` | List your volumes |
706
+ | `super volume files <name> [path]` | List files in a volume |
707
+ | `super volume download <name> <vol-path> [local] [-r]` | Fetch a file (or, with `-r`, a directory) locally |
708
+ | `super volume cat <name> <vol-path>` | Print a volume file to stdout |
709
+ | `super volume delete <name>` | Delete a volume and its manifest |
710
+ | `super volume import <name> <sid> [session-path] [vol-path]` | Copy a folder from a stopped session into volume |
711
+
712
+ Reads need no session and no host — `files` / `download` / `cat` serve the
713
+ volume's committed manifest straight from the API, so this is how you
714
+ get a job's outputs back:
715
+
716
+ ```bash
717
+ super volume files results # what's in there
718
+ super volume cat results slice-0.json # small file to stdout
719
+ super volume download results slice-0.json
720
+ super volume download results / ./out -r # whole volume into ./out
721
+ ```
722
+
723
+ `delete` returns a conflict while any session still has the volume mounted;
724
+ stop or detach that session first.
725
+
726
+ Mount a volume into a session with repeatable `--volume` flags — on `exec`,
727
+ `run`, `shell`, `spawn`, `job run` and `map`:
728
+
729
+ ```bash
730
+ super exec --volume checkpoints:/workspace/checkpoints --sku gpu_1x_l4 \
731
+ python train.py
732
+ super run --volume checkpoints . --script train.py --sku gpu_1x_l4
733
+ ```
734
+
735
+ `--volume NAME` mounts at `/workspace/NAME`; `--volume NAME:/absolute/path`
736
+ mounts at that absolute path inside the session rootfs.
737
+
738
+ **Volumes are not block devices and are not mounted with the unix `mount`
739
+ command.** There is no `/dev/*` node — don't probe for one with `mount`,
740
+ `lsblk`, or `df` inside the session, and don't try to mount/umount/format
741
+ anything. The host composes each volume into the session's root filesystem as a
742
+ read-only content-addressed layer (file bytes fetched lazily from the blob
743
+ store on first read) plus a writable overlay at the mount path. Inside the
744
+ session a volume is just an ordinary directory: use normal file I/O. Writes
745
+ land in the overlay and are committed back to the volume as a new version on
746
+ detach or session close.
747
+
748
+ On a new session,
749
+ `--volume` rides the create and is composed into the filesystem at setup, so a
750
+ volume-mounted command queues and provisions a host like any other session;
751
+ `--volume` also works against an already-running session (`--on <sid>`), where
752
+ it is attached live. Any number of sessions may attach the
753
+ same volume concurrently — commits merge per path, last writer wins per file
754
+ (so key parallel writers' outputs by distinct paths). Only `delete` and
755
+ `import` conflict while a volume is attached; browsing works any time. Append
756
+ `:ro` (`--volume data:/vol:ro`) to attach read-only: the session sees the
757
+ volume but its writes are discarded at close (no commit).
758
+
759
+ Declare volumes once in `supercomputer.json` to skip `--volume` on every command (see
760
+ [Project Config](#project-config-supercomputerjson)):
761
+
762
+ ```json
763
+ { "sku": "gpu_1x_l4", "volumes": [{ "name": "checkpoints", "mount": "/workspace/checkpoints" }] }
764
+ ```
765
+
766
+ The effective set is the `supercomputer.json` volumes plus any `--volume` flags, keyed by
767
+ name; a `--volume` for a name already in `supercomputer.json` re-maps its mount path for
768
+ that invocation (just like `--sku` overrides the config `sku`). The SDK
769
+ re-attaches this set every time it brings the session online — including after a
770
+ stop/resume on a fresh host — and attach is idempotent, so re-passing a volume
771
+ already mounted is a safe no-op.
772
+
773
+ `volume import` is a single batch metadata operation at folder scope; it does
774
+ not issue per-file API calls and does not copy bytes when blobs already exist.
775
+ The source session must be stopped/closed so the import reads a finalized
776
+ manifest. Import (like delete) is refused while the target volume is attached
777
+ to an active session; browsing is not.
778
+
779
+ ```bash
780
+ super volume create checkpoints
781
+ super stop s-abc123
782
+ super volume import checkpoints s-abc123 runs/run-42 run-42
783
+ super volume files checkpoints run-42
784
+ ```
785
+
786
+ #### End-to-end: persist results into a volume across sessions
787
+
788
+ A volume is the way to carry data from one session to another (or to a later
789
+ run on a fresh host). Writes land in the volume on session close; reads see them
790
+ on the next attach — no manual sync step.
791
+
792
+ ```bash
793
+ # 1. Create the volume once.
794
+ super volume create runs
795
+
796
+ # 2. Run a job that writes into the mounted volume. This queues and
797
+ # provisions a host even when none is idle (no --volume special-casing).
798
+ super exec --volume runs:/workspace/runs --sku gpu_1x_l4 \
799
+ -- sh -lc 'python train.py && cp model.pt /workspace/runs/model.pt'
800
+
801
+ # 3. After the session closes, the file is committed to the volume.
802
+ super volume files runs # -> model.pt
803
+
804
+ # 4. A later session mounts the same volume and sees the file.
805
+ super exec --volume runs:/workspace/runs --sku gpu_1x_l4 \
806
+ -- ls -l /workspace/runs/model.pt
807
+ ```
808
+
809
+ Two things worth knowing:
810
+
811
+ - **Mount onto an empty path.** Attaching a volume onto a directory that
812
+ already contains session files is rejected (`volume_mount_target_not_empty`)
813
+ rather than silently shadowing them. Mount at a fresh path like
814
+ `/workspace/runs`. The same applies to `super run <dir> --volume`: a mount
815
+ at the upload destination (`/workspace/<dir>`), or below it where the
816
+ uploaded tree has content, is rejected before anything is uploaded or run.
817
+ - **Commit mid-run by stopping.** Closing the session commits attached volumes
818
+ automatically — that is the common case. To commit a volume mid-run, use
819
+ `super stop <sid> --wait` and use the session again — stopping commits the
820
+ volume, and the session comes back on next use with the mount intact.
821
+ The commit lands asynchronously, up to ~30s after the session reports
822
+ closing — a `volume files`/`cat` in that window reads the previous version.
823
+
824
+ ### Background Processes
825
+
826
+
827
+ | Command | Description |
828
+ | ------------------------------------------------ | ----------------------------------------------- |
829
+ | `super spawn [--sku SKU \| --on SID] [--volume NAME[:MOUNT]] [--emit-pid FILE] -- CMD` | `super run --detach`; can create a session |
830
+ | `super wait <sid> <pid> [--timeout DUR]` | Block until a process exits; return its exit code + tail |
831
+ | `super logs <sid> <pid> [--follow] [--output MODE]` | Show output from a process |
832
+ | `super kill <sid> <pid>` | Kill a background or auto-detached exec process |
833
+
834
+ `spawn --emit-pid FILE` writes `{"session_id": ..., "process_id": ...}` JSON —
835
+ the same format `exec`/`run --emit-pid` produce — not a bare process id.
836
+
837
+ Use `--follow` for live monitoring. Use `--output all` after a command has already
838
+ run to fetch its durable log. `--stream stdout|stderr|combined` selects a stream for
839
+ newer process logs; `combined` is the default and works with older logs.
840
+
841
+ **Waiting on a job.** Prefer `super wait <sid> <pid>` over hand-rolled
842
+ `until super logs ... | grep` loops. It blocks until the process actually
843
+ terminates, prints its exit code and log tail, and **exits with the process's
844
+ own exit code** (`75` if a `--timeout` elapses while it's still running, `69`
845
+ if the host was lost with the process, `1` if it was killed with no code). It
846
+ reads durable logs, so it also works after the host is gone. This is the
847
+ reliable way to detect a failed background job — a grep loop can miss failures
848
+ whose text doesn't match the pattern, and can't tell "still starting" from
849
+ "already died". In `--json` mode `super logs <sid> <pid>` and `super wait` both
850
+ surface terminal `status` and `exit_code`, so a poller can check
851
+ `status != "running"` instead of grepping log text.
852
+
853
+ ### SSH
854
+
855
+
856
+ | Command | Description |
857
+ | ------------------------------------ | --------------------------------------------------------- |
858
+ | `super shell [--sku SKU] [--volume NAME[:MOUNT]]` | Start an interactive supercomputer-native shell in a new session |
859
+ | `super shell [--on SID|SID] [--volume NAME[:MOUNT]] [cmd...]` | Attach an interactive PTY to an existing session |
860
+ | `super ssh <session-id>` | SSH into a session |
861
+ | `super tunnel <sid> [--port PORT]` | SSH ProxyCommand tunnel |
862
+
863
+
864
+ `shell` is the supercomputer-native PTY path. `super shell` creates a temporary
865
+ interactive session, while `super shell <sid>` or `super shell --on <sid>`
866
+ attaches to a persistent session and leaves it alive on exit. `--volume` works
867
+ the same as on `exec`: mounts ride the create for a new session and are
868
+ attached before the prompt on an existing one. `ssh` uses the
869
+ local OpenSSH client over `super tunnel`; use it when you specifically want SSH
870
+ tooling/config behavior.
871
+
872
+ `super tunnel <sid>` defaults to the session SSH server port (`2222`) so it can
873
+ be used directly as an OpenSSH `ProxyCommand`. Pass `--port` only when tunneling
874
+ to a different TCP service inside the session.
875
+
876
+ `super ssh` requires an SSH server inside the session image. If `sshd` is not
877
+ already present, the CLI bootstraps it with `apt-get install openssh-server` on
878
+ the first SSH connection, which can add tens of seconds. To avoid that cost,
879
+ include `openssh-server` in the project image when SSH access matters:
880
+
881
+ ```json
882
+ {
883
+ "image": {
884
+ "apt": ["openssh-server"]
885
+ }
886
+ }
887
+ ```
888
+
889
+ `super ssh` stops that `sshd` when it exits (so the session can go idle); the
890
+ next `super ssh` restarts it, which is quick once the package is installed.
891
+
892
+ ---
893
+
894
+ ## Sweeps (`super map`) — fan one command out over N bindings
895
+
896
+ A sweep runs your command N times with different arguments as a **durable
897
+ server-side fan-out**: submit, disconnect, and the service drives every
898
+ task through its own session — queueing for capacity, retrying failures, and
899
+ recording exit codes + logs. No changes to your code; your script's CLI *is*
900
+ the interface.
901
+
902
+ ```bash
903
+ # Hyperparameter sweep (cartesian product: 3 × 2 = 6 tasks)
904
+ super map --name lr-sweep --sku gpu_1x_l4 \
905
+ -- python3 train.py --lr {lr} --bs {bs} ::: lr=1e-4,3e-4,1e-3 ::: bs=32,64
906
+
907
+ # Datagen over indices (shell brace expansion feeds the group)
908
+ super map --name gen -- python3 gen.py --shard {} --of 500 ::: {0..499}
909
+
910
+ # One task per line from a pipe (no ::: → stdin binds {})
911
+ jq -r '.items[]' work.json | super map --name batch -- python3 proc.py {}
912
+ ```
913
+
914
+ **Supplying args (GNU-parallel style).** Everything after the command is
915
+ parsed for `:::` markers: each one starts a **group** of values (space- or
916
+ comma-separated), and each task runs the command with one combination of
917
+ values substituted in. Four ways to feed values:
918
+
919
+ | Spec | Tasks | Meaning |
920
+ |---|---|---|
921
+ | `cmd {} ::: a b c` | 3 | One task per value; `{}` (or `{1}`) is the value |
922
+ | `cmd {1} {2} ::: a b ::: x y` | 2×2 = 4 | Multiple groups **cross** (cartesian product) |
923
+ | `cmd {1} {2} ::: m1 m2 :::+ t1 t2` | 2 | `:::+` **zips** with the previous group (paired values like model+tokenizer; lengths must match) |
924
+ | `producer \| super map -- cmd {}` | one per line | No `:::` groups → each stdin line is one task |
925
+
926
+ Placeholders: `{}` = the single group's value · `{1}` `{2}` = group by
927
+ position · `{lr}` = named group (write it `::: lr=1e-4,3e-4`) · `{i}` = task
928
+ index (0-based) · `{n}` = task count. A command with **no** placeholders gets
929
+ each task's values appended at the end (`cmd ::: a b` runs `cmd a`, `cmd b`).
930
+ Every task also gets env vars `SUPERCOMPUTER_TASK_ID`, `SUPERCOMPUTER_WORLD_SIZE`, `SUPERCOMPUTER_JOB` —
931
+ a script can read those to self-shard and skip templating entirely.
932
+ `super help map` prints this reference.
933
+
934
+ **Code binding modes.** The sweep pins an immutable snapshot at submit, so
935
+ task 400 runs the same bytes as task 0:
936
+ - **Default (Mode 2):** the current dir (or `--code DIR`) is hashed and
937
+ uploaded once; each task session starts from it. Image comes from
938
+ `supercomputer.json` or the SKU default.
939
+ - **`--from <session-id>` (Mode 1):** fork the session you just verified —
940
+ each task starts from that session's synced workspace (deps installed,
941
+ weights downloaded) and inherits its image. The session must have synced a
942
+ manifest (a stopped session always has; a live one syncs periodically).
943
+
944
+ **Outputs.** Write task-keyed files (`shard-{i}.jsonl`) into the task's own
945
+ workspace and pull them all with **`super sweep harvest <name> <remote-glob>
946
+ <local-dir>`** — it resolves each task's session from the sweep and writes
947
+ `<local-dir>/task-<index>/<path>`, so there is no session-id bookkeeping and
948
+ no cross-task filename collisions. (`super download <task-session> <path>` is
949
+ still there for one file from one task.) Or write into a shared volume. Volume commits are per-path
950
+ merges (last writer wins per file): parallel tasks writing DISTINCT files
951
+ into one shared volume all land safely; concurrent writes to the SAME file
952
+ keep only the last committer, so key every task's output by `{i}`. Commits
953
+ ride each task session's close and can trail the task's "succeeded" state
954
+ by ~30s — read the volume after commits settle.
955
+
956
+ **Code.** With no `--from`/`--code`, map pins the current directory (upload
957
+ skip rules apply) and prints the path/file-count/byte summary before
958
+ uploading; it refuses to pin `$HOME` or a filesystem root implicitly.
959
+
960
+ **Fan-out width.** `--max-parallel N` caps how many tasks run at once;
961
+ without it the cap is **8**, and submitting more tasks than that runs them
962
+ in waves. Submit says so out loud, and the cap is changeable on a live
963
+ sweep: `super sweep set <name> --max-parallel N`.
964
+
965
+ **Canary.** Task 0 runs first and gates the rest: a broken sweep costs one
966
+ task, not N. `super map` blocks until the canary passes, then detaches (Ctrl-C
967
+ detaches the watch — it does NOT cancel). `--no-canary` / `--no-wait` opt out.
968
+ When one task takes minutes, `--canary-smoke 'python3 -c "import train"'`
969
+ gates on a cheap check instead and task 0 then runs its real command with
970
+ the rest — cheaper than `--no-canary`, which risks N bad tasks to save one.
971
+ With `--retries` left at its default, a canary whose command exits non-zero
972
+ fails the gate on that first attempt; pass `--retries N` and the canary gets
973
+ that budget like every other task (the rest still wait behind it). Fix it, then
974
+ `super sweep retry <name>` re-runs the canary and launches the rest once it passes.
975
+ Failures don't consume the retry budget when caused by spot preemption — the
976
+ task just requeues.
977
+
978
+ **Skip the line.** When tasks are blocked on capacity, `super sweep boost
979
+ <name>` (or pressing `s` in `sweep status --watch`, offered only while a
980
+ line exists) moves the sweep to the fast claim lane: queued tasks skip
981
+ ahead of free-lane work, and freed machines alternate fast/free so nobody
982
+ is starved. SDK: `handle.boost()`. Unpriced for now.
983
+
984
+ **Waiting for capacity is not failure.** A task with no host yet keeps its
985
+ queued session, charges no retry, and is reported as blocked:
986
+ `super sweep status` prints `blocked on capacity: N task(s), oldest 6m12s`
987
+ followed by a `why:` line saying what the wait actually is — a line
988
+ (`3 session(s) ahead in the gpu_1x_l4 queue (1 fast-lane)`), the platform
989
+ (`a new machine is being provisioned`, `waiting for a busy machine to free
990
+ up`), or, for a sweep with a `deadline` that is missed or at risk, `could
991
+ not find capacity for this SKU in time for the deadline`. Use it to decide:
992
+ a line or a provision resolves itself (or `boost` past the free lane); the
993
+ deadline line means pick another SKU, move the deadline or cancel. A `state: waiting` sweep is a
994
+ pipeline hold, and status names the upstream sweeps it waits on.
995
+
996
+ **Retry backoff.** A charged failure re-queues with a growing delay (45s ×
997
+ attempt, capped at 10m) so a deterministic crasher doesn't burn its budget
998
+ in seconds; preemption/lost-host requeues never consume the budget (the
999
+ first two re-run immediately, later ones back off the same way, and a task
1000
+ fails with an `infra:` error after 20).
1001
+
1002
+ **Pipelines (`--after`).** `--after SWEEP[,SWEEP]` holds a sweep in
1003
+ `waiting` until the named sweeps finish, then launches it — a durable
1004
+ server-side barrier, so a whole generate → train → eval pipeline submits up
1005
+ front and the client can disconnect:
1006
+
1007
+ ```bash
1008
+ super map --name gen --volume run42:/vol \
1009
+ -- python3 gen.py --out /vol/data/shard-{i}.jsonl ::: {0..63}
1010
+ super map --name train --sku gpu_8x_h100 --volume run42:/vol --after gen \
1011
+ -- python3 train.py --data /vol/data --ckpt /vol/ckpt
1012
+ super map --name eval --volume run42:/vol --after train \
1013
+ -- python3 eval.py --ckpt /vol/ckpt --suite {} ::: mmlu,gsm8k,mbpp
1014
+ ```
1015
+
1016
+ Data flows between stages through the shared volume: map tasks write
1017
+ disjoint task-keyed paths, the next stage mounts the same volume and sees
1018
+ the union (the gate waits for the upstream's volume commits to land before
1019
+ releasing). An upstream failure fails the downstream sweep with
1020
+ `upstream_failed:<name>` instead of running it on missing inputs; after
1021
+ fixing and re-running the upstream, `super sweep retry <name> --all`
1022
+ re-arms the downstream. A `succeeded_partial` upstream counts as finished
1023
+ unless `--after-strict`. Edges bind at submit, so upstreams must be
1024
+ submitted first (which also makes cycles impossible). SDK:
1025
+ `client.map(..., after=gen_handle)` (or a name, or a list; `after_strict=True`).
1026
+
1027
+ **Tracking & recovery:**
1028
+ ```bash
1029
+ super sweeps # all sweeps: state + done/run/fail counts
1030
+ super sweep status <name> # counts + failures clustered by error, with exemplar task
1031
+ super sweep status <name> --watch # same, repainted until terminal (--interval S)
1032
+ super sweep harvest <name> '*.json' ./out # every task's outputs → out/task-<i>/…
1033
+ super sweep logs <name> [--task N] [--attempt K] # durable output (defaults to the exemplar failure, falling back to earlier attempts; --attempt 1 = most recent attempt, explicitly)
1034
+ super sweep set <name> --max-parallel N # change the fan-out cap on a live sweep
1035
+ super sweep retry <name> [--all] # re-run failed tasks (fix code first: resubmit is idempotent); after a canary failure it re-runs the canary first and launches the rest when it passes; --all adds canceled + WEDGED in-flight tasks (lost host / dead placement) and never restarts healthy running work
1036
+ super sweep cancel <name> [--remaining] # stop; --remaining keeps running tasks (partial success)
1037
+ super sweep wait <name> [--poll S] [--timeout S] # block until terminal; exit 0 = succeeded (incl. succeeded_partial), 1 = failed/canceled, 124 = still running at timeout
1038
+ ```
1039
+ `--webhook URL` POSTs the status document on canary pass/fail and
1040
+ completion — agents should submit with `--no-wait --json` and wake on the
1041
+ webhook instead of polling. There is no account-default webhook: without
1042
+ `--webhook`, nothing is POSTed.
1043
+
1044
+ **The status JSON is a versioned public contract** (`schema_version: 1`),
1045
+ specified in `docs/sweep-status-schema.md`: `counts` is an open map keyed by
1046
+ task state, the task→session mapping is `tasks[].session_id`, sweep state is
1047
+ `job.state`, and per-session spend is under `cost` (requested with
1048
+ `super sweep status <name> --cost`). Additive changes keep the
1049
+ version; a removal or rename bumps it. Do not reverse-engineer it — and
1050
+ prefer `super sweep harvest` over re-deriving the task→session→download walk
1051
+ by hand.
1052
+
1053
+ **SDK mirror** — `client.map(command, args=…)`. The *type* of `args` picks
1054
+ the combinator (no `:::` syntax in the SDK):
1055
+
1056
+ | `args` | Tasks | CLI equivalent |
1057
+ |---|---|---|
1058
+ | `{"lr": [1e-4, 3e-4], "bs": [32, 64]}` | 4 — cartesian product, binds `{lr}`/`{bs}` | `::: lr=… ::: bs=…` |
1059
+ | `[{"model": "a", "tok": "x"}, {"model": "b", "tok": "y"}]` | 2 — one per row, pre-paired | `::: … :::+ …` |
1060
+ | `["f1.json", "f2.json"]` | 2 — one per item, binds `{}` | `::: f1.json f2.json` |
1061
+ | `500` or `range(500)` | 500 — same command N times; index via `{i}`/`SUPERCOMPUTER_TASK_ID`, nothing appended | `::: {0..499}` |
1062
+ | `None` (default) | 1 | — |
1063
+
1064
+ ```python
1065
+ sweep = client.map("python3 train.py --lr {lr} --bs {bs}",
1066
+ args={"lr": [1e-4, 3e-4], "bs": [32, 64]}, # 4 tasks
1067
+ name="lr-sweep", sku="gpu_1x_l4", code_dir=".")
1068
+ sweep.wait_canary(); sweep.wait() # or poll sweep.status() / .retry_failed()
1069
+ # from_session="s-…" instead of code_dir for Mode 1. Returns a SweepHandle.
1070
+ ```
1071
+
1072
+ ---
1073
+
1074
+ ## Agent Workflow Patterns
1075
+
1076
+ ### Pattern 1: Quick one-off command
1077
+
1078
+ The simplest pattern. Run a command, get the result, done.
1079
+
1080
+ ```bash
1081
+ super exec --sku gpu_1x_l4 --json nvidia-smi -L
1082
+ # {"stdout": "GPU 0: NVIDIA L4 ...\n", "returncode": 0, "status": "exited", ...}
1083
+ ```
1084
+
1085
+ ### Pattern 2: Run a training script
1086
+
1087
+ Upload code, run it, get results. Session lifecycle is fully managed.
1088
+
1089
+ ```bash
1090
+ super run train.py --sku gpu_1x_l4 --json -- --epochs 50
1091
+ # {"stdout": "...", "stderr": "", "returncode": 0, "duration_ms": 34521}
1092
+ ```
1093
+
1094
+ For a project directory with dependencies:
1095
+
1096
+ ```bash
1097
+ super run . --script train.py --sku gpu_1x_l4 -- --batch-size 32
1098
+ ```
1099
+
1100
+ ### Pattern 3: Persistent session (multi-step workflow)
1101
+
1102
+ Create a session once, interact with it across multiple commands. The session's
1103
+ filesystem persists between invocations — no re-uploading. It comes online on the
1104
+ first use and resumes transparently if it was reaped while idle.
1105
+
1106
+ ```bash
1107
+ # Create (returns a session ID; cold, $0 until first used)
1108
+ super create --sku gpu_1x_l4 --json
1109
+ # {"session_id": "s-abc123", "status": "closed", "sku": "gpu_1x_l4", "checkpoint": false, "restart": true}
1110
+
1111
+ # Install dependencies (brings the session online automatically)
1112
+ super exec --on s-abc123 pip install torch transformers
1113
+
1114
+ # Upload training data
1115
+ super upload s-abc123 ./data
1116
+
1117
+ # Run training
1118
+ super run train.py --on s-abc123 -- --epochs 50 --lr 1e-4
1119
+
1120
+ # Same persistent-session target using the positional alias
1121
+ super run s-abc123 train.py -- --epochs 50 --lr 1e-4
1122
+
1123
+ # Check results
1124
+ super exec --on s-abc123 cat /workspace/results.json
1125
+
1126
+ # Download model weights
1127
+ super download s-abc123 /workspace/model.pt ./model.pt
1128
+
1129
+ # Halt spend now (files kept; resumes on next use)
1130
+ super stop s-abc123
1131
+ ```
1132
+
1133
+ Each command connects in ~1s, runs, and detaches. The session's files persist
1134
+ across all invocations on the host's `/workspace/`.
1135
+
1136
+ This is the recommended pattern for agents running multi-step experiments —
1137
+ cheaper and faster than creating a new session for every command.
1138
+
1139
+ Use `--wait` for commands that are expected to take longer than 30s but should
1140
+ remain foreground from the agent's perspective. If the command detaches anyway,
1141
+ follow the returned `process_id` instead of retrying the same command.
1142
+
1143
+ ### Pattern 4: SDK session (programmatic multi-step)
1144
+
1145
+ For multi-step workflows (install deps → upload data → train → download results),
1146
+ use the Python SDK directly. A session stays alive across all operations.
1147
+
1148
+ ```python
1149
+ from supercomputer import Client, Image
1150
+
1151
+ client = Client()
1152
+ image = (Image.debian_slim()
1153
+ .pip_install(["torch==2.8.0"], index_url="https://download.pytorch.org/whl/cu128")
1154
+ .pip_install(["numpy"]))
1155
+ project = client.project("my-experiment", image=image)
1156
+
1157
+ with project.session(sku="gpu_1x_l4") as s:
1158
+ # Install and verify
1159
+ r = s.run(["python3", "-c", "import torch; print(torch.cuda.is_available())"])
1160
+ print(r.stdout) # True
1161
+
1162
+ # Upload training data
1163
+ s.upload("./data", "data")
1164
+
1165
+ # Run training
1166
+ r = s.run(["python3", "/workspace/data/train.py"], timeout_ms=3600_000)
1167
+ print(r.stdout)
1168
+
1169
+ # Download results
1170
+ weights = s.download("model.pt")
1171
+ open("model.pt", "wb").write(weights)
1172
+
1173
+ # Session closes automatically on exit
1174
+ ```
1175
+
1176
+ ### Pattern 5: Long-running job with monitoring
1177
+
1178
+ For jobs that take minutes/hours, **lead with `spawn` + `wait`, not a maxed-out
1179
+ `exec --wait`.** `spawn` returns a durable `process_id` immediately; `wait`
1180
+ blocks on that id reading the durable log, so it survives host migration and
1181
+ never depends on a foreground stream staying alive:
1182
+
1183
+ ```bash
1184
+ # Start the job — prints a durable proc-id right away, no blocking.
1185
+ super spawn s-abc123 python3 /workspace/train.py
1186
+ # Process proc-456 spawned in session s-abc123
1187
+
1188
+ # Block until it exits (reads durable logs; survives migration). --timeout 0
1189
+ # waits indefinitely; a positive timeout exits 75 (still running) so you retry.
1190
+ # A process id the service has no record of (typo'd or stale) errors out
1191
+ # with "unknown process id" (exit 1) instead of telling you to keep waiting.
1192
+ super wait s-abc123 proc-456 --timeout 0
1193
+
1194
+ # Or watch it live:
1195
+ super logs s-abc123 proc-456 --follow
1196
+ # Fetch the full log after it finishes (default stream is combined):
1197
+ super logs s-abc123 proc-456 --output all
1198
+ # Stop it if needed:
1199
+ super kill s-abc123 proc-456
1200
+ ```
1201
+
1202
+ **Don't max out `--wait` on a long job, and never pipe a live `exec` through
1203
+ `| tail`.** `exec --wait 60m ... | tail` buffers all output until the process
1204
+ ends (so you see nothing live) *and* swallows the detach line carrying the
1205
+ `process_id` — leaving you unable to call `super wait` / `super logs --follow` at
1206
+ all. If you must background a long `exec`, capture the id with `--emit-pid`
1207
+ (`spawn` takes the same flag and writes the same file), which writes
1208
+ `{session_id, process_id}` JSON to a file the instant the job starts —
1209
+ readable even when stdout/stderr are piped or backgrounded:
1210
+
1211
+ ```bash
1212
+ super exec --on s-abc123 --emit-pid /tmp/job.json --wait 60m python3 train.py &
1213
+ # ... the file exists as soon as the process starts:
1214
+ PID=$(python3 -c 'import json;print(json.load(open("/tmp/job.json"))["process_id"])')
1215
+ super wait s-abc123 "$PID" --timeout 0
1216
+ ```
1217
+
1218
+ > **Transient unreachability (`rc=75`).** A foreground `exec` can exit **`rc=75`**
1219
+ > (EX_TEMPFAIL) when the session is briefly unreachable — most often the first
1220
+ > exec right after a host boots, while the session is still binding to it, or a
1221
+ > host reclaimed mid-run (spot preemption) that supercomputer recovers on a
1222
+ > new host. `exec` retries the attach for you and only reports `rc=75` once it
1223
+ > still can't reach the session; the message states the real cause ("not
1224
+ > reachable yet" vs. an ended session) instead of assuming a migration. Just
1225
+ > retry — the next attempt lands on the ready/restored host. `super wait` /
1226
+ > `super logs` ride a migration for you. After one, confirm the process is still
1227
+ > alive (`super logs <sid>` lists it as `running`) rather than assuming `rc=75`
1228
+ > means your job is gone.
1229
+ >
1230
+ > **Out of credit (`rc=4`).** A 402 with `code: insufficient_credit` means the
1231
+ > prepaid balance is exhausted: sessions are closed and queued jobs canceled.
1232
+ > **This is not retryable** — retrying returns the same refusal until credit
1233
+ > lands. Distinguish it from `rc=3` (`payment_overdue`, a failed payment on a
1234
+ > bill) and from `rc=75` (transient unreachability, which *is* retryable). The
1235
+ > remedy is `super credits buy 25` or `super credits autorefill on`; the block
1236
+ > clears about a minute after the credit posts. In the SDK it raises
1237
+ > `supercomputer.InsufficientCreditError`, a sibling of `PaymentOverdueError` — code
1238
+ > that catches only `PaymentOverdueError` will not catch it.
1239
+ >
1240
+ > **Migration rides honor `--wait`.** When a foreground `exec` rides a
1241
+ > restart or experimental checkpoint/restore, the ride is bounded by your `--wait` budget — it never
1242
+ > blocks past it (`super run` with no `--wait`, or `--wait 0`, has no bound; the
1243
+ > ride then uses the full ~10-minute restore window). If the budget runs out while the restore is
1244
+ > still in flight, the result is **`status: "migrating"`** with **`rc=75`**:
1245
+ > the process is neither confirmed alive nor dead yet. JSON callers can use
1246
+ > the status to distinguish a ride-in-progress from a live process
1247
+ > (`status: "running"`, also `rc=75`, which means the command itself outlived
1248
+ > `--wait`). Either way, follow up with `super logs <sid> <pid> --follow` or
1249
+ > `super wait`.
1250
+
1251
+ With the SDK:
1252
+
1253
+ ```python
1254
+ from supercomputer import Client
1255
+
1256
+ client = Client()
1257
+ project = client.project("long-job")
1258
+
1259
+ with project.session(sku="gpu_1x_l4") as s:
1260
+ s.upload("./project")
1261
+ proc = s.spawn(["python3", "/workspace/project/train.py"])
1262
+
1263
+ # Poll periodically
1264
+ import time
1265
+ while proc.status.value == "running":
1266
+ proc.poll(lines=20)
1267
+ print(proc.output[-200:]) # last 200 chars
1268
+ time.sleep(60)
1269
+
1270
+ print(f"Done: exit_code={proc.exit_code}")
1271
+ print(s.logs(proc, output_range="all").output)
1272
+ ```
1273
+
1274
+ ### Pattern 6: Stop now and come back later (file persistence)
1275
+
1276
+ Files written during a session are synced to durable cloud storage. After you
1277
+ stop it (or it is reaped while idle), the session comes back online automatically
1278
+ the next time you use it, with all files restored. There is no manual resume step.
1279
+
1280
+ ```bash
1281
+ # Create a session, do work
1282
+ super create --sku gpu_1x_l4
1283
+ # Session s-abc123 created (cold — no GPU running, $0 until used).
1284
+
1285
+ super exec --on s-abc123 python3 train.py
1286
+ super exec --on s-abc123 ls /workspace/
1287
+ # model.pt metrics.json train.py
1288
+
1289
+ # Halt spend now (files sync to cloud storage)
1290
+ super stop s-abc123
1291
+
1292
+ # Hours later: just use it again — it comes back online automatically,
1293
+ # with files restored from cloud storage.
1294
+ super exec --on s-abc123 ls /workspace/
1295
+ # model.pt metrics.json train.py ← files restored from cloud storage
1296
+ ```
1297
+
1298
+ With the Python SDK:
1299
+
1300
+ ```python
1301
+ from supercomputer import Client, Image
1302
+
1303
+ client = Client()
1304
+ project = client.project("experiment", image=Image.debian_slim())
1305
+
1306
+ # First run: train on L4
1307
+ with project.session(sku="gpu_1x_l4") as s:
1308
+ s.run(["python3", "train.py"])
1309
+ sid = s.session_id
1310
+
1311
+ # Later: just attach and use it again — a cold session comes back
1312
+ # online automatically (rebuilt on a fresh host, files restored).
1313
+ s2 = client.attach_session(sid)
1314
+ s2.run(["ls", "/workspace/"]) # original files are here
1315
+ s2.run(["python3", "finetune.py"])
1316
+ s2.close()
1317
+ ```
1318
+
1319
+ **What survives a resume**: `/workspace` always survives. Changes *outside*
1320
+ `/workspace` (e.g. `pip install` into the image's site-packages) survive an
1321
+ ordinary same-image resume, but are **reset to the image** when the session
1322
+ resumes with a changed `supercomputer.json` image spec (a re-baseline) or when the
1323
+ previous close could not finish syncing — the session prints an
1324
+ `environment reset` banner when that happens. For packages that must survive
1325
+ unconditionally, install them into a venv under the workspace:
1326
+
1327
+ ```bash
1328
+ super exec --on s-abc123 'python3 -m venv /workspace/venv && /workspace/venv/bin/pip install torch'
1329
+ super exec --on s-abc123 /workspace/venv/bin/python train.py
1330
+ ```
1331
+
1332
+ Better yet, put packages in the image spec (`pip_install([...])`) so every
1333
+ resume rebuilds them from the content-addressed image cache. A sweep whose
1334
+ canary pip-installs packages gets them baked into its image automatically
1335
+ before the fan-out (the pass line names them; `--no-bake` disables) — but
1336
+ the pins still belong in `supercomputer.json` so the *next* sweep never pays them.
1337
+
1338
+ ---
1339
+
1340
+ ## Image Builder
1341
+
1342
+ Images define the environment (base OS, packages, env vars). They're declarative
1343
+ and content-addressed — same layers = cache hit, instant startup.
1344
+
1345
+ ```python
1346
+ from supercomputer import Image
1347
+
1348
+ # GPU-ready image with PyTorch
1349
+ image = (
1350
+ Image.debian_slim()
1351
+ .apt_install(["git", "build-essential"])
1352
+ .pip_install(["torch==2.8.0"], index_url="https://download.pytorch.org/whl/cu128")
1353
+ .pip_install(["transformers", "numpy"])
1354
+ .env({"PYTHONUNBUFFERED": "1"})
1355
+ )
1356
+
1357
+ project = client.project("my-project", image=image)
1358
+ ```
1359
+
1360
+ Use `Image.from_registry(...)` when the workload needs a specific container base:
1361
+
1362
+ ```python
1363
+ from supercomputer import Image
1364
+
1365
+ # CUDA devel images include nvcc, ptxas, CUDA headers, and other build-time tools.
1366
+ image = (
1367
+ Image.from_registry("nvidia/cuda:12.8.1-devel-ubuntu24.04")
1368
+ .apt_install(["ninja-build"])
1369
+ .pip_install(["torch==2.8.0"], index_url="https://download.pytorch.org/whl/cu128")
1370
+ .pip_install(["triton", "numpy"])
1371
+ .env({"PYTHONUNBUFFERED": "1"})
1372
+ )
1373
+ ```
1374
+
1375
+ Reach for a CUDA `devel` base when packages build CUDA code from source
1376
+ (`torch.utils.cpp_extension`, flash-attn, xformers, mamba, custom kernels). Runtime
1377
+ CUDA/driver availability is not enough for those workflows; they need the compiler
1378
+ toolchain inside the image.
1379
+
1380
+ For GPU workloads, the base image must also provide a glibc new enough to load the
1381
+ host NVIDIA driver libraries injected by the runtime. This is distro-agnostic:
1382
+ Debian, Ubuntu, SUSE, Rocky, etc. can all work if their glibc satisfies the NVIDIA
1383
+ library requirement. Do not choose old GPU bases casually; failures usually show up
1384
+ as `GLIBC_2.xx not found`, CUDA initialization failures, or `nvidia-smi`/torch CUDA
1385
+ errors. Prefer a recent CUDA base, for example an NVIDIA CUDA image based on Ubuntu
1386
+ 24.04, unless the user has a specific compatible base. For ordinary CPU/Python
1387
+ packages, `Image.debian_slim()` is usually faster to build and cache.
1388
+
1389
+ On a GPU SKU, `supercomputer.json` `image.pip` installs torch packages from the
1390
+ CUDA 12.8 PyTorch index in their own layer, because an unpinned torch from PyPI
1391
+ can be a newer CUDA build the hosts' drivers cannot run. The SDK's
1392
+ `Image.pip_install` does not do this; pass `index_url` yourself, as above. Do
1393
+ not put system build tools such as `ninja` in pip requirements just because a
1394
+ package build mentions them; install `ninja-build` with apt instead.
1395
+ If a workload mixes PyTorch wheels with ordinary PyPI-only packages, split them
1396
+ into separate image layers or configure the package indexes explicitly.
1397
+
1398
+ Pre-built named images:
1399
+
1400
+ - `Image.from_name("agent-gpu")` — the unified default for every `gpu_*` SKU:
1401
+ `nvidia/cuda:12.8.1-devel-ubuntu24.04` (nvcc + CUDA headers, Python 3.12)
1402
+ with a pinned training stack (torch 2.8.0+cu128, transformers, trl, peft,
1403
+ accelerate, flash-attn prebuilt wheel). One image covers T4 through
1404
+ Blackwell (sm75–sm120). Note: flash-attn *kernels* need sm80+ — on T4 the
1405
+ import works but attention stays on sdpa, so avoid TRL
1406
+ `packing`/`padding_free` there. vLLM is part of the pinned stack
1407
+ (`import vllm` / `vllm serve` work out of the box; no extra installs).
1408
+ - `Image.from_name("agent-cpu")` — CPU-only ML stack
1409
+ - `Image.from_name("scientific")` — numpy, scipy, matplotlib, jupyter
1410
+
1411
+ With no `image` section in `supercomputer.json`, CLI commands (`exec`, `run`,
1412
+ `shell`, `sweep`) use the SKU's default image: `agent-gpu` on `gpu_*` SKUs
1413
+ (Python 3.12, torch 2.8.0+cu128 and the pinned stack above) and `agent-cpu`
1414
+ on `cpu_*` SKUs (Python 3.11 with numpy, pandas and scipy; no torch).
1415
+
1416
+ ---
1417
+
1418
+ ## Project Config (`supercomputer.json`)
1419
+
1420
+ Drop a `supercomputer.json` in your project root to configure the image and default SKU.
1421
+ The CLI auto-discovers it by walking up from cwd (same as `.env`).
1422
+
1423
+ ```json
1424
+ {
1425
+ "sku": "gpu_1x_l4",
1426
+ "volumes": [{"name": "checkpoints", "mount": "/workspace/checkpoints"}],
1427
+ "image": {
1428
+ "base": "nvidia/cuda:12.8.1-devel-ubuntu24.04",
1429
+ "apt": ["git", "build-essential", "ninja-build", "ffmpeg"],
1430
+ "pip": ["torch", "triton", "numpy", "transformers"],
1431
+ "env": {"PYTHONUNBUFFERED": "1"},
1432
+ "run": ["echo setup-complete"]
1433
+ }
1434
+ }
1435
+ ```
1436
+
1437
+ All fields are optional. Explicit `--sku` flags override the file; `--volume`
1438
+ flags merge with (and re-map by name) the `volumes` defaults.
1439
+
1440
+
1441
+ | Field | Description |
1442
+ | ------------ | ------------------------------------------ |
1443
+ | `sku` | Default hardware SKU for `exec` and `run` |
1444
+ | `volumes` | Volumes auto-attached to every `exec`/`run` session. Each entry is `"name"`, `"name:/mount"`, or `{"name": ..., "mount": ...}` |
1445
+ | `checkpoint` | `false` (default): live migration off. `true`: EXPERIMENTAL live migration, falling back to restart; single-GPU only and requires restart. See *Surviving preemption* |
1446
+ | `restart` | `true` (default): restore `/workspace` on a new box and re-run the command, which must load its own checkpoints. `false`: recovery off; cold after a spot notice with nothing re-run, ends after host death |
1447
+ | `image.base` | Base image. An `image` section without `base` starts from `python:3.11-slim`, not from the SKU default, so a GPU SKU gets no torch unless `image.pip` adds it |
1448
+ | `image.apt` | Packages to `apt-get install` |
1449
+ | `image.pip` | Packages to `pip install` |
1450
+ | `image.env` | Environment variables baked into the image |
1451
+ | `image.run` | Shell commands to run during build |
1452
+
1453
+
1454
+ When `supercomputer.json` is present, `super exec` and `super run` automatically build the
1455
+ specified image. The image is cached by content hash — same config = instant startup.
1456
+
1457
+ ---
1458
+
1459
+ ## Key Concepts
1460
+
1461
+ **SKU**: Hardware type identifier. Format: `gpu_<N>x_<family>` or `cpu_<N>x_<family>`.
1462
+ Examples: `gpu_1x_l4`, `gpu_8x_h100`, `cpu_2x_epyc`. `super skus` lists what exists now.
1463
+
1464
+ **Session**: A persistent workspace (filesystem at `/workspace/`) that runs commands
1465
+ in a sandboxed container on a host. A session is a filesystem, not a held GPU:
1466
+
1467
+ - *Throwaway*: created and released per command (`super exec`, `super run` without
1468
+ `--on`) — the filesystem is still saved and downloadable afterwards.
1469
+ - *Persistent*: created with `super create`, interacted with via `--on <sid>`. It is
1470
+ cold ($0) until used, comes online automatically on use, and `super stop` halts spend
1471
+ without deleting it.
1472
+
1473
+ **Job**: `super job run` runs a command; its `--retries` re-runs only after a lost box. Its `/workspace`
1474
+ persists after the command, and it follows the session's preemption policy.
1475
+ The box is released when the command ends. See the Jobs section.
1476
+
1477
+ Sessions resume transparently — a stopped or idle-reaped session is rebuilt on a fresh
1478
+ host (files restored from cloud storage) the next time you use it. You never run a resume command.
1479
+
1480
+ **Warmth**: `super sessions` reports a session's state — `hot` (running, spending),
1481
+ `warm` (idle host held, spending), `stopping` (finishing teardown, not billed), `cold`
1482
+ (stopped, $0, resumes on use), `preparing` (coming online), or `ended` (a
1483
+ finished SDK job, never resumes).
1484
+ The `$/HR` column is the rate billed on the session's open usage interval while a
1485
+ host is held and billed (0 when cold or stopping); `SPENT` is compute charged so far; the footer sums `$/HR`. `super usage` reports what was actually charged.
1486
+
1487
+ **Image**: Declarative container spec. Layers are cached by content hash. First run
1488
+ on a host triggers a build (30–120s); subsequent runs with the same image are instant.
1489
+
1490
+ **Project**: Groups sessions with a shared image definition. Created implicitly by
1491
+ CLI commands or explicitly via `client.project("name", image=...)`.
1492
+
1493
+ ---
1494
+
1495
+ ## Tips for Agents
1496
+
1497
+ 1. **Always use `--json`** when parsing output. Human-readable output may change format.
1498
+ 2. **Use `--sku`** to target specific hardware. Without it (and without a `sku` in
1499
+ `supercomputer.json`), the scheduler picks any available host and the default
1500
+ image is `agent-cpu` (no torch), even on a GPU host.
1501
+ 3. **Use `super create` for multi-step work**. One persistent session is faster and
1502
+ cheaper than creating a new ephemeral session for every command.
1503
+ 4. **Treat `returncode: null` as detached, not failed**. Read `session_id` and
1504
+ `process_id`, then call `super logs <sid> <pid> --follow` or poll the process.
1505
+ If stdout was truncated, call `super logs <sid> <pid> --output all`.
1506
+ 5. **Use `--wait` to express foreground patience**. Increase it for commands that
1507
+ should finish soon; let truly long jobs detach and monitor them by process ID.
1508
+ 6. **Pass argv normally; quote a single string (or use `sh -c`) for shell
1509
+ expressions**. A single quoted argument runs as shell text via `bash -lc`.
1510
+ 7. **Cached images are fast** (~2s). If your workflow creates sessions repeatedly with
1511
+ the same image, it's cheap.
1512
+ 8. **Files live in `/workspace/` — and it's your default working directory.**
1513
+ Uploaded files are accessible as relative paths (`python train.py`) or as
1514
+ `/workspace/<filename>` / `/workspace/<dirname>/...`. Commands already run
1515
+ from `/workspace`, so don't `cd /root` or hardcode `/root/...` paths.
1516
+ 9. **Pick a SKU explicitly** with `--sku`. If capacity is unavailable, the session
1517
+ queues until a host is provisioned rather than failing.
1518
+ 10. **Use `super stop` to halt spend now**. Ephemeral sessions (`exec`, `run`
1519
+ without `--on`) release automatically. Persistent sessions (`create`) keep their
1520
+ files; they stop on their own when idle, so `stop` is only needed to halt spend
1521
+ immediately.
1522
+ 11. **Just use a session again to pick up where you left off**. Files persist across
1523
+ stop/idle via cloud storage, and the session comes back online automatically (no resume
1524
+ command). You can target different hardware with `--sku`.
1525
+ 12. **Gracefully stop GPU engines when possible**. Prefer the engine's shutdown
1526
+ method or `super kill <sid> <pid>` over in-session `pkill -9`; if GPU memory
1527
+ remains pinned after a hard kill, recover with `super stop <sid>` and then use the
1528
+ session again.
1529
+
1530
+ ## Autonomous research loops (autoresearch)
1531
+
1532
+ To run an overnight keep/discard experiment loop on the user's code — after
1533
+ [karpathy/autoresearch](https://github.com/karpathy/autoresearch), with the GPU on
1534
+ supercomputer instead of under the desk:
1535
+
1536
+ ```bash
1537
+ super autoresearch init # writes autoresearch/SETUP.md + program.template.md
1538
+ ```
1539
+
1540
+ Then read `autoresearch/SETUP.md` and follow it. It walks you through deciding the
1541
+ metric, the one mutable file vs the frozen eval, and the fixed time budget with the
1542
+ user; you write `program.md`, run the baseline on a persistent session, and hand
1543
+ off to the loop, which only ever re-uploads the mutable file and greps the metric.
1544
+ Progress is visible on the session's page in the supercomputer dashboard (Autoresearch
1545
+ tab: metric chart + results table) once the loop's `results.tsv` upload syncs;
1546
+ point the user there when the loop starts.
1547
+
1548
+ ## Anti-Patterns
1549
+
1550
+
1551
+ | Don't | Why |
1552
+ | ---------------------------------------------- | ------------------------------------------------------------------------------ |
1553
+ | Omit `--json` when parsing output | Human format is for display, not parsing |
1554
+ | Request unavailable SKUs | Session will queue until a host is provisioned; pick a SKU you expect to exist |
1555
+ | Retry after `returncode: null` | The original command is still running; follow its process ID |
1556
+ | Max out `exec --wait` on a long job | Prefer `spawn` + `super wait`; a huge `--wait` blocks the foreground and depends on the stream staying alive |
1557
+ | Pipe a live `exec` through `\| tail` | Buffers until EOF (no live output) and swallows the detach line with the `process_id`. Use `--emit-pid FILE`, or `spawn` + `wait` |
1558
+ | Pipe `spawn` (or any launch) through `\| head` | `head` can exit before the `process_id` line is read: the process runs, but you lose its id. Filter with `grep`, or use `--emit-pid` |
1559
+ | Treat a migration `rc=75` as "job died" | After preemption the command is re-run on a new host (restored, with `--checkpoint`); use `super wait`/`logs`, which ride the migration |
1560
+ | Immediately retry a preempted job | The system automatically attempts to requeue it; resubmitting races the requeue and duplicates the run |
1561
+ | Put local shell syntax outside quotes | The local shell handles it before `super`; quote it or use `sh -c` |
1562
+ | Use `pkill -9` as the first response to a hung GPU engine | Hard kills can bypass framework cleanup; try graceful shutdown or `super kill`, then `super stop` and reuse if needed |
1563
+ | Run commands without `--sku` in production | May land on a CPU when you need a GPU, and the default image has no torch |
1564
+ | Run long work inside `super shell` | Commands typed there are not re-run after a restart. Start them with `super run --detach` |
1565
+ | Save restart checkpoints only to a volume | Volumes commit when the session ends. Save them under `/workspace` during the run |