swm-gpu 0.2.4__tar.gz → 0.2.6__tar.gz
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.
- {swm_gpu-0.2.4 → swm_gpu-0.2.6}/.agents/skills/swm-gpu-workflow/SKILL.md +116 -24
- {swm_gpu-0.2.4 → swm_gpu-0.2.6}/CHANGELOG.md +39 -0
- {swm_gpu-0.2.4 → swm_gpu-0.2.6}/PKG-INFO +1 -1
- {swm_gpu-0.2.4 → swm_gpu-0.2.6}/pyproject.toml +1 -1
- swm_gpu-0.2.6/src/swm/__init__.py +1 -0
- {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/commands/_helpers.py +21 -9
- {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/guard.py +15 -4
- {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/sync/paths.py +8 -0
- swm_gpu-0.2.4/src/swm/__init__.py +0 -1
- {swm_gpu-0.2.4 → swm_gpu-0.2.6}/.agents/skills/swm-gpu-workflow/examples.md +0 -0
- {swm_gpu-0.2.4 → swm_gpu-0.2.6}/.gitignore +0 -0
- {swm_gpu-0.2.4 → swm_gpu-0.2.6}/CODE_OF_CONDUCT.md +0 -0
- {swm_gpu-0.2.4 → swm_gpu-0.2.6}/CONTRIBUTING.md +0 -0
- {swm_gpu-0.2.4 → swm_gpu-0.2.6}/LICENSE +0 -0
- {swm_gpu-0.2.4 → swm_gpu-0.2.6}/NOTICE +0 -0
- {swm_gpu-0.2.4 → swm_gpu-0.2.6}/README.md +0 -0
- {swm_gpu-0.2.4 → swm_gpu-0.2.6}/SECURITY.md +0 -0
- {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/bootstrap.py +0 -0
- {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/bootstrap_frameworks.py +0 -0
- {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/bootstrap_ssh.py +0 -0
- {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/cli.py +0 -0
- {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/commands/__init__.py +0 -0
- {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/commands/config.py +0 -0
- {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/commands/costs.py +0 -0
- {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/commands/guard.py +0 -0
- {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/commands/images.py +0 -0
- {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/commands/models.py +0 -0
- {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/commands/pod.py +0 -0
- {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/commands/pricing.py +0 -0
- {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/commands/remote.py +0 -0
- {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/commands/setup.py +0 -0
- {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/commands/storage.py +0 -0
- {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/commands/sync.py +0 -0
- {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/commands/use.py +0 -0
- {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/config.py +0 -0
- {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/costs/__init__.py +0 -0
- {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/costs/billing.py +0 -0
- {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/costs/budget.py +0 -0
- {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/costs/db.py +0 -0
- {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/costs/reconcile.py +0 -0
- {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/costs/tracker.py +0 -0
- {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/cuda.py +0 -0
- {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/frameworks/__init__.py +0 -0
- {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/frameworks/axolotl.py +0 -0
- {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/frameworks/comfyui.py +0 -0
- {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/frameworks/llm_studio.py +0 -0
- {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/frameworks/ollama.py +0 -0
- {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/frameworks/open_webui.py +0 -0
- {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/frameworks/swarmui.py +0 -0
- {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/frameworks/vllm_server.py +0 -0
- {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/images.py +0 -0
- {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/models/__init__.py +0 -0
- {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/models/civitai.py +0 -0
- {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/models/huggingface.py +0 -0
- {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/models/manifest.py +0 -0
- {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/models/resolver.py +0 -0
- {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/pricing/__init__.py +0 -0
- {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/pricing/calculator.py +0 -0
- {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/pricing/providers.py +0 -0
- {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/providers/__init__.py +0 -0
- {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/providers/aws.py +0 -0
- {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/providers/azure.py +0 -0
- {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/providers/base.py +0 -0
- {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/providers/coreweave.py +0 -0
- {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/providers/fluidstack.py +0 -0
- {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/providers/gcp.py +0 -0
- {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/providers/lambda_labs.py +0 -0
- {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/providers/runpod.py +0 -0
- {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/providers/tensordock.py +0 -0
- {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/providers/vastai.py +0 -0
- {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/providers/vultr.py +0 -0
- {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/remote/__init__.py +0 -0
- {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/remote/ssh.py +0 -0
- {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/storage/__init__.py +0 -0
- {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/storage/b2.py +0 -0
- {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/storage/base.py +0 -0
- {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/storage/gcs.py +0 -0
- {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/storage/s3.py +0 -0
- {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/sync/__init__.py +0 -0
- {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/sync/_autosync_daemon.sh +0 -0
- {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/sync/_common.py +0 -0
- {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/sync/autosync.py +0 -0
- {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/sync/preflight.py +0 -0
- {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/sync/pull.py +0 -0
- {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/sync/push.py +0 -0
- {swm_gpu-0.2.4 → swm_gpu-0.2.6}/src/swm/sync/watcher.py +0 -0
- {swm_gpu-0.2.4 → swm_gpu-0.2.6}/swm_main.py +0 -0
- {swm_gpu-0.2.4 → swm_gpu-0.2.6}/video/.gitignore +0 -0
- {swm_gpu-0.2.4 → swm_gpu-0.2.6}/video/README.md +0 -0
- {swm_gpu-0.2.4 → swm_gpu-0.2.6}/video/download-music.sh +0 -0
- {swm_gpu-0.2.4 → swm_gpu-0.2.6}/video/package-lock.json +0 -0
- {swm_gpu-0.2.4 → swm_gpu-0.2.6}/video/package.json +0 -0
- {swm_gpu-0.2.4 → swm_gpu-0.2.6}/video/remotion.config.ts +0 -0
- {swm_gpu-0.2.4 → swm_gpu-0.2.6}/video/src/Root.tsx +0 -0
- {swm_gpu-0.2.4 → swm_gpu-0.2.6}/video/src/components/ChapterCard.tsx +0 -0
- {swm_gpu-0.2.4 → swm_gpu-0.2.6}/video/src/components/EndCard.tsx +0 -0
- {swm_gpu-0.2.4 → swm_gpu-0.2.6}/video/src/components/GpuTable.tsx +0 -0
- {swm_gpu-0.2.4 → swm_gpu-0.2.6}/video/src/components/OutputBlock.tsx +0 -0
- {swm_gpu-0.2.4 → swm_gpu-0.2.6}/video/src/components/PHBadge.tsx +0 -0
- {swm_gpu-0.2.4 → swm_gpu-0.2.6}/video/src/components/ProviderCascade.tsx +0 -0
- {swm_gpu-0.2.4 → swm_gpu-0.2.6}/video/src/components/TerminalFrame.tsx +0 -0
- {swm_gpu-0.2.4 → swm_gpu-0.2.6}/video/src/components/TypingLine.tsx +0 -0
- {swm_gpu-0.2.4 → swm_gpu-0.2.6}/video/src/components/Wordmark.tsx +0 -0
- {swm_gpu-0.2.4 → swm_gpu-0.2.6}/video/src/compositions/Hero16x9.tsx +0 -0
- {swm_gpu-0.2.4 → swm_gpu-0.2.6}/video/src/compositions/Square1x1.tsx +0 -0
- {swm_gpu-0.2.4 → swm_gpu-0.2.6}/video/src/compositions/Vertical9x16.tsx +0 -0
- {swm_gpu-0.2.4 → swm_gpu-0.2.6}/video/src/config/music.ts +0 -0
- {swm_gpu-0.2.4 → swm_gpu-0.2.6}/video/src/script/scrubbed-logs.ts +0 -0
- {swm_gpu-0.2.4 → swm_gpu-0.2.6}/video/src/styles/tokens.ts +0 -0
- {swm_gpu-0.2.4 → swm_gpu-0.2.6}/video/tsconfig.json +0 -0
|
@@ -11,7 +11,7 @@ license: Apache-2.0
|
|
|
11
11
|
compatibility: "macOS or Linux, Python 3.11+, SSH client"
|
|
12
12
|
metadata:
|
|
13
13
|
author: swm-gpu
|
|
14
|
-
version: "0.2.
|
|
14
|
+
version: "0.2.4"
|
|
15
15
|
docs-url: https://swmgpu.com
|
|
16
16
|
repository: https://github.com/swm-gpu/swm
|
|
17
17
|
---
|
|
@@ -38,10 +38,11 @@ One CLI to search, provision, install, sync, and tear down GPU pods across 10 cl
|
|
|
38
38
|
## Phase 0 — State check
|
|
39
39
|
|
|
40
40
|
```bash
|
|
41
|
-
swm pod list && swm use --show
|
|
41
|
+
swm pod list && swm use --show # running pods + active-pod selection
|
|
42
|
+
swm storage ls # existing workspaces in the default bucket
|
|
42
43
|
```
|
|
43
44
|
|
|
44
|
-
If a matching pod is `RUNNING`, set it active (`swm use <name>`) and skip to Phase 5. Otherwise continue.
|
|
45
|
+
If a matching pod is `RUNNING`, set it active (`swm use <name>`) and skip to Phase 5. If a workspace prefix you want already exists in `swm storage ls`, restore it with `pod create -w <name>`. Otherwise continue.
|
|
45
46
|
|
|
46
47
|
## Phase 1 — Clarify
|
|
47
48
|
|
|
@@ -144,10 +145,11 @@ swm models pull <pod> ollama:llama3 # Ollama
|
|
|
144
145
|
swm models pull <pod> Comfy-Org/Wan_2.2_ComfyUI_Repackaged \
|
|
145
146
|
--as lora --filename split_files/loras/wan2.2_i2v_lightx2v_4steps_lora_v1_low_noise.safetensors
|
|
146
147
|
|
|
147
|
-
swm models
|
|
148
|
+
swm models search <query> [--sort downloads] [-n 20] # find models on HuggingFace Hub
|
|
149
|
+
swm models info <ref> # metadata before pulling (HF or Civitai)
|
|
148
150
|
swm models list <pod> [--all] # tracked (+ untracked) files
|
|
149
151
|
swm models link <pod> /abs/path/to/file --as vae
|
|
150
|
-
swm models remove <pod> <name>
|
|
152
|
+
swm models remove <pod> <name> [-y]
|
|
151
153
|
```
|
|
152
154
|
|
|
153
155
|
API keys (set once; legacy `hf_token` also read):
|
|
@@ -217,41 +219,75 @@ Unknown provider slugs raise `UsageError` listing valid ones.
|
|
|
217
219
|
swm pod create -p runpod -g h200 -n s1 -y
|
|
218
220
|
swm pod create -p vastai -g h200 -n train --gpu-count 4 --volume 200 --cloud-type SECURE -y
|
|
219
221
|
swm pod list # auto-prunes stale entries vs live provider state
|
|
220
|
-
swm pod status
|
|
222
|
+
swm pod status <pod> # GPU, cost, uptime + live sync health (autosync/watcher/last-push)
|
|
223
|
+
swm pod stop <pod> # halt, preserve volume (resume with pod start)
|
|
224
|
+
swm pod start <pod> # resume a stopped pod, wait for SSH
|
|
225
|
+
swm pod prune # drop config entries for pods gone from the provider
|
|
221
226
|
```
|
|
222
227
|
|
|
228
|
+
**Teardown — two distinct commands:**
|
|
229
|
+
|
|
230
|
+
```bash
|
|
231
|
+
swm pod down <pod> # SAFE: push /workspace → storage, then terminate
|
|
232
|
+
swm pod down <pod> --no-sync # skip the push (workspace already synced)
|
|
233
|
+
swm pod down <pod> --force-down # terminate even if push left no recent stamp (DANGEROUS)
|
|
234
|
+
swm pod terminate <pod> -y # DESTRUCTIVE: delete pod + volume, NO sync (irreversible)
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
`pod down` is the everyday teardown — it refuses to wipe `/workspace` or terminate unless the push wrote a fresh push stamp (guards against silent data loss). `pod terminate` skips all sync and deletes the volume; only use it for throwaway / `--no-storage` pods.
|
|
238
|
+
|
|
223
239
|
## Sync
|
|
224
240
|
|
|
225
241
|
```bash
|
|
226
|
-
swm sync pull [pod] # storage → pod
|
|
242
|
+
swm sync pull [pod] # storage → pod (non-destructive: skips existing, never deletes)
|
|
227
243
|
swm sync pull [pod] --tar # tarball mode (600k+ small files; fails loudly on error)
|
|
228
|
-
swm sync push [pod] # pod → storage
|
|
229
|
-
swm sync
|
|
230
|
-
swm sync
|
|
244
|
+
swm sync push [pod] # pod → storage (incremental)
|
|
245
|
+
swm sync push [pod] --force # kill any running transfer, re-baseline from scratch
|
|
246
|
+
swm sync push [pod] --delete # also delete remote files removed locally (watcher must be alive)
|
|
247
|
+
swm sync push [pod] --tar # pack workspace to one tarball object (fast for many small files)
|
|
248
|
+
swm sync status [pod] # s5cmd, watcher, autosync daemon, last-push stamp, pending changes
|
|
231
249
|
```
|
|
232
250
|
|
|
233
|
-
|
|
251
|
+
**Watcher vs daemon — two layers:**
|
|
252
|
+
|
|
253
|
+
```bash
|
|
254
|
+
swm sync watch [pod] # start the inotify file-change watcher only (records what changed)
|
|
255
|
+
swm sync watch [pod] --stop # stop the watcher
|
|
256
|
+
swm sync auto [pod] # start the background daemon: reads watcher log every 60s, pushes
|
|
257
|
+
swm sync auto [pod] -i 30 # custom interval (seconds)
|
|
258
|
+
swm sync auto [pod] --status # daemon state + recent log tail
|
|
259
|
+
swm sync auto [pod] --stop # stop the daemon
|
|
260
|
+
swm sync auto [pod] --force # start without a prior successful pull/push (DANGEROUS — see below)
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
`swm pod create -w <name>` wires up the watcher + auto daemon for you; reach for `sync watch` / `sync auto` only to inspect or recover. **`sync auto --force` is dangerous**: it bypasses the safety check that requires a known-good baseline, so local deletions can propagate to storage. Only force when you've just `sync push --force`ed and know the pod is authoritative.
|
|
264
|
+
|
|
265
|
+
Three-tier sync: inotify watcher → incremental s5cmd → tar. Push reconciles inotify events with a `find -newer` scan so `pip install` bursts aren't missed (and falls back to a full `find` count when the watcher is dead). Pull re-runs framework link-repair steps so `/workspace/models/` symlinks stay wired after restore.
|
|
234
266
|
|
|
235
267
|
## Guard
|
|
236
268
|
|
|
237
269
|
```bash
|
|
238
|
-
swm guard set <pod> --mode auto-down --idle-timeout 30 # set/update policy
|
|
270
|
+
swm guard set <pod> --mode auto-down --idle-timeout 30 # set/update policy (auto-down/auto-stop/remind/manual)
|
|
239
271
|
swm guard set <pod> --mode remind # notify-only
|
|
240
272
|
swm guard disable <pod> [--force-manual] # remove override / pin manual
|
|
241
|
-
swm guard defaults --mode auto-down --idle-timeout 30 #
|
|
242
|
-
swm guard list # live status
|
|
243
|
-
swm guard
|
|
273
|
+
swm guard defaults --mode auto-down --idle-timeout 30 # default policy applied to new pods
|
|
274
|
+
swm guard list # all guarded pods + live idle status
|
|
275
|
+
swm guard run [pods…] [--once] [--interval N] # run the loop in foreground (debug/cron)
|
|
276
|
+
swm guard stop-daemon # stop the background daemon
|
|
244
277
|
```
|
|
245
278
|
|
|
246
|
-
Monitors 4 signals: SSH sessions (`who`), GPU utilization, filesystem writes, active processes. Setting `auto-stop`/`auto-down` auto-starts the local daemon. `--idle-timeout 0` and `--poll-interval 0` are honored as explicit values (not unset).
|
|
279
|
+
Monitors 4 signals: SSH sessions (`who`), GPU utilization, filesystem writes, active processes. Setting `auto-stop`/`auto-down` auto-starts the local daemon; `guard run` is the explicit foreground runner (use `--once` for a single sweep). `--idle-timeout 0` and `--poll-interval 0` are honored as explicit values (not unset). `auto-down` runs the same push-stamp safety check as `pod down` — it leaves the pod alive rather than terminate on a partial push.
|
|
247
280
|
|
|
248
281
|
## Costs
|
|
249
282
|
|
|
250
283
|
```bash
|
|
251
|
-
swm costs live # right-now burn
|
|
284
|
+
swm costs live # right-now burn across active pods
|
|
252
285
|
swm costs summary [--period today|...] # breakdown by provider/pod (today = UTC day)
|
|
253
|
-
swm costs
|
|
254
|
-
swm costs
|
|
286
|
+
swm costs log [-n 50] [-p vastai] # detailed per-session history
|
|
287
|
+
swm costs reconcile [-p vastai] # compare local tracking vs provider card charges
|
|
288
|
+
swm costs budget set 100 [-s global] [-t month] # set a spending alert
|
|
289
|
+
swm costs budget show # active budgets + current spend
|
|
290
|
+
swm costs budget remove <scope> [-t month] # remove a budget
|
|
255
291
|
```
|
|
256
292
|
|
|
257
293
|
## Remote
|
|
@@ -266,8 +302,9 @@ swm download [pod] remote/file -d ~/Downloads
|
|
|
266
302
|
## Pricing reference
|
|
267
303
|
|
|
268
304
|
```bash
|
|
269
|
-
swm pricing compare --gpu h100|h200|b200|a100|rtx-4090|l40s
|
|
270
|
-
swm pricing estimate -g h200 --hours 24
|
|
305
|
+
swm pricing compare --gpu h100|h200|b200|a100|rtx-4090|l40s # per-GPU/hr across providers
|
|
306
|
+
swm pricing estimate -g h200 --hours 24 [--storage GB] [--tier secure] # projected workload cost
|
|
307
|
+
swm pricing specs --gpu h200 # hardware specs (VRAM, bandwidth) side-by-side
|
|
271
308
|
```
|
|
272
309
|
|
|
273
310
|
## Instance ID format
|
|
@@ -289,18 +326,70 @@ swm pricing estimate -g h200 --hours 24
|
|
|
289
326
|
| `azure` | az CLI (`pip install swm-gpu[azure]`) | yes |
|
|
290
327
|
| `coreweave` | Kubernetes | yes |
|
|
291
328
|
|
|
292
|
-
## Storage
|
|
329
|
+
## Storage (buckets & workspaces)
|
|
330
|
+
|
|
331
|
+
Inspect and manage cloud-storage buckets directly — **no running pod required**. Top-level prefixes in your default bucket are your workspace names, so `swm storage ls` is the fastest "what's in my bucket?" probe.
|
|
332
|
+
|
|
333
|
+
```bash
|
|
334
|
+
swm storage list # configured buckets across all providers
|
|
335
|
+
swm storage ls # default bucket root = workspace names
|
|
336
|
+
swm storage ls studio-live/ # drill into one workspace (sizes/mtimes show here)
|
|
337
|
+
swm storage ls -b b2:other-bucket # explicit provider:bucket
|
|
338
|
+
swm storage create my-models -p b2 # create a bucket (-l location, -c storage-class)
|
|
339
|
+
swm storage upload ./file remote/path # one-off upload
|
|
340
|
+
swm storage download remote/path ./file # one-off download
|
|
341
|
+
swm storage rm workspace2/ --dry-run # COUNT objects under a prefix, delete nothing
|
|
342
|
+
swm storage rm workspace2/ -y # delete all objects under prefix (S3 batch delete)
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
`storage rm` uses the S3 batch-delete API (1000 objects/request) and prompts before deleting unless `-y`. It's a hard delete on S3-compatible backends (B2/S3) — always `--dry-run` first. Top-level `ls` shows `DIR` with no size/mtime (B2 `CommonPrefixes` carry neither); drill into a prefix for real sizes.
|
|
293
346
|
|
|
294
|
-
S3-compatible via s5cmd
|
|
347
|
+
Backend config keys (S3-compatible via s5cmd):
|
|
295
348
|
|
|
296
349
|
| Backend | Keys |
|
|
297
350
|
|---|---|
|
|
298
|
-
| Backblaze B2 | `b2.key_id`, `b2.app_key`, `b2.bucket` |
|
|
351
|
+
| Backblaze B2 | `b2.key_id`, `b2.app_key`, `b2.bucket`, `b2.s3_endpoint` |
|
|
299
352
|
| Amazon S3 | `s3.access_key`, `s3.secret_key`, `s3.bucket` |
|
|
300
353
|
| Google GCS | `gcs.hmac_access`, `gcs.hmac_secret`, `gcp.bucket` |
|
|
301
354
|
|
|
302
355
|
Default: `swm config set storage.default b2:<bucket>`. Credentials are masked by suffix in `config get/list` and never written to the pod (passed as transient env vars per s5cmd call).
|
|
303
356
|
|
|
357
|
+
## Frameworks & workspace attach (setup)
|
|
358
|
+
|
|
359
|
+
```bash
|
|
360
|
+
swm setup list # available frameworks
|
|
361
|
+
swm setup install <name> [pod] # idempotent install into its own venv
|
|
362
|
+
swm setup start <name> [pod] [-p port] [--model M] [--extra-args "…"]
|
|
363
|
+
swm setup stop <name> [pod]
|
|
364
|
+
swm setup storage [pod] [-p b2|gcs|s3|all] # install s5cmd + configure creds on an existing pod
|
|
365
|
+
swm setup workspace [pod] [-n name] [-b provider:bucket] [--force]
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
**`swm setup workspace` is the recovery command** for the silent-data-loss scenario: it does the full workspace bootstrap in one shot (installs s5cmd, configures creds, pulls or initializes the workspace, starts the auto-sync daemon) and persists the pod ↔ workspace ↔ bucket association. Use it when:
|
|
369
|
+
- you created a pod with `--no-storage` and now want persistence,
|
|
370
|
+
- `pod create`'s SSH probe timed out so bootstrap was skipped,
|
|
371
|
+
- you want to reattach a pod to a different workspace (`--force`).
|
|
372
|
+
|
|
373
|
+
## Config
|
|
374
|
+
|
|
375
|
+
```bash
|
|
376
|
+
swm config set <key> <value> # e.g. swm config set runpod.api_key sk-xxx
|
|
377
|
+
swm config get <key> # single value (secrets suffix-masked)
|
|
378
|
+
swm config list # all values (secrets masked)
|
|
379
|
+
swm config delete <key> # remove a key
|
|
380
|
+
swm config path # location of the config file
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
Common keys: provider `<slug>.api_key`; storage `b2.*` / `s3.*` / `gcs.*` + `storage.default`; model tokens `hf.api_key`, `civitai.api_key`; guard `guard.*` defaults.
|
|
384
|
+
|
|
385
|
+
## Images
|
|
386
|
+
|
|
387
|
+
```bash
|
|
388
|
+
swm images list [-p provider] [--cuda 12.8] [--refresh] [-n 30] [--all]
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
Lists Docker images discoverable for a provider (newest-first) for use with `swm pod create --image`. `--refresh` re-queries the provider instead of using cached results.
|
|
392
|
+
|
|
304
393
|
---
|
|
305
394
|
|
|
306
395
|
## Anti-patterns
|
|
@@ -314,4 +403,7 @@ Default: `swm config set storage.default b2:<bucket>`. Credentials are masked by
|
|
|
314
403
|
- **Never tear down before `du -sh /workspace`** — 10–30 GB bloat extends the next pull.
|
|
315
404
|
- **Never guess nested HF file paths** — run `swm models info <repo>` and copy the exact `rfilename`.
|
|
316
405
|
- **Never skip Phase 0** — a forgotten running pod bills silently.
|
|
406
|
+
- **Never confuse `pod down` with `pod terminate`** — `down` syncs `/workspace` first; `terminate` deletes the volume with no sync. Use `terminate` only on `--no-storage` / throwaway pods.
|
|
407
|
+
- **Never trust a green `sync push` alone** — confirm `swm pod status` shows a recent last-push stamp and autosync running before tearing down. A push can exit 0 yet skip the stamp on a partial transfer.
|
|
408
|
+
- **Never use `swm storage rm` without `--dry-run` first** — it's a hard S3 batch delete with no undo.
|
|
317
409
|
- **Never hand off** before Phase 5 passes.
|
|
@@ -6,6 +6,45 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.2.6] - 2026-06-16
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
- **Auto-down never fired on workspace-backed pods (cost bug).** The lifecycle
|
|
13
|
+
guard's filesystem-activity signal keyed off the mtime of the autosync watcher
|
|
14
|
+
log (`/workspace/.swm_changes.log`), which autosync rewrites every cycle — so
|
|
15
|
+
pods were reported perpetually "active," the idle timer never elapsed, and
|
|
16
|
+
`auto-down`/`auto-stop` never triggered. The guard now detects *real*
|
|
17
|
+
workspace writes via `find`, excluding swm bookkeeping (`.swm_*`,
|
|
18
|
+
`.swm_guard`), build caches (`.uv-cache`, `.cache`, `__pycache__`), logs,
|
|
19
|
+
`terminfo`, `.git`, and `.nv`.
|
|
20
|
+
- `swm setup start` no longer crashes with a traceback when the post-start
|
|
21
|
+
health probe hits a connection reset. `_probe_url` caught `ConnectError`/
|
|
22
|
+
`TimeoutException` but not `httpx.ReadError` ("connection reset by peer" while
|
|
23
|
+
a freshly-opened SSH tunnel settles); it now catches all `httpx.HTTPError`.
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
- Workspace sync now excludes `/.uv-cache/` (uv build/wheel cache) and
|
|
27
|
+
`/terminfo/` (managed-CPython terminal database). The former churned
|
|
28
|
+
constantly and caused delete-reconciliation failures and ~11 GB of wasted
|
|
29
|
+
storage; the latter's dedup-hardlinked files broke hardlink staging.
|
|
30
|
+
|
|
31
|
+
### Docs
|
|
32
|
+
- Rewrote the `swm-gpu-workflow` skill for full command coverage — added the
|
|
33
|
+
entire `swm storage` group, `swm config`, `swm images`, `swm sync auto`,
|
|
34
|
+
`swm setup workspace`/`storage`, `pod terminate` vs `pod down`, and more.
|
|
35
|
+
|
|
36
|
+
## [0.2.5] - 2026-05-29
|
|
37
|
+
|
|
38
|
+
### Fixed
|
|
39
|
+
- Framework SSH tunnels (e.g. `swm setup start comfyui`) now use the
|
|
40
|
+
configured SSH identity. `_open_tunnel` built a bare `ssh` command without
|
|
41
|
+
`-i <key_path>`, so when a custom `ssh.key_path` (or `<provider>.ssh_key`)
|
|
42
|
+
was set it silently fell back to the default key, failed authentication, and
|
|
43
|
+
— because of `ExitOnForwardFailure=yes` with output discarded — exited
|
|
44
|
+
immediately without forwarding the port. The tunnel now resolves its
|
|
45
|
+
host/port/user/key via the same path as `swm run`/`swm ssh`, so it also
|
|
46
|
+
targets the direct IP and mapped port-22 endpoint when available.
|
|
47
|
+
|
|
9
48
|
## [0.2.4] - 2026-05-27
|
|
10
49
|
|
|
11
50
|
### Fixed
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.2.6"
|
|
@@ -180,29 +180,36 @@ def _open_tunnel(inst, ports: dict[int, str]) -> list[int] | None:
|
|
|
180
180
|
"""
|
|
181
181
|
import subprocess
|
|
182
182
|
|
|
183
|
+
from swm.remote.ssh import _ssh_config_for
|
|
184
|
+
|
|
183
185
|
if not inst.ssh_host:
|
|
184
186
|
return None
|
|
185
187
|
unmapped = [p for p in ports if p not in (inst.ports or {})]
|
|
186
188
|
if not unmapped:
|
|
187
189
|
return None
|
|
188
190
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
+
# Resolve host/port/user/key the same way `swm run`/`swm ssh` do, so the
|
|
192
|
+
# tunnel uses the configured identity (e.g. ssh.key_path) and the direct
|
|
193
|
+
# IP + mapped port-22 when available. Building a bare `ssh` command here
|
|
194
|
+
# silently fell back to default keys and could miss a custom key.
|
|
195
|
+
c = _ssh_config_for(inst)
|
|
191
196
|
|
|
192
|
-
cmd = [
|
|
193
|
-
|
|
197
|
+
cmd = ["ssh", "-N"]
|
|
198
|
+
if c["key_path"]:
|
|
199
|
+
cmd.extend(["-i", str(c["key_path"])])
|
|
200
|
+
cmd.extend([
|
|
194
201
|
"-o", "StrictHostKeyChecking=no",
|
|
195
202
|
"-o", "UserKnownHostsFile=/dev/null",
|
|
196
203
|
"-o", "LogLevel=ERROR",
|
|
197
204
|
"-o", "ExitOnForwardFailure=yes",
|
|
198
205
|
"-o", "ServerAliveInterval=30",
|
|
199
206
|
"-o", "ServerAliveCountMax=3",
|
|
200
|
-
]
|
|
207
|
+
])
|
|
201
208
|
for p in unmapped:
|
|
202
209
|
cmd.extend(["-L", f"{p}:localhost:{p}"])
|
|
203
|
-
if
|
|
204
|
-
cmd.extend(["-p", str(
|
|
205
|
-
cmd.append(f"{user}@{
|
|
210
|
+
if c["port"] != 22:
|
|
211
|
+
cmd.extend(["-p", str(c["port"])])
|
|
212
|
+
cmd.append(f"{c['user']}@{c['host']}")
|
|
206
213
|
|
|
207
214
|
subprocess.Popen(
|
|
208
215
|
cmd,
|
|
@@ -226,7 +233,12 @@ def _probe_url(url: str, timeout: int = 60) -> bool:
|
|
|
226
233
|
r = httpx.get(url, timeout=5, follow_redirects=True)
|
|
227
234
|
if r.status_code < 500:
|
|
228
235
|
return True
|
|
229
|
-
except (httpx.
|
|
236
|
+
except (httpx.HTTPError, OSError):
|
|
237
|
+
# httpx.HTTPError covers all transport-level failures —
|
|
238
|
+
# ConnectError, ReadError ("connection reset by peer" while a
|
|
239
|
+
# freshly-opened SSH tunnel settles), WriteError, TimeoutException,
|
|
240
|
+
# RemoteProtocolError, etc. A probe must never crash the command;
|
|
241
|
+
# an unreachable URL just means "not ready yet, retry".
|
|
230
242
|
pass
|
|
231
243
|
remaining = deadline - time.monotonic()
|
|
232
244
|
time.sleep(min(interval, max(remaining, 0)))
|
|
@@ -81,10 +81,21 @@ def avg_gpu_util() -> float:
|
|
|
81
81
|
|
|
82
82
|
|
|
83
83
|
def recent_fs_write(now: float) -> bool:
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
84
|
+
# Detect REAL workspace writes (user data, generation output, installs),
|
|
85
|
+
# excluding swm's own bookkeeping (autosync change-log/stamps/guard), build
|
|
86
|
+
# caches, logs, and framework noise. Keying off WATCH_LOG mtime was a bug:
|
|
87
|
+
# the autosync watcher rewrites that file every cycle, so the pod looked
|
|
88
|
+
# perpetually active and auto-down could never fire.
|
|
89
|
+
minutes = max(2, int(max(POLL * 2, 120) / 60))
|
|
90
|
+
out = sh(
|
|
91
|
+
"find /workspace -mindepth 1 -mmin -%d -type f "
|
|
92
|
+
"-not -path '*/.swm_*' -not -path '*/.swm_guard/*' "
|
|
93
|
+
"-not -path '*/.uv-cache/*' -not -path '*/.cache/*' "
|
|
94
|
+
"-not -path '*/__pycache__/*' -not -name '*.log' "
|
|
95
|
+
"-not -path '*/terminfo/*' -not -path '*/.git/*' "
|
|
96
|
+
"-not -path '*/.nv/*' 2>/dev/null | head -1" % minutes
|
|
97
|
+
)
|
|
98
|
+
return bool(out.strip())
|
|
88
99
|
|
|
89
100
|
|
|
90
101
|
def transfer_locked() -> bool:
|
|
@@ -43,4 +43,12 @@ WATCH_EXCLUDES: tuple[str, ...] = (
|
|
|
43
43
|
r"\.log$",
|
|
44
44
|
r"/\.cache/",
|
|
45
45
|
r"/\.nv/",
|
|
46
|
+
# uv's build/wheel cache (workspace-owned uv from v0.2.x). Regenerable,
|
|
47
|
+
# and its ephemeral builds-v0/.tmp* dirs churn constantly — syncing it
|
|
48
|
+
# both wastes storage and causes delete-reconciliation failures.
|
|
49
|
+
r"/\.uv-cache/",
|
|
50
|
+
# Managed-CPython terminfo database (workspace-owned python). Its entries
|
|
51
|
+
# are dedup-hardlinked, which breaks hardlink-staging (s5cmd then can't
|
|
52
|
+
# open them → push fails). Non-essential for any venv/ML workload.
|
|
53
|
+
r"/terminfo/",
|
|
46
54
|
)
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.2.4"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|