fcloud-sdk 0.1.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.
- fcloud/SKILL.md +1185 -0
- fcloud/__init__.py +85 -0
- fcloud/__main__.py +150 -0
- fcloud/_direct_bridge.py +1645 -0
- fcloud/_legacy_env.py +14 -0
- fcloud/cli/__init__.py +23 -0
- fcloud/cli/attach.py +180 -0
- fcloud/cli/common.py +382 -0
- fcloud/cli/console.py +227 -0
- fcloud/cli/context.py +161 -0
- fcloud/cli/exec_cmd.py +201 -0
- fcloud/cli/files.py +280 -0
- fcloud/cli/hardware.py +83 -0
- fcloud/cli/help.py +150 -0
- fcloud/cli/interactive.py +104 -0
- fcloud/cli/job.py +300 -0
- fcloud/cli/main.py +140 -0
- fcloud/cli/migration.py +276 -0
- fcloud/cli/mount.py +150 -0
- fcloud/cli/output.py +17 -0
- fcloud/cli/processes.py +360 -0
- fcloud/cli/registry.py +28 -0
- fcloud/cli/run.py +318 -0
- fcloud/cli/sessions.py +592 -0
- fcloud/cli/setup.py +44 -0
- fcloud/cli/ssh.py +255 -0
- fcloud/cli/sweep.py +578 -0
- fcloud/cli/sweep_harvest.py +187 -0
- fcloud/cli/sweep_watch.py +104 -0
- fcloud/cli/volume.py +317 -0
- fcloud/cli/wait.py +314 -0
- fcloud/cli_args.py +348 -0
- fcloud/client.py +632 -0
- fcloud/client_projects.py +52 -0
- fcloud/client_sessions.py +111 -0
- fcloud/client_volumes.py +80 -0
- fcloud/config.py +404 -0
- fcloud/direct.py +535 -0
- fcloud/errors.py +102 -0
- fcloud/fileset.py +155 -0
- fcloud/image.py +483 -0
- fcloud/job.py +249 -0
- fcloud/providers/__init__.py +5 -0
- fcloud/providers/requests_http.py +59 -0
- fcloud/py.typed +0 -0
- fcloud/session.py +442 -0
- fcloud/setup_cmd.py +213 -0
- fcloud/shell.py +558 -0
- fcloud/sweeps.py +557 -0
- fcloud/tunnel.py +251 -0
- fcloud/types.py +302 -0
- fcloud/v2_connect.py +928 -0
- fcloud/version.py +33 -0
- fcloud/volume_wait.py +131 -0
- fcloud/volumes.py +204 -0
- fcloud_sdk-0.1.0.dist-info/METADATA +234 -0
- fcloud_sdk-0.1.0.dist-info/RECORD +63 -0
- fcloud_sdk-0.1.0.dist-info/WHEEL +5 -0
- fcloud_sdk-0.1.0.dist-info/entry_points.txt +3 -0
- fcloud_sdk-0.1.0.dist-info/licenses/LICENSE +202 -0
- fcloud_sdk-0.1.0.dist-info/licenses/NOTICE +4 -0
- fcloud_sdk-0.1.0.dist-info/top_level.txt +2 -0
- foom/__init__.py +36 -0
fcloud/SKILL.md
ADDED
|
@@ -0,0 +1,1185 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: fcloud
|
|
3
|
+
description: >-
|
|
4
|
+
Use fcloud 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
|
+
# fcloud — GPU compute from the command line
|
|
10
|
+
|
|
11
|
+
fcloud 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: `fcloud` | CLI command: `fcloud` | Python 3.10+
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Setup
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pip install fcloud-sdk
|
|
24
|
+
fcloud set_token <api_key> # one-time; saves to ~/.fcloud/token
|
|
25
|
+
fcloud health # verify connectivity
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Token resolution order:
|
|
29
|
+
|
|
30
|
+
1. `api_key=` parameter to `Client()`
|
|
31
|
+
2. `FCLOUD_API_KEY` environment variable (legacy `FOOM_API_KEY` still accepted)
|
|
32
|
+
3. Saved token in `~/.fcloud/token`
|
|
33
|
+
4. `FCLOUD_API_KEY` from the nearest `.env` file
|
|
34
|
+
|
|
35
|
+
(For a human at a terminal, plain `fcloud set_token` with no argument prompts
|
|
36
|
+
with hidden input, keeping the key out of shell history.)
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Quick Start
|
|
41
|
+
|
|
42
|
+
### Run a script on a GPU (throwaway session)
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
# Upload a script, run it on an L4, print output, tear down
|
|
46
|
+
fcloud run train.py --sku gpu_1x_l4
|
|
47
|
+
|
|
48
|
+
# Upload a directory, run a specific script, pass arguments
|
|
49
|
+
fcloud run . --script train.py --sku gpu_1x_l4 -- --epochs 50 --lr 1e-4
|
|
50
|
+
|
|
51
|
+
# Run a one-off command (no file upload)
|
|
52
|
+
fcloud exec --sku gpu_1x_l4 nvidia-smi -L
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Choosing a SKU
|
|
56
|
+
|
|
57
|
+
Request hardware by passing `--sku` to `exec`/`run`/`create`. SKUs follow the
|
|
58
|
+
pattern `gpu_<count>x_<family>` or `cpu_<count>x_<family>`: `gpu_1x_l4`,
|
|
59
|
+
`gpu_1x_a10`, `gpu_8x_h100`, `cpu_generic`, `cpu_2x_epyc`, etc. If a SKU has
|
|
60
|
+
no available capacity, the session queues until a host is provisioned.
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## CLI Reference
|
|
65
|
+
|
|
66
|
+
### Global Flags
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
| Flag | Description |
|
|
70
|
+
| -------- | --------------------------------------------------------------------- |
|
|
71
|
+
| `--json` | Machine-readable JSON output to stdout. Status messages go to stderr. |
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
Use `--json` in agent workflows so output can be parsed programmatically. Place
|
|
75
|
+
`--json` before your remote command (e.g. `fcloud exec --json -- python app.py`):
|
|
76
|
+
flags after the command — or after a `--` — belong to *your* program, so
|
|
77
|
+
`fcloud exec -- python app.py --json` passes `--json` to `app.py`, not to fcloud.
|
|
78
|
+
Unknown flags are rejected (e.g. a typo'd `--limitt`) rather than silently
|
|
79
|
+
ignored.
|
|
80
|
+
|
|
81
|
+
### Infrastructure
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
| Command | Description |
|
|
85
|
+
| ---------------------- | ----------------------------------------- |
|
|
86
|
+
| `fcloud health` | Check API connectivity |
|
|
87
|
+
| `fcloud set_token <key>` | Save API key |
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
### Execution
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
| Command | Description |
|
|
94
|
+
| ----------------------------------------------------------------------------- | ----------------------- |
|
|
95
|
+
| `fcloud exec [--volume NAME[:MOUNT]] [--sku SKU] [--on SID|SID] [--wait 30s] [--emit-pid FILE] <cmd...>` | Run a command on a host |
|
|
96
|
+
| `fcloud run [--volume NAME[:MOUNT]] [--on SID|SID] <file|dir> [--sku SKU] [--script NAME] [-- args...]` | Upload + run on a host |
|
|
97
|
+
| `fcloud upload [--on SID|SID] <local> [remote]` / `fcloud download [--on SID|SID] <remote> [local]` | Transfer files (session via `--on` or positional) |
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
Without `--on`, `exec` and `run` create a throwaway session, do the work, and
|
|
101
|
+
release it automatically — its filesystem is still saved and downloadable
|
|
102
|
+
afterwards. For a true no-persistence run (cheaper; nothing syncs to
|
|
103
|
+
storage), use a **job** — see the Jobs section. With `--on <session-id>` or a
|
|
104
|
+
positional session ID alias, they attach to an existing session, run, and
|
|
105
|
+
detach (session stays alive).
|
|
106
|
+
|
|
107
|
+
Cached images complete in ~2s. Cold builds (first run on a host) take 30–120s.
|
|
108
|
+
|
|
109
|
+
### Exec Semantics
|
|
110
|
+
|
|
111
|
+
`fcloud exec` is optimized for agents and ML workloads. It starts commands as
|
|
112
|
+
managed processes and waits for a foreground result for a configurable budget.
|
|
113
|
+
|
|
114
|
+
**Working directory.** Commands run from `/workspace` by default — the same
|
|
115
|
+
place `fcloud upload` puts your files. So an uploaded `train.py` runs as
|
|
116
|
+
`python train.py` (relative path, no `cd` and no need to spell out
|
|
117
|
+
`/workspace/train.py`), and commands that write to relative paths also land in
|
|
118
|
+
`/workspace` and persist. Do **not** assume a different default (e.g. `/root`)
|
|
119
|
+
or hardcode `/root/...` paths — that's the single most common cwd mistake. To
|
|
120
|
+
run elsewhere for one command, `cd` inside it: `sh -c 'cd /some/dir && ...'`.
|
|
121
|
+
This applies uniformly to `exec`, `run`, `spawn`, and `shell`.
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
# Default wait budget is 30s
|
|
125
|
+
fcloud exec --on s-abc123 python3 /workspace/train.py
|
|
126
|
+
|
|
127
|
+
# Wait longer before detaching
|
|
128
|
+
fcloud exec --on s-abc123 --wait 10m python3 /workspace/train.py
|
|
129
|
+
|
|
130
|
+
# Positional session alias for --on
|
|
131
|
+
fcloud exec s-abc123 --wait 10m -- python3 /workspace/train.py
|
|
132
|
+
|
|
133
|
+
# Alias for --wait, useful when an agent thinks in timeouts
|
|
134
|
+
fcloud exec --on s-abc123 --timeout 10m python3 /workspace/train.py
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
If the command exits within `--wait`, `exec` behaves like a normal foreground
|
|
138
|
+
command: stdout/stderr are printed and the CLI exits with the remote return code.
|
|
139
|
+
|
|
140
|
+
If the command is still running when the wait budget expires, `exec` does **not**
|
|
141
|
+
kill it. It detaches and prints a process ID:
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
Command is still running; use fcloud logs s-abc123 proc-456 --follow
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
In `--json` mode, a detached command returns `status: "running"`,
|
|
148
|
+
`returncode: null`, plus `session_id` and `process_id`. Agents should treat that
|
|
149
|
+
as success-with-handle, not as command failure, then block on it with
|
|
150
|
+
`fcloud wait <session_id> <process_id>` (returns the real exit code + tail) or
|
|
151
|
+
tail it with `fcloud logs --follow`.
|
|
152
|
+
|
|
153
|
+
```json
|
|
154
|
+
{
|
|
155
|
+
"stdout": "",
|
|
156
|
+
"stderr": "",
|
|
157
|
+
"returncode": null,
|
|
158
|
+
"status": "running",
|
|
159
|
+
"session_id": "s-abc123",
|
|
160
|
+
"process_id": "proc-456",
|
|
161
|
+
"message": "Command is still running; use fcloud logs s-abc123 proc-456 --follow"
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
`exec` returns a bounded stdout payload by default so a noisy command cannot flood
|
|
166
|
+
the caller. If the JSON response has `stdout_truncated: true`, do **not** rerun the
|
|
167
|
+
job just to recover logs. Use the returned `session_id` and `process_id`:
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
# Fetch the durable combined process log captured by the host
|
|
171
|
+
fcloud logs s-abc123 proc-456 --output all
|
|
172
|
+
|
|
173
|
+
# Fetch a specific range or stream
|
|
174
|
+
fcloud logs s-abc123 proc-456 --output head --output-bytes 128k
|
|
175
|
+
fcloud logs s-abc123 proc-456 --stream stderr --output all
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
> The default stream is **combined** (stdout + stderr) — prefer it. Most ML
|
|
179
|
+
> tooling (tqdm progress bars, HuggingFace/TRL logs) writes to **stderr**, so
|
|
180
|
+
> `--stream stdout` on a training job often looks empty even though the job is
|
|
181
|
+
> producing output. Reach for `--stream stdout`/`stderr` only when you
|
|
182
|
+
> specifically need one split.
|
|
183
|
+
|
|
184
|
+
**Liveness while polling a long job:** `fcloud logs <sid> <pid> --json` includes
|
|
185
|
+
`last_write_ts` (when the durable log was last written) and `session_status`.
|
|
186
|
+
A `status: "running"` process whose `last_write_ts` stops advancing across
|
|
187
|
+
polls is stalled or on a lost host — no need to diff `output_total_bytes`
|
|
188
|
+
between polls. A `status: "lost_host"` (with `process_alive: false`) means the
|
|
189
|
+
session ended without the process's exit ever being recorded: the host was
|
|
190
|
+
lost, the tail is final, don't keep polling.
|
|
191
|
+
|
|
192
|
+
SDK equivalent:
|
|
193
|
+
|
|
194
|
+
```python
|
|
195
|
+
log = session.logs("proc-456", output_range="all")
|
|
196
|
+
print(log.output)
|
|
197
|
+
|
|
198
|
+
stderr = session.logs("proc-456", stream="stderr", output_range="all")
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Persistent-session commands (`--on <sid>`) print a one-line `✓ ready` to
|
|
202
|
+
stderr on every attach; fuller provision/setup narration appears only when
|
|
203
|
+
the session actually has setup work to do. Terminal/problem statuses also
|
|
204
|
+
print to stderr. Parse stdout (or use `--json`) — never stderr.
|
|
205
|
+
|
|
206
|
+
#### Shell Parsing Rules
|
|
207
|
+
|
|
208
|
+
Prefer passing commands as normal argv:
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
fcloud exec --on s-abc123 ls /workspace
|
|
212
|
+
fcloud exec s-abc123 -- ls /workspace
|
|
213
|
+
fcloud exec --on s-abc123 python3 /workspace/train.py --epochs 10
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
A single quoted string is treated as shell text (it runs via `bash -lc`),
|
|
217
|
+
so both of these work and are equivalent:
|
|
218
|
+
|
|
219
|
+
```bash
|
|
220
|
+
fcloud exec --on s-abc123 'ls /workspace'
|
|
221
|
+
fcloud exec --on s-abc123 'echo one two && echo piped | tr a-z A-Z'
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
When you need shell syntax (`;`, `&&`, pipes, redirects, variables), either pass a
|
|
225
|
+
single quoted shell expression or be explicit with `sh -c`:
|
|
226
|
+
|
|
227
|
+
```bash
|
|
228
|
+
fcloud exec --on s-abc123 'cd /workspace; ls'
|
|
229
|
+
fcloud exec --on s-abc123 -- sh -c 'sleep 45; echo done'
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
Remember that unquoted shell metacharacters are handled by the local shell before
|
|
233
|
+
`fcloud` sees them:
|
|
234
|
+
|
|
235
|
+
```bash
|
|
236
|
+
# Runs "sleep 45" remotely, then "echo done" locally
|
|
237
|
+
fcloud exec --on s-abc123 sleep 45; echo done
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### Jobs (`fcloud job`) — ephemeral run-to-completion
|
|
241
|
+
|
|
242
|
+
A job is a session **without a saved filesystem**: image + `--include` files
|
|
243
|
+
+ volumes in; exit code, logs, and volume writes out. The workspace is
|
|
244
|
+
discarded at close — a job can never be resumed, `ls`'d, mounted, or
|
|
245
|
+
downloaded after it ends (those return a clear error pointing at volumes and
|
|
246
|
+
logs). In exchange, nothing syncs to storage while it runs, and close only
|
|
247
|
+
has the volumes to flush.
|
|
248
|
+
|
|
249
|
+
`job run` returns once the command has exited **and** its volumes are
|
|
250
|
+
committed (it prints `committing volumes…` while that runs), so
|
|
251
|
+
`fcloud volume files <name>` right after it shows the job's output. If the
|
|
252
|
+
host is unreachable at close the commit is deferred to its teardown replay
|
|
253
|
+
and the command says so — `--json` reports it as `volume_sync`.
|
|
254
|
+
|
|
255
|
+
| Command | Description |
|
|
256
|
+
|---|---|
|
|
257
|
+
| `fcloud job run [flags] -- <cmd...>` | Run to completion on a fresh ephemeral session; exits with the command's exit code |
|
|
258
|
+
| `fcloud job ls [--all] [--limit N]` | List jobs (`--all` includes finished ones) |
|
|
259
|
+
| `fcloud job logs <job-id> [--follow]` | Durable output — live or after close |
|
|
260
|
+
| `fcloud job wait <job-id>` | Block until the job ends |
|
|
261
|
+
| `fcloud job kill <job-id>` | Stop now; workspace discarded, volume commits + logs survive |
|
|
262
|
+
|
|
263
|
+
`job run` flags: `--sku SKU` · `--volume NAME[:MOUNT]` (repeatable — the
|
|
264
|
+
durable output surface) · `--include PATH` (repeatable; uploaded before the
|
|
265
|
+
command starts) · `--env K=V` · `--secret KEY[=VALUE]` · `--retries N` ·
|
|
266
|
+
`--min-disk-gb N` · `--detach` (print the job id and return).
|
|
267
|
+
|
|
268
|
+
Jobs are never checkpoint/migrated: a lost host (spot preemption) kills the
|
|
269
|
+
run, and `--retries N` re-runs the command on a fresh session — so make job
|
|
270
|
+
commands re-runnable. If a job is preempted, the system automatically
|
|
271
|
+
attempts to requeue it — don't immediately retry/resubmit yourself; follow
|
|
272
|
+
the same job id with `fcloud job wait`/`logs`. Write outputs to a volume:
|
|
273
|
+
|
|
274
|
+
```bash
|
|
275
|
+
fcloud volume create results
|
|
276
|
+
fcloud job run --sku gpu_1x_l4 --volume results:/results -- \
|
|
277
|
+
python train.py --out /results/model.pt
|
|
278
|
+
fcloud volume files results # outputs survive the job
|
|
279
|
+
fcloud volume download results model.pt # pull them back locally
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
SDK mirror:
|
|
283
|
+
|
|
284
|
+
```python
|
|
285
|
+
job = project.job("python train.py --out /results/model.pt",
|
|
286
|
+
sku="gpu_1x_l4",
|
|
287
|
+
volume_mounts=[{"name": "results", "mount_path": "/results"}],
|
|
288
|
+
include=["./src"], retries=1)
|
|
289
|
+
log = job.wait() # ProcessLog: exit_code, output
|
|
290
|
+
|
|
291
|
+
client.list_volume_files("results") # [{path, size, ...}]
|
|
292
|
+
data = client.read_volume_file("results", "model.pt") # bytes, no session
|
|
293
|
+
client.download_volume_file("results", "model.pt", "./model.pt")
|
|
294
|
+
client.delete_volume("results") # when you're done with it
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
Rule of thumb: a **session** when you'll iterate interactively or want the
|
|
298
|
+
filesystem later; a **job** for batch/CI-style runs with declared outputs.
|
|
299
|
+
|
|
300
|
+
### Persistent Sessions
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
| Command | Description |
|
|
304
|
+
| ----------------------------- | ---------------------------------------------------- |
|
|
305
|
+
| `fcloud create [--sku SKU]` | Create a session (cold — $0, no host, until first used) |
|
|
306
|
+
| `fcloud sessions [--all]` | List sessions |
|
|
307
|
+
| `fcloud stop <session-id> [--wait]` | Stop a session now: halt GPU spend (files kept). `--wait` blocks until the workspace manifest is durable |
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
`create` registers a session and returns a session ID without allocating a host —
|
|
311
|
+
it is cold and costs nothing until you use it. The first `exec`/`run`/`upload`/
|
|
312
|
+
`shell`/`ssh` brings it online automatically with its files intact. Use
|
|
313
|
+
`--on <session-id>` with `exec`, `run`, `upload`, or `download` to interact with it
|
|
314
|
+
repeatedly.
|
|
315
|
+
|
|
316
|
+
Sessions resume transparently: there is **no `resume` command**. A session that was
|
|
317
|
+
stopped or reaped while idle comes back online (on a fresh host, with its workspace
|
|
318
|
+
restored from cloud storage) the moment you use it again. `fcloud stop` exists only to halt spend
|
|
319
|
+
immediately — your files are preserved either way. (Ephemeral **job**
|
|
320
|
+
sessions are the one exception: their workspace is never saved and they can
|
|
321
|
+
never resume — see Jobs.)
|
|
322
|
+
|
|
323
|
+
`fcloud stop` can return `stop deferred; host will retry teardown` — the stop
|
|
324
|
+
stands, but the workspace manifest is not durable yet. Anything that reads
|
|
325
|
+
that manifest (`fcloud ls`, `fcloud mount`, `fcloud map --from <sid>`) must wait
|
|
326
|
+
for it: pass **`fcloud stop <sid> --wait`** (`--timeout SECONDS`, default 300,
|
|
327
|
+
`0` waits forever) instead of polling `fcloud sessions` for `state=cold`
|
|
328
|
+
yourself. It exits 1 if the wait expires; the stop is unaffected.
|
|
329
|
+
|
|
330
|
+
**Keep a client attached until the host is up.** First use claims a host for the
|
|
331
|
+
session; if no client stays attached while it comes online, the host is reclaimed
|
|
332
|
+
~60s after it's claimed (the session goes back to cold and resumes on next use —
|
|
333
|
+
no data loss, but you paid for a boot that did nothing). To run **N boxes at
|
|
334
|
+
once**, keep one client attached per box for its whole run — e.g. N parallel
|
|
335
|
+
`fcloud exec --on <sid> --wait <long>` (or `spawn` + `fcloud wait`) — rather than
|
|
336
|
+
creating N sessions and attending to them one at a time, which lets the idle
|
|
337
|
+
ones lose their hosts.
|
|
338
|
+
|
|
339
|
+
### File Transfer
|
|
340
|
+
|
|
341
|
+
| Command | Description | Needs active session? |
|
|
342
|
+
| -------------------------------------- | ------------------------------------------- | ------------------------------- |
|
|
343
|
+
| `fcloud upload <sid> <local> [remote]` | Upload files to a session | Yes (live host receives them) |
|
|
344
|
+
| `fcloud download <sid> <remote> [local]` | Download a file from a session | No — works on active **or** closed |
|
|
345
|
+
| `fcloud ls <sid> [path]` | List a stopped session's workspace manifest | No |
|
|
346
|
+
| `fcloud mount <sid> <mountpoint>` | Mount a stopped session's files (read-only) | No |
|
|
347
|
+
|
|
348
|
+
Only `upload` needs a live session. `download` uses the live host when the
|
|
349
|
+
session is active, for the freshest bytes, and falls back to the saved cloud-storage
|
|
350
|
+
manifest otherwise. `ls` and `mount` are post-hoc manifest views: use them after
|
|
351
|
+
`fcloud stop`, not while the session is active. None of the post-close views
|
|
352
|
+
work for ephemeral **jobs** (no manifest is ever saved) — a closed job's
|
|
353
|
+
outputs live in its volumes (`fcloud volume files <name>`) and logs
|
|
354
|
+
(`fcloud job logs <id>`).
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
Uploaded files land in `/workspace/` on the host. Files **at or under 5 MiB**
|
|
358
|
+
stream inline over the WebSocket; anything larger goes client → cloud storage
|
|
359
|
+
→ host via a pre-signed URL minted by the fcloud API. `fcloud upload` prints which
|
|
360
|
+
files took that second route (and names them again if it fails), so a broken
|
|
361
|
+
large-file upload is attributable to the file, not to the command — one
|
|
362
|
+
oversized results JSON in an otherwise tiny code tree is enough to put the
|
|
363
|
+
batch on the pre-signed path. That path is a single PUT, so each file is
|
|
364
|
+
capped at **5 GiB**; split larger files (e.g. `split -b 4G`) or stage them
|
|
365
|
+
through a volume/import path instead. Downloads work the same way in
|
|
366
|
+
reverse.
|
|
367
|
+
|
|
368
|
+
**Client version.** Every request states the client's version, and the API
|
|
369
|
+
rejects clients below the deployment's floor with HTTP 426 / `client_too_old`
|
|
370
|
+
(`fcloud.ClientTooOldError`) naming the upgrade command. If you see that, run
|
|
371
|
+
`pip install -U fcloud-sdk` — it is a client-age problem, never a credential one.
|
|
372
|
+
|
|
373
|
+
### Browsing & mounting a session
|
|
374
|
+
|
|
375
|
+
`fcloud ls` and `fcloud mount` read the session's file manifest, which the
|
|
376
|
+
host writes **when the session is stopped**. So they show a session's files
|
|
377
|
+
after a `fcloud stop` (the common "grab my outputs later" case); a session
|
|
378
|
+
that has never been stopped lists empty.
|
|
379
|
+
|
|
380
|
+
```bash
|
|
381
|
+
fcloud ls s-abc123 # list files at the root
|
|
382
|
+
fcloud ls s-abc123 data # list files under data/
|
|
383
|
+
fcloud mount s-abc123 ~/mnt # browse in Finder/Explorer (read-only)
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
A stopped session's **durable logs** appear under a virtual `_logs/` directory
|
|
387
|
+
next to its workspace files — the image build log, the session lifecycle log,
|
|
388
|
+
and the per-process exec logs the host streamed to cloud storage:
|
|
389
|
+
|
|
390
|
+
```bash
|
|
391
|
+
fcloud ls s-abc123 _logs # build.log, session.log, exec-<pid>.log, ...
|
|
392
|
+
fcloud download s-abc123 _logs/build.log # read a log long after the session is gone
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
These are the same streams `fcloud logs <sid> <pid>` shows live; `_logs/` just
|
|
396
|
+
makes them browsable and downloadable post-hoc, and they appear under `_logs/`
|
|
397
|
+
in `fcloud mount` too.
|
|
398
|
+
|
|
399
|
+
`mount` streams file bytes **directly from cloud storage** to your machine (the
|
|
400
|
+
API only serves the listing + a redirect), so it stays fast for large
|
|
401
|
+
files. It uses rclone's **NFS mount** (the OS NFS client), so it needs only
|
|
402
|
+
rclone — **no macFUSE, no kernel extension, no recovery-mode approval**:
|
|
403
|
+
|
|
404
|
+
```bash
|
|
405
|
+
brew install rclone # macOS
|
|
406
|
+
# Linux: sudo apt-get install -y rclone (or curl https://rclone.org/install.sh | sudo bash)
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
Then `fcloud mount <sid> <dir>` and open `<dir>` in Finder/Explorer. `<dir>`
|
|
410
|
+
must be a path that does **not** exist yet — fcloud creates the mountpoint and
|
|
411
|
+
deletes it on unmount, and refuses an existing folder so it can never touch
|
|
412
|
+
your data. Note: on
|
|
413
|
+
macOS the **Terminal** may be blocked from reading the mount (a privacy/TCC
|
|
414
|
+
quirk) — browse it in **Finder**, or use `fcloud ls` from the CLI. `fcloud ls`
|
|
415
|
+
needs no rclone at all (plain API call). Press Ctrl-C to unmount.
|
|
416
|
+
|
|
417
|
+
### Volumes
|
|
418
|
+
|
|
419
|
+
Volumes are named, manifest-backed folders for moving data between sessions.
|
|
420
|
+
They are not EBS volumes and do not reserve separate block storage: file bytes
|
|
421
|
+
stay in the shared content-addressed blob store, while the volume records the
|
|
422
|
+
path → blob manifest.
|
|
423
|
+
|
|
424
|
+
| Command | Description |
|
|
425
|
+
| ----------------------------------------------------------- | ------------------------------------------------ |
|
|
426
|
+
| `fcloud volume create <name>` | Create an empty volume |
|
|
427
|
+
| `fcloud volume list` | List your volumes |
|
|
428
|
+
| `fcloud volume files <name> [path]` | List files in a volume |
|
|
429
|
+
| `fcloud volume download <name> <vol-path> [local] [-r]` | Fetch a file (or, with `-r`, a directory) locally |
|
|
430
|
+
| `fcloud volume cat <name> <vol-path>` | Print a volume file to stdout |
|
|
431
|
+
| `fcloud volume delete <name>` | Delete a volume and its manifest |
|
|
432
|
+
| `fcloud volume import <name> <sid> [session-path] [vol-path]` | Copy a folder from a stopped session into volume |
|
|
433
|
+
|
|
434
|
+
Reads need no session and no host — `files` / `download` / `cat` serve the
|
|
435
|
+
volume's committed manifest straight from the API, so this is how you
|
|
436
|
+
get a job's outputs back:
|
|
437
|
+
|
|
438
|
+
```bash
|
|
439
|
+
fcloud volume files results # what's in there
|
|
440
|
+
fcloud volume cat results slice-0.json # small file to stdout
|
|
441
|
+
fcloud volume download results slice-0.json
|
|
442
|
+
fcloud volume download results / ./out -r # whole volume into ./out
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
`delete` returns a conflict while any session still has the volume mounted;
|
|
446
|
+
stop or detach that session first.
|
|
447
|
+
|
|
448
|
+
Mount a volume into a session with repeatable `--volume` flags — on `exec`,
|
|
449
|
+
`run`, `shell`, `spawn`, `job run` and `map`:
|
|
450
|
+
|
|
451
|
+
```bash
|
|
452
|
+
fcloud exec --volume checkpoints:/workspace/checkpoints --sku gpu_1x_l4 \
|
|
453
|
+
python train.py
|
|
454
|
+
fcloud run --volume checkpoints . --script train.py --sku gpu_1x_l4
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
`--volume NAME` mounts at `/workspace/NAME`; `--volume NAME:/absolute/path`
|
|
458
|
+
mounts at that absolute path inside the session rootfs.
|
|
459
|
+
|
|
460
|
+
**Volumes are not block devices and are not mounted with the unix `mount`
|
|
461
|
+
command.** There is no `/dev/*` node — don't probe for one with `mount`,
|
|
462
|
+
`lsblk`, or `df` inside the session, and don't try to mount/umount/format
|
|
463
|
+
anything. The host composes each volume into the session's root filesystem as a
|
|
464
|
+
read-only content-addressed layer (file bytes fetched lazily from the blob
|
|
465
|
+
store on first read) plus a writable overlay at the mount path. Inside the
|
|
466
|
+
session a volume is just an ordinary directory: use normal file I/O. Writes
|
|
467
|
+
land in the overlay and are committed back to the volume as a new version on
|
|
468
|
+
detach or session close.
|
|
469
|
+
|
|
470
|
+
Volumes are attached
|
|
471
|
+
**after** the session is online, so a volume-mounted command queues and
|
|
472
|
+
provisions a host like any other session, and `--volume` also works against an
|
|
473
|
+
already-running session (`--on <sid>`). Mounted volumes are active in at most
|
|
474
|
+
one session, and browsing/import/delete returns a conflict while a volume is
|
|
475
|
+
attached.
|
|
476
|
+
|
|
477
|
+
Declare volumes once in `fcloud.json` to skip `--volume` on every command (see
|
|
478
|
+
[Project Config](#project-config-fcloudjson)):
|
|
479
|
+
|
|
480
|
+
```json
|
|
481
|
+
{ "sku": "gpu_1x_l4", "volumes": [{ "name": "checkpoints", "mount": "/workspace/checkpoints" }] }
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
The effective set is the `fcloud.json` volumes plus any `--volume` flags, keyed by
|
|
485
|
+
name; a `--volume` for a name already in `fcloud.json` re-maps its mount path for
|
|
486
|
+
that invocation (just like `--sku` overrides the config `sku`). The SDK
|
|
487
|
+
re-attaches this set every time it brings the session online — including after a
|
|
488
|
+
stop/resume on a fresh host — and attach is idempotent, so re-passing a volume
|
|
489
|
+
already mounted is a safe no-op.
|
|
490
|
+
|
|
491
|
+
`volume import` is a single batch metadata operation at folder scope; it does
|
|
492
|
+
not issue per-file API calls and does not copy bytes when blobs already exist.
|
|
493
|
+
The source session must be stopped/closed so the import reads a finalized
|
|
494
|
+
manifest. Volume browsing/modification is blocked while that volume is attached
|
|
495
|
+
to an active session.
|
|
496
|
+
|
|
497
|
+
```bash
|
|
498
|
+
fcloud volume create checkpoints
|
|
499
|
+
fcloud stop s-abc123
|
|
500
|
+
fcloud volume import checkpoints s-abc123 runs/run-42 run-42
|
|
501
|
+
fcloud volume files checkpoints run-42
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
#### End-to-end: persist results into a volume across sessions
|
|
505
|
+
|
|
506
|
+
A volume is the way to carry data from one session to another (or to a later
|
|
507
|
+
run on a fresh host). Writes land in the volume on session close; reads see them
|
|
508
|
+
on the next attach — no manual sync step.
|
|
509
|
+
|
|
510
|
+
```bash
|
|
511
|
+
# 1. Create the volume once.
|
|
512
|
+
fcloud volume create runs
|
|
513
|
+
|
|
514
|
+
# 2. Run a job that writes into the mounted volume. This queues and
|
|
515
|
+
# provisions a host even when none is idle (no --volume special-casing).
|
|
516
|
+
fcloud exec --volume runs:/workspace/runs --sku gpu_1x_l4 \
|
|
517
|
+
-- sh -lc 'python train.py && cp model.pt /workspace/runs/model.pt'
|
|
518
|
+
|
|
519
|
+
# 3. After the ephemeral session closes, the file is committed to the volume.
|
|
520
|
+
fcloud volume files runs # -> model.pt
|
|
521
|
+
|
|
522
|
+
# 4. A later session mounts the same volume and sees the file.
|
|
523
|
+
fcloud exec --volume runs:/workspace/runs --sku gpu_1x_l4 \
|
|
524
|
+
-- ls -l /workspace/runs/model.pt
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
Two things worth knowing:
|
|
528
|
+
|
|
529
|
+
- **Mount onto an empty path.** Attaching a volume onto a directory that
|
|
530
|
+
already contains session files is rejected (`volume_mount_target_not_empty`)
|
|
531
|
+
rather than silently shadowing them. Mount at a fresh path like
|
|
532
|
+
`/workspace/runs`. The same applies to `fcloud run <dir> --volume`: a mount
|
|
533
|
+
at the upload destination (`/workspace/<dir>`), or below it where the
|
|
534
|
+
uploaded tree has content, is rejected before the session starts.
|
|
535
|
+
- **Save without stopping.** Closing the session commits attached volumes
|
|
536
|
+
automatically — that is the common case. To checkpoint a volume mid-run while
|
|
537
|
+
the session keeps running, use `fcloud stop` and start again — stopping commits
|
|
538
|
+
the volume, and the session comes back on next use with the mount intact.
|
|
539
|
+
The commit lands asynchronously, up to ~30s after the session reports
|
|
540
|
+
closing — a `volume files`/`cat` in that window reads the previous version.
|
|
541
|
+
|
|
542
|
+
### Background Processes (requires an active session)
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
| Command | Description |
|
|
546
|
+
| ------------------------------------------------ | ----------------------------------------------- |
|
|
547
|
+
| `fcloud spawn [--on SID\|SID] [--volume NAME[:MOUNT]] [--emit-pid FILE] <cmd...>` | Start a background process |
|
|
548
|
+
| `fcloud wait <sid> <pid> [--timeout DUR]` | Block until a process exits; return its exit code + tail |
|
|
549
|
+
| `fcloud logs <sid> <pid> [--follow] [--output MODE]` | Show output from a process |
|
|
550
|
+
| `fcloud kill <sid> <pid>` | Kill a background or auto-detached exec process |
|
|
551
|
+
|
|
552
|
+
`spawn --emit-pid FILE` writes `{"session_id": ..., "process_id": ...}` JSON —
|
|
553
|
+
the same format `exec`/`run --emit-pid` produce — not a bare process id.
|
|
554
|
+
|
|
555
|
+
Use `--follow` for live monitoring. Use `--output all` after a command has already
|
|
556
|
+
run to fetch its durable log. `--stream stdout|stderr|combined` selects a stream for
|
|
557
|
+
newer process logs; `combined` is the default and works with older logs.
|
|
558
|
+
|
|
559
|
+
**Waiting on a job.** Prefer `fcloud wait <sid> <pid>` over hand-rolled
|
|
560
|
+
`until fcloud logs ... | grep` loops. It blocks until the process actually
|
|
561
|
+
terminates, prints its exit code and log tail, and **exits with the process's
|
|
562
|
+
own exit code** (`75` if a `--timeout` elapses while it's still running). It
|
|
563
|
+
reads durable logs, so it also works after the host is gone. This is the
|
|
564
|
+
reliable way to detect a failed background job — a grep loop can miss failures
|
|
565
|
+
whose text doesn't match the pattern, and can't tell "still starting" from
|
|
566
|
+
"already died". In `--json` mode `fcloud logs <sid> <pid>` and `fcloud wait` both
|
|
567
|
+
surface terminal `status` and `exit_code`, so a poller can check
|
|
568
|
+
`status != "running"` instead of grepping log text.
|
|
569
|
+
|
|
570
|
+
### SSH
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
| Command | Description |
|
|
574
|
+
| ------------------------------------ | --------------------------------------------------------- |
|
|
575
|
+
| `fcloud shell [--sku SKU] [--volume NAME[:MOUNT]]` | Start an interactive fcloud-native shell in a new session |
|
|
576
|
+
| `fcloud shell [--on SID|SID] [--volume NAME[:MOUNT]] [cmd...]` | Attach an interactive PTY to an existing session |
|
|
577
|
+
| `fcloud ssh <session-id>` | SSH into a session |
|
|
578
|
+
| `fcloud tunnel <sid> [--port PORT]` | SSH ProxyCommand tunnel |
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
`shell` is the fcloud-native PTY path. `fcloud shell` creates a temporary
|
|
582
|
+
interactive session, while `fcloud shell <sid>` or `fcloud shell --on <sid>`
|
|
583
|
+
attaches to a persistent session and leaves it alive on exit. `--volume` works
|
|
584
|
+
the same as on `exec`: mounts ride the create for a new session and are
|
|
585
|
+
attached before the prompt on an existing one. `ssh` uses the
|
|
586
|
+
local OpenSSH client over `fcloud tunnel`; use it when you specifically want SSH
|
|
587
|
+
tooling/config behavior.
|
|
588
|
+
|
|
589
|
+
`fcloud tunnel <sid>` defaults to the session SSH server port (`2222`) so it can
|
|
590
|
+
be used directly as an OpenSSH `ProxyCommand`. Pass `--port` only when tunneling
|
|
591
|
+
to a different TCP service inside the session.
|
|
592
|
+
|
|
593
|
+
`fcloud ssh` requires an SSH server inside the session image. If `sshd` is not
|
|
594
|
+
already present, the CLI bootstraps it with `apt-get install openssh-server` on
|
|
595
|
+
the first SSH connection, which can add tens of seconds. To avoid that cost,
|
|
596
|
+
include `openssh-server` in the project image when SSH access matters:
|
|
597
|
+
|
|
598
|
+
```json
|
|
599
|
+
{
|
|
600
|
+
"image": {
|
|
601
|
+
"apt": ["openssh-server"]
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
```
|
|
605
|
+
|
|
606
|
+
Repeat SSH connections to the same session reuse the already-running `sshd`.
|
|
607
|
+
|
|
608
|
+
---
|
|
609
|
+
|
|
610
|
+
## Sweeps (`fcloud map`) — fan one command out over N bindings
|
|
611
|
+
|
|
612
|
+
A sweep runs your command N times with different arguments as a **durable
|
|
613
|
+
server-side fan-out**: submit, disconnect, and the service drives every
|
|
614
|
+
task through its own session — queueing for capacity, retrying failures, and
|
|
615
|
+
recording exit codes + logs. No changes to your code; your script's CLI *is*
|
|
616
|
+
the interface.
|
|
617
|
+
|
|
618
|
+
```bash
|
|
619
|
+
# Hyperparameter sweep (cartesian product: 3 × 2 = 6 tasks)
|
|
620
|
+
fcloud map --name lr-sweep --sku gpu_1x_l4 \
|
|
621
|
+
-- python3 train.py --lr {lr} --bs {bs} ::: lr=1e-4,3e-4,1e-3 ::: bs=32,64
|
|
622
|
+
|
|
623
|
+
# Datagen over indices (shell brace expansion feeds the group)
|
|
624
|
+
fcloud map --name gen -- python3 gen.py --shard {} --of 500 ::: {0..499}
|
|
625
|
+
|
|
626
|
+
# One task per line from a pipe (no ::: → stdin binds {})
|
|
627
|
+
jq -r '.items[]' work.json | fcloud map --name batch -- python3 proc.py {}
|
|
628
|
+
```
|
|
629
|
+
|
|
630
|
+
**Supplying args (GNU-parallel style).** Everything after the command is
|
|
631
|
+
parsed for `:::` markers: each one starts a **group** of values (space- or
|
|
632
|
+
comma-separated), and each task runs the command with one combination of
|
|
633
|
+
values substituted in. Four ways to feed values:
|
|
634
|
+
|
|
635
|
+
| Spec | Tasks | Meaning |
|
|
636
|
+
|---|---|---|
|
|
637
|
+
| `cmd {} ::: a b c` | 3 | One task per value; `{}` (or `{1}`) is the value |
|
|
638
|
+
| `cmd {1} {2} ::: a b ::: x y` | 2×2 = 4 | Multiple groups **cross** (cartesian product) |
|
|
639
|
+
| `cmd {1} {2} ::: m1 m2 :::+ t1 t2` | 2 | `:::+` **zips** with the previous group (paired values like model+tokenizer; lengths must match) |
|
|
640
|
+
| `producer \| fcloud map -- cmd {}` | one per line | No `:::` groups → each stdin line is one task |
|
|
641
|
+
|
|
642
|
+
Placeholders: `{}` = the single group's value · `{1}` `{2}` = group by
|
|
643
|
+
position · `{lr}` = named group (write it `::: lr=1e-4,3e-4`) · `{i}` = task
|
|
644
|
+
index (0-based) · `{n}` = task count. A command with **no** placeholders gets
|
|
645
|
+
each task's values appended at the end (`cmd ::: a b` runs `cmd a`, `cmd b`).
|
|
646
|
+
Every task also gets env vars `FCLOUD_TASK_ID`, `FCLOUD_WORLD_SIZE`, `FCLOUD_JOB` —
|
|
647
|
+
a script can read those to self-shard and skip templating entirely.
|
|
648
|
+
`fcloud help map` prints this reference.
|
|
649
|
+
|
|
650
|
+
**Code binding modes.** The sweep pins an immutable snapshot at submit, so
|
|
651
|
+
task 400 runs the same bytes as task 0:
|
|
652
|
+
- **Default (Mode 2):** the current dir (or `--code DIR`) is hashed and
|
|
653
|
+
uploaded once; each task session starts from it. Image comes from
|
|
654
|
+
`fcloud.json` or the SKU default.
|
|
655
|
+
- **`--from <session-id>` (Mode 1):** fork the session you just verified —
|
|
656
|
+
each task starts from that session's synced workspace (deps installed,
|
|
657
|
+
weights downloaded) and inherits its image. The session must have synced a
|
|
658
|
+
manifest (a stopped session always has; a live one syncs periodically).
|
|
659
|
+
|
|
660
|
+
**Outputs.** Write task-keyed files (`shard-{i}.jsonl`) into the task's own
|
|
661
|
+
workspace and pull them all with **`fcloud sweep harvest <name> <remote-glob>
|
|
662
|
+
<local-dir>`** — it resolves each task's session from the sweep and writes
|
|
663
|
+
`<local-dir>/task-<index>/<path>`, so there is no session-id bookkeeping and
|
|
664
|
+
no cross-task filename collisions. (`fcloud download <task-session> <path>` is
|
|
665
|
+
still there for one file from one task.) Or write into a shared volume. Volume commits are per-path
|
|
666
|
+
merges (last writer wins per file): parallel tasks writing DISTINCT files
|
|
667
|
+
into one shared volume all land safely; concurrent writes to the SAME file
|
|
668
|
+
keep only the last committer, so key every task's output by `{i}`. Commits
|
|
669
|
+
ride each task session's close and can trail the task's "succeeded" state
|
|
670
|
+
by ~30s — read the volume after commits settle.
|
|
671
|
+
|
|
672
|
+
**Code.** With no `--from`/`--code`, map pins the current directory (upload
|
|
673
|
+
skip rules apply) and prints the path/file-count/byte summary before
|
|
674
|
+
uploading; it refuses to pin `$HOME` or a filesystem root implicitly.
|
|
675
|
+
|
|
676
|
+
**Fan-out width.** `--max-parallel N` caps how many tasks run at once;
|
|
677
|
+
without it the cap is **8**, and submitting more tasks than that runs them
|
|
678
|
+
in waves. Submit says so out loud, and the cap is changeable on a live
|
|
679
|
+
sweep: `fcloud sweep set <name> --max-parallel N`.
|
|
680
|
+
|
|
681
|
+
**Canary.** Task 0 runs first and gates the rest: a broken sweep costs one
|
|
682
|
+
task, not N. `fcloud map` blocks until the canary passes, then detaches (Ctrl-C
|
|
683
|
+
detaches the watch — it does NOT cancel). `--no-canary` / `--no-wait` opt out.
|
|
684
|
+
When one task takes minutes, `--canary-smoke 'python3 -c "import train"'`
|
|
685
|
+
gates on a cheap check instead and task 0 then runs its real command with
|
|
686
|
+
the rest — cheaper than `--no-canary`, which risks N bad tasks to save one.
|
|
687
|
+
Failures don't consume the retry budget when caused by spot preemption — the
|
|
688
|
+
task just requeues.
|
|
689
|
+
|
|
690
|
+
**Waiting for capacity is not failure.** A task with no host yet keeps its
|
|
691
|
+
queued session, charges no retry, and is reported as blocked:
|
|
692
|
+
`fcloud sweep status` prints `blocked on capacity: N task(s), oldest 6m12s`.
|
|
693
|
+
That is the fleet scaling up, not your sweep breaking — cancel if the wait
|
|
694
|
+
is longer than the work is worth.
|
|
695
|
+
|
|
696
|
+
**Tracking & recovery:**
|
|
697
|
+
```bash
|
|
698
|
+
fcloud sweeps # all sweeps: state + done/run/fail counts
|
|
699
|
+
fcloud sweep status <name> # counts + failures clustered by error, with exemplar task
|
|
700
|
+
fcloud sweep status <name> --watch # same, repainted until terminal (--interval S)
|
|
701
|
+
fcloud sweep harvest <name> '*.json' ./out # every task's outputs → out/task-<i>/…
|
|
702
|
+
fcloud sweep logs <name> [--task N] # durable output (defaults to the exemplar failure; falls back to earlier attempts)
|
|
703
|
+
fcloud sweep set <name> --max-parallel N # change the fan-out cap on a live sweep
|
|
704
|
+
fcloud sweep retry <name> [--all] # re-run only failed tasks (fix code first: resubmit is idempotent)
|
|
705
|
+
fcloud sweep cancel <name> [--remaining] # stop; --remaining keeps running tasks (partial success)
|
|
706
|
+
fcloud sweep wait <name> # block until terminal; exit 0 on success
|
|
707
|
+
```
|
|
708
|
+
`--webhook URL` (or account default) POSTs the status document on canary
|
|
709
|
+
pass/fail and completion — agents should submit with `--no-wait --json` and
|
|
710
|
+
wake on the webhook instead of polling.
|
|
711
|
+
|
|
712
|
+
**The status JSON is a versioned public contract** (`schema_version: 1`),
|
|
713
|
+
specified in `docs/sweep-status-schema.md`: `counts` is an open map keyed by
|
|
714
|
+
task state, the task→session mapping is `tasks[].session_id`, sweep state is
|
|
715
|
+
`job.state`, and per-session spend is under `cost`. Additive changes keep the
|
|
716
|
+
version; a removal or rename bumps it. Do not reverse-engineer it — and
|
|
717
|
+
prefer `fcloud sweep harvest` over re-deriving the task→session→download walk
|
|
718
|
+
by hand.
|
|
719
|
+
|
|
720
|
+
**SDK mirror** — `client.map(command, args=…)`. The *type* of `args` picks
|
|
721
|
+
the combinator (no `:::` syntax in the SDK):
|
|
722
|
+
|
|
723
|
+
| `args` | Tasks | CLI equivalent |
|
|
724
|
+
|---|---|---|
|
|
725
|
+
| `{"lr": [1e-4, 3e-4], "bs": [32, 64]}` | 4 — cartesian product, binds `{lr}`/`{bs}` | `::: lr=… ::: bs=…` |
|
|
726
|
+
| `[{"model": "a", "tok": "x"}, {"model": "b", "tok": "y"}]` | 2 — one per row, pre-paired | `::: … :::+ …` |
|
|
727
|
+
| `["f1.json", "f2.json"]` | 2 — one per item, binds `{}` | `::: f1.json f2.json` |
|
|
728
|
+
| `500` or `range(500)` | 500 — same command N times; index via `{i}`/`FCLOUD_TASK_ID`, nothing appended | `::: {0..499}` |
|
|
729
|
+
| `None` (default) | 1 | — |
|
|
730
|
+
|
|
731
|
+
```python
|
|
732
|
+
sweep = client.map("python3 train.py --lr {lr} --bs {bs}",
|
|
733
|
+
args={"lr": [1e-4, 3e-4], "bs": [32, 64]}, # 4 tasks
|
|
734
|
+
name="lr-sweep", sku="gpu_1x_l4", code_dir=".")
|
|
735
|
+
sweep.wait_canary(); sweep.wait() # or poll sweep.status() / .retry_failed()
|
|
736
|
+
# from_session="s-…" instead of code_dir for Mode 1. Returns a SweepHandle.
|
|
737
|
+
```
|
|
738
|
+
|
|
739
|
+
---
|
|
740
|
+
|
|
741
|
+
## Agent Workflow Patterns
|
|
742
|
+
|
|
743
|
+
### Pattern 1: Quick one-off command
|
|
744
|
+
|
|
745
|
+
The simplest pattern. Run a command, get the result, done.
|
|
746
|
+
|
|
747
|
+
```bash
|
|
748
|
+
fcloud exec --sku gpu_1x_l4 --json nvidia-smi -L
|
|
749
|
+
# {"stdout": "GPU 0: NVIDIA L4 ...\n", "returncode": 0, "status": "exited", ...}
|
|
750
|
+
```
|
|
751
|
+
|
|
752
|
+
### Pattern 2: Run a training script
|
|
753
|
+
|
|
754
|
+
Upload code, run it, get results. Session lifecycle is fully managed.
|
|
755
|
+
|
|
756
|
+
```bash
|
|
757
|
+
fcloud run train.py --sku gpu_1x_l4 --json -- --epochs 50
|
|
758
|
+
# {"stdout": "...", "stderr": "", "returncode": 0, "duration_ms": 34521}
|
|
759
|
+
```
|
|
760
|
+
|
|
761
|
+
For a project directory with dependencies:
|
|
762
|
+
|
|
763
|
+
```bash
|
|
764
|
+
fcloud run . --script train.py --sku gpu_1x_l4 -- --batch-size 32
|
|
765
|
+
```
|
|
766
|
+
|
|
767
|
+
### Pattern 3: Persistent session (multi-step workflow)
|
|
768
|
+
|
|
769
|
+
Create a session once, interact with it across multiple commands. The session's
|
|
770
|
+
filesystem persists between invocations — no re-uploading. It comes online on the
|
|
771
|
+
first use and resumes transparently if it was reaped while idle.
|
|
772
|
+
|
|
773
|
+
```bash
|
|
774
|
+
# Create (returns a session ID; cold, $0 until first used)
|
|
775
|
+
fcloud create --sku gpu_1x_l4 --json
|
|
776
|
+
# {"session_id": "s-abc123", "status": "closed", "sku": "gpu_1x_l4"}
|
|
777
|
+
|
|
778
|
+
# Install dependencies (brings the session online automatically)
|
|
779
|
+
fcloud exec --on s-abc123 pip install torch transformers
|
|
780
|
+
|
|
781
|
+
# Upload training data
|
|
782
|
+
fcloud upload s-abc123 ./data
|
|
783
|
+
|
|
784
|
+
# Run training
|
|
785
|
+
fcloud run train.py --on s-abc123 -- --epochs 50 --lr 1e-4
|
|
786
|
+
|
|
787
|
+
# Same persistent-session target using the positional alias
|
|
788
|
+
fcloud run s-abc123 train.py -- --epochs 50 --lr 1e-4
|
|
789
|
+
|
|
790
|
+
# Check results
|
|
791
|
+
fcloud exec --on s-abc123 cat /workspace/results.json
|
|
792
|
+
|
|
793
|
+
# Download model weights
|
|
794
|
+
fcloud download s-abc123 /workspace/model.pt ./model.pt
|
|
795
|
+
|
|
796
|
+
# Halt spend now (files kept; resumes on next use)
|
|
797
|
+
fcloud stop s-abc123
|
|
798
|
+
```
|
|
799
|
+
|
|
800
|
+
Each command connects in ~1s, runs, and detaches. The session's files persist
|
|
801
|
+
across all invocations on the host's `/workspace/`.
|
|
802
|
+
|
|
803
|
+
This is the recommended pattern for agents running multi-step experiments —
|
|
804
|
+
cheaper and faster than creating a new session for every command.
|
|
805
|
+
|
|
806
|
+
Use `--wait` for commands that are expected to take longer than 30s but should
|
|
807
|
+
remain foreground from the agent's perspective. If the command detaches anyway,
|
|
808
|
+
follow the returned `process_id` instead of retrying the same command.
|
|
809
|
+
|
|
810
|
+
### Pattern 4: SDK session (programmatic multi-step)
|
|
811
|
+
|
|
812
|
+
For multi-step workflows (install deps → upload data → train → download results),
|
|
813
|
+
use the Python SDK directly. A session stays alive across all operations.
|
|
814
|
+
|
|
815
|
+
```python
|
|
816
|
+
from fcloud import Client, Image
|
|
817
|
+
|
|
818
|
+
client = Client()
|
|
819
|
+
image = Image.debian_slim().pip_install(["torch", "numpy"])
|
|
820
|
+
project = client.project("my-experiment", image=image)
|
|
821
|
+
|
|
822
|
+
with project.session(sku="gpu_1x_l4") as s:
|
|
823
|
+
# Install and verify
|
|
824
|
+
r = s.run(["python3", "-c", "import torch; print(torch.cuda.is_available())"])
|
|
825
|
+
print(r.stdout) # True
|
|
826
|
+
|
|
827
|
+
# Upload training data
|
|
828
|
+
s.upload("./data", "data")
|
|
829
|
+
|
|
830
|
+
# Run training
|
|
831
|
+
r = s.run(["python3", "/workspace/data/train.py"], timeout_ms=3600_000)
|
|
832
|
+
print(r.stdout)
|
|
833
|
+
|
|
834
|
+
# Download results
|
|
835
|
+
weights = s.download("model.pt")
|
|
836
|
+
open("model.pt", "wb").write(weights)
|
|
837
|
+
|
|
838
|
+
# Session closes automatically on exit
|
|
839
|
+
```
|
|
840
|
+
|
|
841
|
+
### Pattern 5: Long-running job with monitoring
|
|
842
|
+
|
|
843
|
+
For jobs that take minutes/hours, **lead with `spawn` + `wait`, not a maxed-out
|
|
844
|
+
`exec --wait`.** `spawn` returns a durable `process_id` immediately; `wait`
|
|
845
|
+
blocks on that id reading the durable log, so it survives host migration and
|
|
846
|
+
never depends on a foreground stream staying alive:
|
|
847
|
+
|
|
848
|
+
```bash
|
|
849
|
+
# Start the job — prints a durable proc-id right away, no blocking.
|
|
850
|
+
fcloud spawn s-abc123 python3 /workspace/train.py
|
|
851
|
+
# Process proc-456 spawned in session s-abc123
|
|
852
|
+
|
|
853
|
+
# Block until it exits (reads durable logs; survives migration). --timeout 0
|
|
854
|
+
# waits indefinitely; a positive timeout exits 75 (still running) so you retry.
|
|
855
|
+
# A process id the service has no record of (typo'd or stale) errors out
|
|
856
|
+
# with "unknown process id" (exit 1) instead of telling you to keep waiting.
|
|
857
|
+
fcloud wait s-abc123 proc-456 --timeout 0
|
|
858
|
+
|
|
859
|
+
# Or watch it live:
|
|
860
|
+
fcloud logs s-abc123 proc-456 --follow
|
|
861
|
+
# Fetch the full log after it finishes (default stream is combined):
|
|
862
|
+
fcloud logs s-abc123 proc-456 --output all
|
|
863
|
+
# Stop it if needed:
|
|
864
|
+
fcloud kill s-abc123 proc-456
|
|
865
|
+
```
|
|
866
|
+
|
|
867
|
+
**Don't max out `--wait` on a long job, and never pipe a live `exec` through
|
|
868
|
+
`| tail`.** `exec --wait 60m ... | tail` buffers all output until the process
|
|
869
|
+
ends (so you see nothing live) *and* swallows the detach line carrying the
|
|
870
|
+
`process_id` — leaving you unable to call `fcloud wait` / `fcloud logs --follow` at
|
|
871
|
+
all. If you must background a long `exec`, capture the id with `--emit-pid`
|
|
872
|
+
(`spawn` takes the same flag and writes the same file), which writes
|
|
873
|
+
`{session_id, process_id}` JSON to a file the instant the job starts —
|
|
874
|
+
readable even when stdout/stderr are piped or backgrounded:
|
|
875
|
+
|
|
876
|
+
```bash
|
|
877
|
+
fcloud exec --on s-abc123 --emit-pid /tmp/job.json --wait 60m python3 train.py &
|
|
878
|
+
# ... the file exists as soon as the process starts:
|
|
879
|
+
PID=$(python3 -c 'import json;print(json.load(open("/tmp/job.json"))["process_id"])')
|
|
880
|
+
fcloud wait s-abc123 "$PID" --timeout 0
|
|
881
|
+
```
|
|
882
|
+
|
|
883
|
+
> **Transient unreachability (`rc=75`).** A foreground `exec` can exit **`rc=75`**
|
|
884
|
+
> (EX_TEMPFAIL) when the session is briefly unreachable — most often the first
|
|
885
|
+
> exec right after a host boots, while the session is still binding to it, or a
|
|
886
|
+
> host reclaimed mid-run (spot preemption) that fcloud checkpoint-restores onto a
|
|
887
|
+
> new host. `exec` retries the attach for you and only reports `rc=75` once it
|
|
888
|
+
> still can't reach the session; the message states the real cause ("not
|
|
889
|
+
> reachable yet" vs. an ended session) instead of assuming a migration. Just
|
|
890
|
+
> retry — the next attempt lands on the ready/restored host. `fcloud wait` /
|
|
891
|
+
> `fcloud logs` ride a migration for you. After one, confirm the process is still
|
|
892
|
+
> alive (`fcloud logs <sid>` lists it as `running`) rather than assuming `rc=75`
|
|
893
|
+
> means your job is gone.
|
|
894
|
+
>
|
|
895
|
+
> **Migration rides honor `--wait`.** When a foreground `exec` rides a
|
|
896
|
+
> checkpoint/restore, the ride is bounded by your `--wait` budget — it never
|
|
897
|
+
> blocks past it (`--wait 0` disables the bound; the ride then uses the full
|
|
898
|
+
> ~10-minute restore window). If the budget runs out while the restore is
|
|
899
|
+
> still in flight, the result is **`status: "migrating"`** with **`rc=75`**:
|
|
900
|
+
> the process is neither confirmed alive nor dead yet. JSON callers can use
|
|
901
|
+
> the status to distinguish a ride-in-progress from a live process
|
|
902
|
+
> (`status: "running"`, `rc=0`, which means the command itself outlived
|
|
903
|
+
> `--wait`). Either way, follow up with `fcloud logs <sid> <pid> --follow` or
|
|
904
|
+
> `fcloud wait`.
|
|
905
|
+
|
|
906
|
+
With the SDK:
|
|
907
|
+
|
|
908
|
+
```python
|
|
909
|
+
from fcloud import Client
|
|
910
|
+
|
|
911
|
+
client = Client()
|
|
912
|
+
project = client.project("long-job")
|
|
913
|
+
|
|
914
|
+
with project.session(sku="gpu_1x_l4") as s:
|
|
915
|
+
s.upload("./project")
|
|
916
|
+
proc = s.spawn(["python3", "/workspace/project/train.py"])
|
|
917
|
+
|
|
918
|
+
# Poll periodically
|
|
919
|
+
import time
|
|
920
|
+
while proc.status.value == "running":
|
|
921
|
+
proc.poll(lines=20)
|
|
922
|
+
print(proc.output[-200:]) # last 200 chars
|
|
923
|
+
time.sleep(60)
|
|
924
|
+
|
|
925
|
+
print(f"Done: exit_code={proc.exit_code}")
|
|
926
|
+
print(s.logs(proc, output_range="all").output)
|
|
927
|
+
```
|
|
928
|
+
|
|
929
|
+
### Pattern 6: Stop now and come back later (file persistence)
|
|
930
|
+
|
|
931
|
+
Files written during a session are synced to durable cloud storage. After you
|
|
932
|
+
stop it (or it is reaped while idle), the session comes back online automatically
|
|
933
|
+
the next time you use it, with all files restored. There is no manual resume step.
|
|
934
|
+
|
|
935
|
+
```bash
|
|
936
|
+
# Create a session, do work
|
|
937
|
+
fcloud create --sku gpu_1x_l4
|
|
938
|
+
# Session s-abc123 created (cold — $0 until used)
|
|
939
|
+
|
|
940
|
+
fcloud exec --on s-abc123 python3 train.py
|
|
941
|
+
fcloud exec --on s-abc123 ls /workspace/
|
|
942
|
+
# model.pt metrics.json train.py
|
|
943
|
+
|
|
944
|
+
# Halt spend now (files sync to cloud storage)
|
|
945
|
+
fcloud stop s-abc123
|
|
946
|
+
|
|
947
|
+
# Hours later: just use it again — it comes back online automatically,
|
|
948
|
+
# with files restored from cloud storage.
|
|
949
|
+
fcloud exec --on s-abc123 ls /workspace/
|
|
950
|
+
# model.pt metrics.json train.py ← files restored from cloud storage
|
|
951
|
+
```
|
|
952
|
+
|
|
953
|
+
With the Python SDK:
|
|
954
|
+
|
|
955
|
+
```python
|
|
956
|
+
from fcloud import Client, Image
|
|
957
|
+
|
|
958
|
+
client = Client()
|
|
959
|
+
project = client.project("experiment", image=Image.debian_slim())
|
|
960
|
+
|
|
961
|
+
# First run: train on L4
|
|
962
|
+
with project.session(sku="gpu_1x_l4") as s:
|
|
963
|
+
s.run(["python3", "train.py"])
|
|
964
|
+
sid = s.session_id
|
|
965
|
+
|
|
966
|
+
# Later: just attach and use it again — a cold session comes back
|
|
967
|
+
# online automatically (rebuilt on a fresh host, files restored).
|
|
968
|
+
s2 = client.attach_session(sid)
|
|
969
|
+
s2.run(["ls", "/workspace/"]) # original files are here
|
|
970
|
+
s2.run(["python3", "finetune.py"])
|
|
971
|
+
s2.close()
|
|
972
|
+
```
|
|
973
|
+
|
|
974
|
+
**What survives a resume**: `/workspace` always survives. Changes *outside*
|
|
975
|
+
`/workspace` (e.g. `pip install` into the image's site-packages) survive an
|
|
976
|
+
ordinary same-image resume, but are **reset to the image** when the session
|
|
977
|
+
resumes with a changed `fcloud.json` image spec (a re-baseline) or when the
|
|
978
|
+
previous close could not finish syncing — the session prints an
|
|
979
|
+
`environment reset` banner when that happens. For packages that must survive
|
|
980
|
+
unconditionally, install them into a venv under the workspace:
|
|
981
|
+
|
|
982
|
+
```bash
|
|
983
|
+
fcloud exec --on s-abc123 'python3 -m venv /workspace/venv && /workspace/venv/bin/pip install torch'
|
|
984
|
+
fcloud exec --on s-abc123 /workspace/venv/bin/python train.py
|
|
985
|
+
```
|
|
986
|
+
|
|
987
|
+
Better yet, put packages in the image spec (`pip_install([...])`) so every
|
|
988
|
+
resume rebuilds them from the content-addressed image cache.
|
|
989
|
+
|
|
990
|
+
---
|
|
991
|
+
|
|
992
|
+
## Image Builder
|
|
993
|
+
|
|
994
|
+
Images define the environment (base OS, packages, env vars). They're declarative
|
|
995
|
+
and content-addressed — same layers = cache hit, instant startup.
|
|
996
|
+
|
|
997
|
+
```python
|
|
998
|
+
from fcloud import Image
|
|
999
|
+
|
|
1000
|
+
# GPU-ready image with PyTorch
|
|
1001
|
+
image = (
|
|
1002
|
+
Image.debian_slim()
|
|
1003
|
+
.apt_install(["git", "build-essential"])
|
|
1004
|
+
.pip_install(["torch", "transformers", "numpy"])
|
|
1005
|
+
.env({"PYTHONUNBUFFERED": "1"})
|
|
1006
|
+
)
|
|
1007
|
+
|
|
1008
|
+
project = client.project("my-project", image=image)
|
|
1009
|
+
```
|
|
1010
|
+
|
|
1011
|
+
Use `Image.from_registry(...)` when the workload needs a specific container base:
|
|
1012
|
+
|
|
1013
|
+
```python
|
|
1014
|
+
from fcloud import Image
|
|
1015
|
+
|
|
1016
|
+
# CUDA devel images include nvcc, ptxas, CUDA headers, and other build-time tools.
|
|
1017
|
+
image = (
|
|
1018
|
+
Image.from_registry("nvidia/cuda:12.8.1-devel-ubuntu24.04")
|
|
1019
|
+
.apt_install(["ninja-build"])
|
|
1020
|
+
.pip_install(["torch", "triton", "numpy"])
|
|
1021
|
+
.env({"PYTHONUNBUFFERED": "1"})
|
|
1022
|
+
)
|
|
1023
|
+
```
|
|
1024
|
+
|
|
1025
|
+
Reach for a CUDA `devel` base when packages build CUDA code from source
|
|
1026
|
+
(`torch.utils.cpp_extension`, flash-attn, xformers, mamba, custom kernels). Runtime
|
|
1027
|
+
CUDA/driver availability is not enough for those workflows; they need the compiler
|
|
1028
|
+
toolchain inside the image.
|
|
1029
|
+
|
|
1030
|
+
For GPU workloads, the base image must also provide a glibc new enough to load the
|
|
1031
|
+
host NVIDIA driver libraries injected by the runtime. This is distro-agnostic:
|
|
1032
|
+
Debian, Ubuntu, SUSE, Rocky, etc. can all work if their glibc satisfies the NVIDIA
|
|
1033
|
+
library requirement. Do not choose old GPU bases casually; failures usually show up
|
|
1034
|
+
as `GLIBC_2.xx not found`, CUDA initialization failures, or `nvidia-smi`/torch CUDA
|
|
1035
|
+
errors. Prefer a recent CUDA base, for example an NVIDIA CUDA image based on Ubuntu
|
|
1036
|
+
24.04, unless the user has a specific compatible base. For ordinary CPU/Python
|
|
1037
|
+
packages, `Image.debian_slim()` is usually faster to build and cache.
|
|
1038
|
+
|
|
1039
|
+
For PyTorch GPU images, `fcloud` may route pip installs through a PyTorch wheel
|
|
1040
|
+
index. Do not put system build tools such as `ninja` in pip requirements just
|
|
1041
|
+
because a package build mentions them; install `ninja-build` with apt instead.
|
|
1042
|
+
If a workload mixes PyTorch wheels with ordinary PyPI-only packages, split them
|
|
1043
|
+
into separate image layers or configure the package indexes explicitly.
|
|
1044
|
+
|
|
1045
|
+
Pre-built named images:
|
|
1046
|
+
|
|
1047
|
+
- `Image.from_name("agent-gpu")` — the unified default for every `gpu_*` SKU:
|
|
1048
|
+
`nvidia/cuda:12.8.1-devel-ubuntu24.04` (nvcc + CUDA headers, Python 3.12)
|
|
1049
|
+
with a pinned training stack (torch 2.8.0+cu128, transformers, trl, peft,
|
|
1050
|
+
accelerate, flash-attn prebuilt wheel). One image covers T4 through
|
|
1051
|
+
Blackwell (sm75–sm120). Note: flash-attn *kernels* need sm80+ — on T4 the
|
|
1052
|
+
import works but attention stays on sdpa, so avoid TRL
|
|
1053
|
+
`packing`/`padding_free` there. vLLM is part of the pinned stack
|
|
1054
|
+
(`import vllm` / `vllm serve` work out of the box; no extra installs).
|
|
1055
|
+
- `Image.from_name("agent-cpu")` — CPU-only ML stack
|
|
1056
|
+
- `Image.from_name("scientific")` — numpy, scipy, matplotlib, jupyter
|
|
1057
|
+
|
|
1058
|
+
The default image for CLI commands (`exec`, `run`) is `debian_slim` (Python 3.11).
|
|
1059
|
+
|
|
1060
|
+
---
|
|
1061
|
+
|
|
1062
|
+
## Project Config (`fcloud.json`)
|
|
1063
|
+
|
|
1064
|
+
Drop a `fcloud.json` in your project root to configure the image and default SKU.
|
|
1065
|
+
The CLI auto-discovers it by walking up from cwd (same as `.env`).
|
|
1066
|
+
|
|
1067
|
+
```json
|
|
1068
|
+
{
|
|
1069
|
+
"sku": "gpu_1x_l4",
|
|
1070
|
+
"volumes": [{"name": "checkpoints", "mount": "/workspace/checkpoints"}],
|
|
1071
|
+
"image": {
|
|
1072
|
+
"base": "nvidia/cuda:12.8.1-devel-ubuntu24.04",
|
|
1073
|
+
"apt": ["git", "build-essential", "ninja-build", "ffmpeg"],
|
|
1074
|
+
"pip": ["torch", "triton", "numpy", "transformers"],
|
|
1075
|
+
"env": {"PYTHONUNBUFFERED": "1"},
|
|
1076
|
+
"run": ["echo setup-complete"]
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
```
|
|
1080
|
+
|
|
1081
|
+
All fields are optional. Explicit `--sku` flags override the file; `--volume`
|
|
1082
|
+
flags merge with (and re-map by name) the `volumes` defaults.
|
|
1083
|
+
|
|
1084
|
+
|
|
1085
|
+
| Field | Description |
|
|
1086
|
+
| ------------ | ------------------------------------------ |
|
|
1087
|
+
| `sku` | Default hardware SKU for `exec` and `run` |
|
|
1088
|
+
| `volumes` | Volumes auto-attached to every `exec`/`run` session. Each entry is `"name"`, `"name:/mount"`, or `{"name": ..., "mount": ...}` |
|
|
1089
|
+
| `image.base` | Base image (default: `python:3.11-slim`) |
|
|
1090
|
+
| `image.apt` | Packages to `apt-get install` |
|
|
1091
|
+
| `image.pip` | Packages to `pip install` |
|
|
1092
|
+
| `image.env` | Environment variables baked into the image |
|
|
1093
|
+
| `image.run` | Shell commands to run during build |
|
|
1094
|
+
|
|
1095
|
+
|
|
1096
|
+
When `fcloud.json` is present, `fcloud exec` and `fcloud run` automatically build the
|
|
1097
|
+
specified image. The image is cached by content hash — same config = instant startup.
|
|
1098
|
+
|
|
1099
|
+
---
|
|
1100
|
+
|
|
1101
|
+
## Key Concepts
|
|
1102
|
+
|
|
1103
|
+
**SKU**: Hardware type identifier. Format: `gpu_<N>x_<family>` or `cpu_<N>x_<family>`.
|
|
1104
|
+
Examples: `gpu_1x_l4`, `gpu_8x_h100`, `cpu_2x_epyc`.
|
|
1105
|
+
|
|
1106
|
+
**Session**: A persistent workspace (filesystem at `/workspace/`) that runs commands
|
|
1107
|
+
in a sandboxed container on a host. A session is a filesystem, not a held GPU:
|
|
1108
|
+
|
|
1109
|
+
- *Throwaway*: created and released per command (`fcloud exec`, `fcloud run` without
|
|
1110
|
+
`--on`) — the filesystem is still saved and downloadable afterwards.
|
|
1111
|
+
- *Persistent*: created with `fcloud create`, interacted with via `--on <sid>`. It is
|
|
1112
|
+
cold ($0) until used, comes online automatically on use, and `fcloud stop` halts spend
|
|
1113
|
+
without deleting it.
|
|
1114
|
+
|
|
1115
|
+
**Job**: An *ephemeral* run-to-completion session (`fcloud job run`): its workspace is
|
|
1116
|
+
never persisted, it cannot resume, and its post-close outputs are volume commits,
|
|
1117
|
+
the exit code, and logs. Cheaper (no storage sync); dies on preemption and re-runs
|
|
1118
|
+
(`--retries`) instead of migrating. See the Jobs section.
|
|
1119
|
+
|
|
1120
|
+
Sessions resume transparently — a stopped or idle-reaped session is rebuilt on a fresh
|
|
1121
|
+
host (files restored from cloud storage) the next time you use it. You never run a resume command.
|
|
1122
|
+
|
|
1123
|
+
**Warmth**: `fcloud sessions` reports a session's state — `hot` (running, spending),
|
|
1124
|
+
`warm` (idle host held, spending), `stopping` (finishing teardown), `cold`
|
|
1125
|
+
(stopped, $0, resumes on use), or `preparing` (coming online).
|
|
1126
|
+
|
|
1127
|
+
**Image**: Declarative container spec. Layers are cached by content hash. First run
|
|
1128
|
+
on a host triggers a build (30–120s); subsequent runs with the same image are instant.
|
|
1129
|
+
|
|
1130
|
+
**Project**: Groups sessions with a shared image definition. Created implicitly by
|
|
1131
|
+
CLI commands or explicitly via `client.project("name", image=...)`.
|
|
1132
|
+
|
|
1133
|
+
---
|
|
1134
|
+
|
|
1135
|
+
## Tips for Agents
|
|
1136
|
+
|
|
1137
|
+
1. **Always use `--json`** when parsing output. Human-readable output may change format.
|
|
1138
|
+
2. **Use `--sku`** to target specific hardware. Without it, the scheduler picks any
|
|
1139
|
+
available host.
|
|
1140
|
+
3. **Use `fcloud create` for multi-step work**. One persistent session is faster and
|
|
1141
|
+
cheaper than creating a new ephemeral session for every command.
|
|
1142
|
+
4. **Treat `returncode: null` as detached, not failed**. Read `session_id` and
|
|
1143
|
+
`process_id`, then call `fcloud logs <sid> <pid> --follow` or poll the process.
|
|
1144
|
+
If stdout was truncated, call `fcloud logs <sid> <pid> --output all`.
|
|
1145
|
+
5. **Use `--wait` to express foreground patience**. Increase it for commands that
|
|
1146
|
+
should finish soon; let truly long jobs detach and monitor them by process ID.
|
|
1147
|
+
6. **Pass argv normally; quote a single string (or use `sh -c`) for shell
|
|
1148
|
+
expressions**. A single quoted argument runs as shell text via `bash -lc`.
|
|
1149
|
+
7. **Cached images are fast** (~2s). If your workflow creates sessions repeatedly with
|
|
1150
|
+
the same image, it's cheap.
|
|
1151
|
+
8. **Files live in `/workspace/` — and it's your default working directory.**
|
|
1152
|
+
Uploaded files are accessible as relative paths (`python train.py`) or as
|
|
1153
|
+
`/workspace/<filename>` / `/workspace/<dirname>/...`. Commands already run
|
|
1154
|
+
from `/workspace`, so don't `cd /root` or hardcode `/root/...` paths.
|
|
1155
|
+
9. **Pick a SKU explicitly** with `--sku`. If capacity is unavailable, the session
|
|
1156
|
+
queues until a host is provisioned rather than failing.
|
|
1157
|
+
10. **Use `fcloud stop` to halt spend now**. Ephemeral sessions (`exec`, `run`
|
|
1158
|
+
without `--on`) release automatically. Persistent sessions (`create`) keep their
|
|
1159
|
+
files; they stop on their own when idle, so `stop` is only needed to halt spend
|
|
1160
|
+
immediately.
|
|
1161
|
+
11. **Just use a session again to pick up where you left off**. Files persist across
|
|
1162
|
+
stop/idle via cloud storage, and the session comes back online automatically (no resume
|
|
1163
|
+
command). You can target different hardware with `--sku`.
|
|
1164
|
+
12. **Gracefully stop GPU engines when possible**. Prefer the engine's shutdown
|
|
1165
|
+
method or `fcloud kill <sid> <pid>` over in-session `pkill -9`; if GPU memory
|
|
1166
|
+
remains pinned after a hard kill, recover with `fcloud stop <sid>` and then use the
|
|
1167
|
+
session again.
|
|
1168
|
+
|
|
1169
|
+
## Anti-Patterns
|
|
1170
|
+
|
|
1171
|
+
|
|
1172
|
+
| Don't | Why |
|
|
1173
|
+
| ---------------------------------------------- | ------------------------------------------------------------------------------ |
|
|
1174
|
+
| Omit `--json` when parsing output | Human format is for display, not parsing |
|
|
1175
|
+
| Request unavailable SKUs | Session will queue until a host is provisioned; pick a SKU you expect to exist |
|
|
1176
|
+
| Retry after `returncode: null` | The original command is still running; follow its process ID |
|
|
1177
|
+
| Max out `exec --wait` on a long job | Prefer `spawn` + `fcloud wait`; a huge `--wait` blocks the foreground and depends on the stream staying alive |
|
|
1178
|
+
| 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` |
|
|
1179
|
+
| Treat a migration `rc=75` as "job died" | Spot preemption restores the process on a new host; retry the exec, or use `fcloud wait`/`logs` which ride the migration |
|
|
1180
|
+
| Immediately retry a preempted job | The system automatically attempts to requeue it; resubmitting races the requeue and duplicates the run |
|
|
1181
|
+
| Put local shell syntax outside quotes | The local shell handles it before `fcloud`; quote it or use `sh -c` |
|
|
1182
|
+
| Use `pkill -9` as the first response to a hung GPU engine | Hard kills can bypass framework cleanup; try graceful shutdown or `fcloud kill`, then `fcloud stop` and reuse if needed |
|
|
1183
|
+
| Run commands without `--sku` in production | May land on a CPU when you need a GPU |
|
|
1184
|
+
| Write a job's outputs only to `/workspace` | A job's workspace is discarded at close — write results to a `--volume` (or download while it runs); only volumes, logs, and the exit code survive |
|
|
1185
|
+
| Expect `fcloud ls`/`download`/resume on a closed job | Jobs never save a manifest; those refuse by design. Use `fcloud volume files <name>` and `fcloud job logs <id>` |
|