swm-gpu 0.2.2__tar.gz → 0.2.4__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/.agents/skills/swm-gpu-workflow/SKILL.md +317 -0
- swm_gpu-0.2.4/.agents/skills/swm-gpu-workflow/examples.md +68 -0
- {swm_gpu-0.2.2 → swm_gpu-0.2.4}/CHANGELOG.md +56 -0
- {swm_gpu-0.2.2 → swm_gpu-0.2.4}/PKG-INFO +10 -1
- {swm_gpu-0.2.2 → swm_gpu-0.2.4}/pyproject.toml +8 -2
- swm_gpu-0.2.4/src/swm/__init__.py +1 -0
- {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/bootstrap.py +60 -16
- {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/cli.py +9 -2
- {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/commands/_helpers.py +18 -4
- {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/commands/config.py +22 -8
- {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/commands/costs.py +4 -6
- {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/commands/guard.py +26 -3
- {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/commands/images.py +1 -1
- {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/commands/models.py +75 -56
- {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/commands/pod.py +139 -24
- {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/commands/pricing.py +5 -3
- {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/commands/setup.py +0 -29
- {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/commands/sync.py +57 -4
- {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/commands/use.py +8 -2
- {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/frameworks/swarmui.py +8 -5
- {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/guard.py +35 -5
- {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/models/manifest.py +8 -3
- {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/pricing/providers.py +96 -0
- {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/sync/autosync.py +9 -0
- {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/sync/pull.py +6 -4
- {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/sync/push.py +61 -13
- swm_gpu-0.2.2/.agents/skills/swm-gpu-workflow/SKILL.md +0 -276
- swm_gpu-0.2.2/src/swm/__init__.py +0 -1
- {swm_gpu-0.2.2 → swm_gpu-0.2.4}/.gitignore +0 -0
- {swm_gpu-0.2.2 → swm_gpu-0.2.4}/CODE_OF_CONDUCT.md +0 -0
- {swm_gpu-0.2.2 → swm_gpu-0.2.4}/CONTRIBUTING.md +0 -0
- {swm_gpu-0.2.2 → swm_gpu-0.2.4}/LICENSE +0 -0
- {swm_gpu-0.2.2 → swm_gpu-0.2.4}/NOTICE +0 -0
- {swm_gpu-0.2.2 → swm_gpu-0.2.4}/README.md +0 -0
- {swm_gpu-0.2.2 → swm_gpu-0.2.4}/SECURITY.md +0 -0
- {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/bootstrap_frameworks.py +0 -0
- {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/bootstrap_ssh.py +0 -0
- {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/commands/__init__.py +0 -0
- {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/commands/remote.py +0 -0
- {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/commands/storage.py +0 -0
- {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/config.py +0 -0
- {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/costs/__init__.py +0 -0
- {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/costs/billing.py +0 -0
- {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/costs/budget.py +0 -0
- {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/costs/db.py +0 -0
- {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/costs/reconcile.py +0 -0
- {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/costs/tracker.py +0 -0
- {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/cuda.py +0 -0
- {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/frameworks/__init__.py +0 -0
- {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/frameworks/axolotl.py +0 -0
- {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/frameworks/comfyui.py +0 -0
- {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/frameworks/llm_studio.py +0 -0
- {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/frameworks/ollama.py +0 -0
- {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/frameworks/open_webui.py +0 -0
- {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/frameworks/vllm_server.py +0 -0
- {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/images.py +0 -0
- {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/models/__init__.py +0 -0
- {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/models/civitai.py +0 -0
- {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/models/huggingface.py +0 -0
- {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/models/resolver.py +0 -0
- {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/pricing/__init__.py +0 -0
- {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/pricing/calculator.py +0 -0
- {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/providers/__init__.py +0 -0
- {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/providers/aws.py +0 -0
- {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/providers/azure.py +0 -0
- {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/providers/base.py +0 -0
- {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/providers/coreweave.py +0 -0
- {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/providers/fluidstack.py +0 -0
- {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/providers/gcp.py +0 -0
- {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/providers/lambda_labs.py +0 -0
- {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/providers/runpod.py +0 -0
- {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/providers/tensordock.py +0 -0
- {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/providers/vastai.py +0 -0
- {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/providers/vultr.py +0 -0
- {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/remote/__init__.py +0 -0
- {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/remote/ssh.py +0 -0
- {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/storage/__init__.py +0 -0
- {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/storage/b2.py +0 -0
- {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/storage/base.py +0 -0
- {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/storage/gcs.py +0 -0
- {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/storage/s3.py +0 -0
- {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/sync/__init__.py +0 -0
- {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/sync/_autosync_daemon.sh +0 -0
- {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/sync/_common.py +0 -0
- {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/sync/paths.py +0 -0
- {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/sync/preflight.py +0 -0
- {swm_gpu-0.2.2 → swm_gpu-0.2.4}/src/swm/sync/watcher.py +0 -0
- {swm_gpu-0.2.2 → swm_gpu-0.2.4}/swm_main.py +0 -0
- {swm_gpu-0.2.2 → swm_gpu-0.2.4}/video/.gitignore +0 -0
- {swm_gpu-0.2.2 → swm_gpu-0.2.4}/video/README.md +0 -0
- {swm_gpu-0.2.2 → swm_gpu-0.2.4}/video/download-music.sh +0 -0
- {swm_gpu-0.2.2 → swm_gpu-0.2.4}/video/package-lock.json +0 -0
- {swm_gpu-0.2.2 → swm_gpu-0.2.4}/video/package.json +0 -0
- {swm_gpu-0.2.2 → swm_gpu-0.2.4}/video/remotion.config.ts +0 -0
- {swm_gpu-0.2.2 → swm_gpu-0.2.4}/video/src/Root.tsx +0 -0
- {swm_gpu-0.2.2 → swm_gpu-0.2.4}/video/src/components/ChapterCard.tsx +0 -0
- {swm_gpu-0.2.2 → swm_gpu-0.2.4}/video/src/components/EndCard.tsx +0 -0
- {swm_gpu-0.2.2 → swm_gpu-0.2.4}/video/src/components/GpuTable.tsx +0 -0
- {swm_gpu-0.2.2 → swm_gpu-0.2.4}/video/src/components/OutputBlock.tsx +0 -0
- {swm_gpu-0.2.2 → swm_gpu-0.2.4}/video/src/components/PHBadge.tsx +0 -0
- {swm_gpu-0.2.2 → swm_gpu-0.2.4}/video/src/components/ProviderCascade.tsx +0 -0
- {swm_gpu-0.2.2 → swm_gpu-0.2.4}/video/src/components/TerminalFrame.tsx +0 -0
- {swm_gpu-0.2.2 → swm_gpu-0.2.4}/video/src/components/TypingLine.tsx +0 -0
- {swm_gpu-0.2.2 → swm_gpu-0.2.4}/video/src/components/Wordmark.tsx +0 -0
- {swm_gpu-0.2.2 → swm_gpu-0.2.4}/video/src/compositions/Hero16x9.tsx +0 -0
- {swm_gpu-0.2.2 → swm_gpu-0.2.4}/video/src/compositions/Square1x1.tsx +0 -0
- {swm_gpu-0.2.2 → swm_gpu-0.2.4}/video/src/compositions/Vertical9x16.tsx +0 -0
- {swm_gpu-0.2.2 → swm_gpu-0.2.4}/video/src/config/music.ts +0 -0
- {swm_gpu-0.2.2 → swm_gpu-0.2.4}/video/src/script/scrubbed-logs.ts +0 -0
- {swm_gpu-0.2.2 → swm_gpu-0.2.4}/video/src/styles/tokens.ts +0 -0
- {swm_gpu-0.2.2 → swm_gpu-0.2.4}/video/tsconfig.json +0 -0
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: swm-gpu-workflow
|
|
3
|
+
description: >
|
|
4
|
+
Use when the user wants to search cloud GPUs across providers, provision a
|
|
5
|
+
GPU pod, install AI frameworks (ComfyUI, SwarmUI, vLLM, Ollama, Open WebUI,
|
|
6
|
+
Axolotl, H2O LLM Studio), pull models from HuggingFace / Civitai / URLs to a
|
|
7
|
+
unified store, sync workspaces to S3, manage idle-pod lifecycle, or track
|
|
8
|
+
GPU spend — all via the swm CLI. Do NOT use for general Docker, Kubernetes,
|
|
9
|
+
or non-GPU cloud tasks.
|
|
10
|
+
license: Apache-2.0
|
|
11
|
+
compatibility: "macOS or Linux, Python 3.11+, SSH client"
|
|
12
|
+
metadata:
|
|
13
|
+
author: swm-gpu
|
|
14
|
+
version: "0.2.3"
|
|
15
|
+
docs-url: https://swmgpu.com
|
|
16
|
+
repository: https://github.com/swm-gpu/swm
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
# swm — Cloud GPU Workflow Manager
|
|
20
|
+
|
|
21
|
+
One CLI to search, provision, install, sync, and tear down GPU pods across 10 cloud providers. Why swm over ad-hoc SSH scripts:
|
|
22
|
+
|
|
23
|
+
- **Cross-provider search** — `swm gpus` hits 10 providers with live pricing and stock in one call.
|
|
24
|
+
- **Declarative frameworks** — `swm setup install <name>` brings up ComfyUI / SwarmUI / vLLM / Ollama / Open WebUI / Axolotl / llm-studio idempotently; venv, custom nodes, and multi-GPU tensor parallelism handled.
|
|
25
|
+
- **Unified model store** — `swm models pull` auto-detects HF / Civitai / Ollama / URL refs, lands files under `/workspace/models/<bucket>/`, symlinks them into each framework's expected path.
|
|
26
|
+
- **Workspace persistence** — three-tier S3 sync (inotify → s5cmd → tar) survives auto-down. `pod create -w <name>` restores the last session.
|
|
27
|
+
- **Lifecycle guard** — `auto-down` / `auto-stop` / `remind` policies stop idle bills.
|
|
28
|
+
|
|
29
|
+
> **Stop condition.** Don't hand off until **Phase 5** passes. A pod that's "almost up" still bills.
|
|
30
|
+
|
|
31
|
+
## Prerequisites
|
|
32
|
+
|
|
33
|
+
- `swm --version` works (`pipx install swm-gpu` or `brew tap swm-gpu/swm && brew install swm`).
|
|
34
|
+
- `ssh` and `scp` on PATH.
|
|
35
|
+
- At least one provider key: `swm config set runpod.api_key <key>` (or `vastai`, `lambda`, …).
|
|
36
|
+
- Optional `/workspace` persistence: `swm config set b2.key_id …` + `swm config set storage.default b2:<bucket>`.
|
|
37
|
+
|
|
38
|
+
## Phase 0 — State check
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
swm pod list && swm use --show
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
If a matching pod is `RUNNING`, set it active (`swm use <name>`) and skip to Phase 5. Otherwise continue.
|
|
45
|
+
|
|
46
|
+
## Phase 1 — Clarify
|
|
47
|
+
|
|
48
|
+
Use `AskQuestion`:
|
|
49
|
+
|
|
50
|
+
| Question | Options |
|
|
51
|
+
|---|---|
|
|
52
|
+
| **VRAM floor**? | `≤16 GB` / `16–24 GB` / `24–48 GB` / `48–80 GB` / `>80 GB` |
|
|
53
|
+
| **Provider**? | `runpod` / `vastai` / `lambda` / `tensordock` / `vultr` / `auto (cheapest)` |
|
|
54
|
+
| **Lifecycle**? | `auto-down` (default) / `auto-stop` / `remind` / `manual` |
|
|
55
|
+
| **Idle timeout**? | `10` / `20` / `30` / `60` / `120` min |
|
|
56
|
+
| Persist `/workspace`? | `yes` (default) / `no — one-shot, --no-storage` |
|
|
57
|
+
|
|
58
|
+
| VRAM floor | GPU class to search |
|
|
59
|
+
|---|---|
|
|
60
|
+
| ≤16 GB | `RTX 4090`, `A4000`, `RTX 6000 Ada` |
|
|
61
|
+
| 16–24 GB | `RTX 4090`, `RTX 6000 Ada`, `L40S` |
|
|
62
|
+
| 24–48 GB | `A100 40GB`, `A100 80GB`, `L40S` |
|
|
63
|
+
| 48–80 GB | `H100`, `H100 SXM`, `A100 80GB` |
|
|
64
|
+
| >80 GB | `H200`, `B200`, multi-GPU `H100 SXM` |
|
|
65
|
+
|
|
66
|
+
For unsupported frameworks, read the README for: install method (`pip` vs `uv sync` vs custom), ComfyUI custom node vs standalone server, default ports, model cache location, UI mode toggles.
|
|
67
|
+
|
|
68
|
+
## Phase 2 — Pick GPU
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
swm gpus -g <class> --sort price --secure | head -20
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Pick the cheapest in-stock SKU meeting the VRAM floor. **Capture the exact display name** (e.g. `"NVIDIA H100 80GB HBM3"`) — that's what `-g` on `pod create` wants. Quote it; spaces break shells.
|
|
75
|
+
|
|
76
|
+
## Phase 3 — Provision
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
swm pod create -p <provider> -g "<gpu-display-name>" \
|
|
80
|
+
-n <name> --lifecycle <policy> --idle-timeout <min> -y
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
- `-y` is **mandatory** in agent shells — the `Proceed?` prompt has no fallback.
|
|
84
|
+
- Container disk wipes on stop; only `/workspace` persists. `--volume N` sizes it (RunPod requires `>= 1`).
|
|
85
|
+
- `-w <name>` restores the last workspace pushed under that name.
|
|
86
|
+
- After this the new pod is the active pod; subsequent `swm run` / `swm setup` can omit the id.
|
|
87
|
+
|
|
88
|
+
## Phase 4 — Install
|
|
89
|
+
|
|
90
|
+
### 4a. Prefer built-in frameworks
|
|
91
|
+
|
|
92
|
+
| Framework | Use when |
|
|
93
|
+
|---|---|
|
|
94
|
+
| `comfyui` | Image/video gen, custom-node ecosystems |
|
|
95
|
+
| `swarmui` | Higher-level wrapper around ComfyUI |
|
|
96
|
+
| `vllm` | Multi-GPU LLM serving (OpenAI-compatible) |
|
|
97
|
+
| `ollama` | Single-GPU LLM chat |
|
|
98
|
+
| `open-webui` | ChatGPT-style frontend for Ollama / vLLM |
|
|
99
|
+
| `axolotl` | LLM fine-tuning |
|
|
100
|
+
| `llm-studio` | H2O no-code fine-tuning |
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
swm setup list
|
|
104
|
+
swm setup install <name> # idempotent
|
|
105
|
+
swm setup start <name> # background, prints proxy URL, probes health
|
|
106
|
+
swm setup start vllm --model Qwen/Qwen3-8B # vLLM model selection
|
|
107
|
+
swm setup start comfyui --port 8288 --extra-args "--use-sage-attention"
|
|
108
|
+
swm setup stop <name>
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
vLLM auto-detects GPU count for tensor parallelism. swm opens SSH tunnels for unexposed ports and probes `/health` before reporting ready.
|
|
112
|
+
|
|
113
|
+
### 4b. Custom tools — chain `swm run`
|
|
114
|
+
|
|
115
|
+
Each `swm run` is its own SSH session. Use absolute paths, `&&`-chain idempotent commands, install into `/workspace`:
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
swm run "pip install -q uv && mkdir -p /workspace/.cache/uv \
|
|
119
|
+
&& cd /workspace && [ -d <repo> ] || git clone --depth 1 <git-url> \
|
|
120
|
+
&& cd <repo> && UV_CACHE_DIR=/workspace/.cache/uv uv sync"
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Use `uv sync` over `pip install -e .` when `pyproject.toml` has `[tool.uv.sources]` — pip silently ignores those overrides.
|
|
124
|
+
|
|
125
|
+
### 4c. ComfyUI custom nodes
|
|
126
|
+
|
|
127
|
+
Install **before** `swm setup start comfyui`. Custom-node deps go into ComfyUI's venv (not system pip):
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
swm run "cd /workspace/ComfyUI/custom_nodes \
|
|
131
|
+
&& [ -d <node-dir> ] || git clone --depth 1 <git-url> \
|
|
132
|
+
&& /workspace/ComfyUI/venv/bin/pip install -r <node-dir>/requirements.txt"
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### 4d. Models — use the unified store
|
|
136
|
+
|
|
137
|
+
`swm models pull` lands files under `/workspace/models/<bucket>/` and symlinks them into each framework's expected path. Source auto-detected from the ref shape:
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
swm models pull <pod> Qwen/Qwen3-8B # HuggingFace
|
|
141
|
+
swm models pull <pod> civitai:1925758 --as lora # Civitai
|
|
142
|
+
swm models pull <pod> https://example.com/m.safetensors --as checkpoint # URL
|
|
143
|
+
swm models pull <pod> ollama:llama3 # Ollama
|
|
144
|
+
swm models pull <pod> Comfy-Org/Wan_2.2_ComfyUI_Repackaged \
|
|
145
|
+
--as lora --filename split_files/loras/wan2.2_i2v_lightx2v_4steps_lora_v1_low_noise.safetensors
|
|
146
|
+
|
|
147
|
+
swm models info <ref> # metadata before pulling
|
|
148
|
+
swm models list <pod> [--all] # tracked (+ untracked) files
|
|
149
|
+
swm models link <pod> /abs/path/to/file --as vae
|
|
150
|
+
swm models remove <pod> <name>
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
API keys (set once; legacy `hf_token` also read):
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
swm config set hf.api_key <token> # gated HF models
|
|
157
|
+
swm config set civitai.api_key <token> # Civitai gated / NSFW
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
`--filename` supports nested HF paths (`split_files/loras/foo.safetensors`). If a bare basename 404s, swm prints matching paths from the repo. Multi-file pulls from one HF repo get distinct manifest entries (`hf:{repo}:{filename}`); no overwrites.
|
|
161
|
+
|
|
162
|
+
### 4e. Background daemon pattern
|
|
163
|
+
|
|
164
|
+
Mirror swm's `exec_background`: `nohup … > log 2>&1 < /dev/null &` with `&` **outside** `bash -c`, then sleep + `pgrep` so the call returns non-zero on crash:
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
swm run "nohup env <VAR>=<val> /workspace/<repo>/.venv/bin/<entrypoint> \
|
|
168
|
+
--host 127.0.0.1 --port <port> > /workspace/<name>.log 2>&1 < /dev/null & \
|
|
169
|
+
sleep 3 && pgrep -fa <entrypoint> \
|
|
170
|
+
|| (tail -50 /workspace/<name>.log; exit 1)"
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Bind `127.0.0.1` for internal services, `0.0.0.0` only if the proxy URL needs to reach it. Omit `< /dev/null` and SSH may hang on stdin.
|
|
174
|
+
|
|
175
|
+
## Phase 5 — Verify
|
|
176
|
+
|
|
177
|
+
Every claim must hold before hand-off:
|
|
178
|
+
|
|
179
|
+
- Framework main port returns `200` (ComfyUI `:8188`, vLLM `:8000/v1/models`, Ollama `:11434/api/tags`, Open WebUI `:8080/api/version`).
|
|
180
|
+
- Side-server `/health` returns `200` (if any).
|
|
181
|
+
- Custom-node directory appears in the host's startup log.
|
|
182
|
+
- Expected node/model types appear in `/object_info` (ComfyUI) or `/v1/models` (vLLM).
|
|
183
|
+
- Checkpoint dirs non-empty and roughly the size from the model card.
|
|
184
|
+
- VRAM in the expected ballpark (low if lazy-loaded, near full after first request).
|
|
185
|
+
- `df -h /workspace` not over 80 %.
|
|
186
|
+
|
|
187
|
+
If any check fails, iterate on the relevant phase. Don't hand off a half-working pod.
|
|
188
|
+
|
|
189
|
+
## Phase 6 — Hand off
|
|
190
|
+
|
|
191
|
+
1. **Public URL** verbatim (RunPod: `https://<pod>-<port>.proxy.runpod.net`).
|
|
192
|
+
2. **Sync status**: `swm sync status <pod>` — confirm watcher running, last push stamp recent.
|
|
193
|
+
3. **Local download** for workflow / config files: `swm download <relative-path> -d ~/Downloads`.
|
|
194
|
+
4. **Framework foot-guns** — spell out UI toggles (e.g. ACE-Step's `cloud → local` mode, model-selector defaults). #1 reason a working pod looks broken.
|
|
195
|
+
5. **Resume command** with `-w <name>` so the next session restores the workspace.
|
|
196
|
+
6. **Log tails**: `swm run "tail -f /tmp/<host>.log"` for live debugging.
|
|
197
|
+
|
|
198
|
+
See [examples.md](examples.md) for a fully worked run (ACE-Step on H100).
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
# Command reference
|
|
203
|
+
|
|
204
|
+
## GPU search
|
|
205
|
+
|
|
206
|
+
```bash
|
|
207
|
+
swm gpus -g h200 --secure --sort price # filter, secure cloud only, cheapest first
|
|
208
|
+
swm gpus -g h200 -c 4 --max-price 4 # 4×H200 under $4/hr
|
|
209
|
+
swm gpus -p vastai -n 50 --all # single provider, more rows
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Unknown provider slugs raise `UsageError` listing valid ones.
|
|
213
|
+
|
|
214
|
+
## Pod management
|
|
215
|
+
|
|
216
|
+
```bash
|
|
217
|
+
swm pod create -p runpod -g h200 -n s1 -y
|
|
218
|
+
swm pod create -p vastai -g h200 -n train --gpu-count 4 --volume 200 --cloud-type SECURE -y
|
|
219
|
+
swm pod list # auto-prunes stale entries vs live provider state
|
|
220
|
+
swm pod status / stop / start / down / prune <pod>
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
## Sync
|
|
224
|
+
|
|
225
|
+
```bash
|
|
226
|
+
swm sync pull [pod] # storage → pod
|
|
227
|
+
swm sync pull [pod] --tar # tarball mode (600k+ small files; fails loudly on error)
|
|
228
|
+
swm sync push [pod] # pod → storage
|
|
229
|
+
swm sync watch [pod] # inotify auto-push
|
|
230
|
+
swm sync status [pod] # s5cmd, watcher, autosync, pending changes
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
Three-tier sync: inotify watcher → incremental s5cmd → tar. Push reconciles inotify events with a `find -newer` scan so `pip install` bursts aren't missed. Pull re-runs framework link-repair steps so `/workspace/models/` symlinks stay wired after restore.
|
|
234
|
+
|
|
235
|
+
## Guard
|
|
236
|
+
|
|
237
|
+
```bash
|
|
238
|
+
swm guard set <pod> --mode auto-down --idle-timeout 30 # set/update policy
|
|
239
|
+
swm guard set <pod> --mode remind # notify-only
|
|
240
|
+
swm guard disable <pod> [--force-manual] # remove override / pin manual
|
|
241
|
+
swm guard defaults --mode auto-down --idle-timeout 30 # for new pods
|
|
242
|
+
swm guard list # live status
|
|
243
|
+
swm guard stop-daemon
|
|
244
|
+
```
|
|
245
|
+
|
|
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).
|
|
247
|
+
|
|
248
|
+
## Costs
|
|
249
|
+
|
|
250
|
+
```bash
|
|
251
|
+
swm costs live # right-now burn
|
|
252
|
+
swm costs summary [--period today|...] # breakdown by provider/pod (today = UTC day)
|
|
253
|
+
swm costs reconcile # verify vs provider billing (RunPod, Vast.ai)
|
|
254
|
+
swm costs budget set 100 # $/month alert
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
## Remote
|
|
258
|
+
|
|
259
|
+
```bash
|
|
260
|
+
swm ssh <pod>
|
|
261
|
+
swm run [pod] '<cmd>'
|
|
262
|
+
swm upload [pod] ./local remote/path
|
|
263
|
+
swm download [pod] remote/file -d ~/Downloads
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
## Pricing reference
|
|
267
|
+
|
|
268
|
+
```bash
|
|
269
|
+
swm pricing compare --gpu h100|h200|b200|a100|rtx-4090|l40s
|
|
270
|
+
swm pricing estimate -g h200 --hours 24
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
## Instance ID format
|
|
274
|
+
|
|
275
|
+
`provider:id` (`runpod:abc123`, `vastai:34182944`). A bare id auto-resolves by querying configured providers. `swm use <pod>` validates and sets the active pod.
|
|
276
|
+
|
|
277
|
+
## Providers
|
|
278
|
+
|
|
279
|
+
| Slug | API | Live pricing |
|
|
280
|
+
|---|---|---|
|
|
281
|
+
| `runpod` | GraphQL | yes |
|
|
282
|
+
| `vastai` | REST | yes |
|
|
283
|
+
| `lambda` | REST | yes |
|
|
284
|
+
| `vultr` | REST | yes |
|
|
285
|
+
| `tensordock` | REST | yes |
|
|
286
|
+
| `fluidstack` | REST | yes |
|
|
287
|
+
| `aws` | boto3 | yes |
|
|
288
|
+
| `gcp` | gcloud CLI | yes |
|
|
289
|
+
| `azure` | az CLI (`pip install swm-gpu[azure]`) | yes |
|
|
290
|
+
| `coreweave` | Kubernetes | yes |
|
|
291
|
+
|
|
292
|
+
## Storage
|
|
293
|
+
|
|
294
|
+
S3-compatible via s5cmd. Config keys:
|
|
295
|
+
|
|
296
|
+
| Backend | Keys |
|
|
297
|
+
|---|---|
|
|
298
|
+
| Backblaze B2 | `b2.key_id`, `b2.app_key`, `b2.bucket` |
|
|
299
|
+
| Amazon S3 | `s3.access_key`, `s3.secret_key`, `s3.bucket` |
|
|
300
|
+
| Google GCS | `gcs.hmac_access`, `gcs.hmac_secret`, `gcp.bucket` |
|
|
301
|
+
|
|
302
|
+
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
|
+
|
|
304
|
+
---
|
|
305
|
+
|
|
306
|
+
## Anti-patterns
|
|
307
|
+
|
|
308
|
+
- **Never omit `-y` on `swm pod create`** — agent shells hang on the `Proceed?` prompt.
|
|
309
|
+
- **Never install outside `/workspace`** — container disk wipes on stop; only `/workspace` survives auto-down.
|
|
310
|
+
- **Never drop `&` outside `bash -c`** when starting daemons — SSH waits for foreground and `swm run` hangs.
|
|
311
|
+
- **Never reuse one venv across frameworks** with conflicting `torch` pins — separate venvs (~7 GB each) beat dependency hell.
|
|
312
|
+
- **Never trust `pip install -e .`** for projects with `[tool.uv.sources]` — use `uv sync`.
|
|
313
|
+
- **Never `swm run -q`** during initial setup — quiet mode swallows install errors. Reserve `-q` for scripted health checks.
|
|
314
|
+
- **Never tear down before `du -sh /workspace`** — 10–30 GB bloat extends the next pull.
|
|
315
|
+
- **Never guess nested HF file paths** — run `swm models info <repo>` and copy the exact `rfilename`.
|
|
316
|
+
- **Never skip Phase 0** — a forgotten running pod bills silently.
|
|
317
|
+
- **Never hand off** before Phase 5 passes.
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# swm worked examples
|
|
2
|
+
|
|
3
|
+
Full end-to-end runs that pass Phase 5. Use these as templates when bringing up a new framework.
|
|
4
|
+
|
|
5
|
+
## ACE-Step on H100 (ComfyUI custom node + side server)
|
|
6
|
+
|
|
7
|
+
Music generation with the ACE-Step diffusion model. Two processes: ComfyUI on `:8188` (public via RunPod proxy) and `acestep-openrouter` on `127.0.0.1:8002` (internal, called by the custom node).
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
swm pod create -p runpod -g "NVIDIA H100 80GB HBM3" \
|
|
11
|
+
-n ace-step-test --lifecycle auto-down --idle-timeout 20 -y
|
|
12
|
+
|
|
13
|
+
swm setup install comfyui
|
|
14
|
+
|
|
15
|
+
swm run "pip install -q uv"
|
|
16
|
+
swm run "mkdir -p /workspace/.cache/uv /workspace/.cache/huggingface \
|
|
17
|
+
&& cd /workspace && [ -d ACE-Step-1.5 ] \
|
|
18
|
+
|| git clone --depth 1 https://github.com/ace-step/ACE-Step-1.5.git \
|
|
19
|
+
&& cd ACE-Step-1.5 && UV_CACHE_DIR=/workspace/.cache/uv uv sync"
|
|
20
|
+
|
|
21
|
+
swm run "cd /workspace/ComfyUI/custom_nodes \
|
|
22
|
+
&& [ -d ACE-Step-ComfyUI ] \
|
|
23
|
+
|| git clone --depth 1 https://github.com/ace-step/ACE-Step-ComfyUI.git \
|
|
24
|
+
&& /workspace/ComfyUI/venv/bin/pip install \
|
|
25
|
+
-r ACE-Step-ComfyUI/requirements.txt"
|
|
26
|
+
|
|
27
|
+
swm run "nohup env \
|
|
28
|
+
ACESTEP_CHECKPOINTS_DIR=/workspace/.cache/acestep-checkpoints \
|
|
29
|
+
HF_HOME=/workspace/.cache/huggingface \
|
|
30
|
+
/workspace/ACE-Step-1.5/.venv/bin/acestep-openrouter \
|
|
31
|
+
--host 127.0.0.1 --port 8002 \
|
|
32
|
+
> /workspace/acestep.log 2>&1 < /dev/null & \
|
|
33
|
+
sleep 3 && pgrep -fa acestep-openrouter \
|
|
34
|
+
|| (tail -60 /workspace/acestep.log; exit 1)"
|
|
35
|
+
|
|
36
|
+
swm setup start comfyui
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
**Verified end state:**
|
|
40
|
+
|
|
41
|
+
- ComfyUI 0.19.3 on `:8188`, ACE-Step custom node imported in 0.0 s
|
|
42
|
+
- `acestep-openrouter` on `127.0.0.1:8002`, `/health` → `{"status":"ok"}`, `/v1/models` → `acemusic/acestep-v15-turbo`
|
|
43
|
+
- Checkpoints (9.4 GB) under `/workspace/.cache/acestep-checkpoints/`: `acestep-v15-turbo` (4.5 GB), `acestep-5Hz-lm-1.7B` (3.5 GB), `Qwen3-Embedding-0.6B` (1.2 GB), `vae` (322 MB)
|
|
44
|
+
- Workflow at `/workspace/ComfyUI/custom_nodes/ACE-Step-ComfyUI/workflows/text2music.json`
|
|
45
|
+
- GPU memory 8.6 / 80 GiB before first inference
|
|
46
|
+
|
|
47
|
+
**Hand-off foot-gun:** `AceStepText2MusicServer` defaults to `cloud` mode and must be flipped to `local` in the ComfyUI node UI — otherwise the H100 sits idle and burns money.
|
|
48
|
+
|
|
49
|
+
**Switch to XL 4B variant** (without recreating the pod):
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
swm run "pkill -f acestep-openrouter; nohup env \
|
|
53
|
+
ACESTEP_CONFIG_PATH=acestep-v15-xl-turbo \
|
|
54
|
+
ACESTEP_CHECKPOINTS_DIR=/workspace/.cache/acestep-checkpoints \
|
|
55
|
+
HF_HOME=/workspace/.cache/huggingface \
|
|
56
|
+
/workspace/ACE-Step-1.5/.venv/bin/acestep-openrouter \
|
|
57
|
+
--host 127.0.0.1 --port 8002 \
|
|
58
|
+
> /workspace/acestep.log 2>&1 < /dev/null & \
|
|
59
|
+
sleep 3 && pgrep -fa acestep-openrouter"
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
**Resume next session** (restores `/workspace` from S3, custom nodes and checkpoints intact):
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
swm pod create -p runpod -g "NVIDIA H100 80GB HBM3" -n ace-step-test \
|
|
66
|
+
-w ace-step-test --lifecycle auto-down --idle-timeout 20 -y
|
|
67
|
+
swm setup start comfyui
|
|
68
|
+
```
|
|
@@ -6,6 +6,62 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.2.4] - 2026-05-27
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
- `swm sync push --force` now propagates the s5cmd exit code through the CLI;
|
|
13
|
+
previously partial failures (e.g. B2 503 SlowDown on a single object) were
|
|
14
|
+
silently reported as success.
|
|
15
|
+
- `swm sync push --force` advances the push stamp even on partial s5cmd
|
|
16
|
+
failure during the initial baseline upload, so the next autosync cycle can
|
|
17
|
+
retry missed files via `find -newer` instead of permanently refusing to start.
|
|
18
|
+
- `swm sync auto --force` touches the push stamp on the pod when missing,
|
|
19
|
+
matching the client-side `--force` semantics so the daemon's internal stamp
|
|
20
|
+
check no longer blocks startup after a forced push.
|
|
21
|
+
- `swm pod create -n <name>` (without `-w`) no longer fails when `/workspace`
|
|
22
|
+
contains files from the docker image — `swm` now uploads the existing
|
|
23
|
+
contents as the new workspace baseline and starts autosync normally. This
|
|
24
|
+
was the root cause of silent data loss when a pod was created without `-w`.
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
- `swm pod status <pod>` now reports autosync running state, watcher state,
|
|
28
|
+
last push age, and pending change count over SSH. Loudly warns when
|
|
29
|
+
autosync is not running on a pod that has been up > 5 minutes.
|
|
30
|
+
- `swm pod down --force-down` flag to override the new sync-safety guard
|
|
31
|
+
(terminate even if the workspace push did not write a recent stamp).
|
|
32
|
+
|
|
33
|
+
### Changed
|
|
34
|
+
- `swm pod down` refuses to wipe `/workspace` or terminate the pod when the
|
|
35
|
+
workspace push exits non-zero or fails to write a push stamp within the
|
|
36
|
+
last 10 minutes. The lifecycle guard's `auto-down` path applies the same
|
|
37
|
+
guard and leaves the pod alive on partial-push failure.
|
|
38
|
+
|
|
39
|
+
## [0.2.3] - 2026-05-24
|
|
40
|
+
|
|
41
|
+
### Fixed
|
|
42
|
+
- `swm sync pull --tar` no longer reports success when download/extract fails.
|
|
43
|
+
- `swm models pull` tracks each HF file under a unique manifest key; multi-file
|
|
44
|
+
repos (e.g. high/low LoRA pairs) no longer overwrite each other.
|
|
45
|
+
- HF pull 404s suggest nested repo paths; `--filename` resolves basenames when unique.
|
|
46
|
+
- `swm guard set` honors `--idle-timeout 0` / `--poll-interval 0` and starts the
|
|
47
|
+
local guard daemon for auto-stop/auto-down policies.
|
|
48
|
+
- `swm sync status` reports push stamp, watcher, pending changes, and auto-sync.
|
|
49
|
+
- `swm use` validates the pod exists before setting the active pod.
|
|
50
|
+
- SwarmUI install creates `dlbackend` before cloning ComfyUI (fixes #9).
|
|
51
|
+
- `swm gpus -p badslug` errors instead of returning empty results.
|
|
52
|
+
- `swm costs summary --period today` uses UTC calendar day, not rolling 24h.
|
|
53
|
+
- Cost tracking failures on pod lifecycle commands emit dim warnings.
|
|
54
|
+
|
|
55
|
+
### Changed
|
|
56
|
+
- Removed deprecated shims: `swm models set`, `swm pod gpus`, hidden `setup comfyui` /
|
|
57
|
+
`setup swarmui`.
|
|
58
|
+
- `swm guard disable --force-manual` pins a pod to manual mode.
|
|
59
|
+
- `swm pricing` supports H100, A100, RTX 4090, and L40S reference data.
|
|
60
|
+
- Shell completion suggests `provider:id` pod references.
|
|
61
|
+
- Config masking uses suffix-based sensitive key detection.
|
|
62
|
+
- Azure optional dependencies in `pyproject.toml` (`pip install swm-gpu[azure]`).
|
|
63
|
+
- Regenerated repo docs (`docs/cli-reference.md`, `docs/configuration.md`).
|
|
64
|
+
|
|
9
65
|
## [0.2.2] - 2026-05-24
|
|
10
66
|
|
|
11
67
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: swm-gpu
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.4
|
|
4
4
|
Summary: One CLI to search, provision, and manage cloud GPUs across 10 providers
|
|
5
5
|
Project-URL: Homepage, https://swmgpu.com
|
|
6
6
|
Project-URL: Repository, https://github.com/swm-gpu/swm
|
|
@@ -29,7 +29,16 @@ Requires-Dist: httpx>=0.27
|
|
|
29
29
|
Requires-Dist: rich>=13.0
|
|
30
30
|
Requires-Dist: tomli-w>=1.0
|
|
31
31
|
Provides-Extra: all
|
|
32
|
+
Requires-Dist: azure-identity>=1.15; extra == 'all'
|
|
33
|
+
Requires-Dist: azure-mgmt-compute>=30.0; extra == 'all'
|
|
34
|
+
Requires-Dist: azure-mgmt-network>=25.0; extra == 'all'
|
|
35
|
+
Requires-Dist: azure-mgmt-resource>=23.0; extra == 'all'
|
|
32
36
|
Requires-Dist: kubernetes>=29.0; extra == 'all'
|
|
37
|
+
Provides-Extra: azure
|
|
38
|
+
Requires-Dist: azure-identity>=1.15; extra == 'azure'
|
|
39
|
+
Requires-Dist: azure-mgmt-compute>=30.0; extra == 'azure'
|
|
40
|
+
Requires-Dist: azure-mgmt-network>=25.0; extra == 'azure'
|
|
41
|
+
Requires-Dist: azure-mgmt-resource>=23.0; extra == 'azure'
|
|
33
42
|
Provides-Extra: coreweave
|
|
34
43
|
Requires-Dist: kubernetes>=29.0; extra == 'coreweave'
|
|
35
44
|
Provides-Extra: dev
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "swm-gpu"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.4"
|
|
4
4
|
description = "One CLI to search, provision, and manage cloud GPUs across 10 providers"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.11"
|
|
@@ -30,8 +30,14 @@ dependencies = [
|
|
|
30
30
|
|
|
31
31
|
[project.optional-dependencies]
|
|
32
32
|
coreweave = ["kubernetes>=29.0"]
|
|
33
|
+
azure = [
|
|
34
|
+
"azure-identity>=1.15",
|
|
35
|
+
"azure-mgmt-compute>=30.0",
|
|
36
|
+
"azure-mgmt-network>=25.0",
|
|
37
|
+
"azure-mgmt-resource>=23.0",
|
|
38
|
+
]
|
|
33
39
|
dev = ["nuitka", "ordered-set", "zstandard"]
|
|
34
|
-
all = ["kubernetes>=29.0"]
|
|
40
|
+
all = ["kubernetes>=29.0", "azure-identity>=1.15", "azure-mgmt-compute>=30.0", "azure-mgmt-network>=25.0", "azure-mgmt-resource>=23.0"]
|
|
35
41
|
|
|
36
42
|
[project.urls]
|
|
37
43
|
Homepage = "https://swmgpu.com"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.2.4"
|
|
@@ -309,20 +309,29 @@ _WS_MARKER_NAMES = (
|
|
|
309
309
|
)
|
|
310
310
|
|
|
311
311
|
|
|
312
|
-
def
|
|
313
|
-
"""
|
|
312
|
+
def _workspace_leftover_files(session: RemoteSession) -> list[str]:
|
|
313
|
+
"""Return paths in /workspace/ that aren't swm markers.
|
|
314
314
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
315
|
+
Non-marker files commonly appear because docker images use
|
|
316
|
+
/workspace as a default WORKDIR and seed it with content (e.g. the
|
|
317
|
+
pytorch base image). Callers decide how to handle these — usually
|
|
318
|
+
by uploading them as the initial baseline rather than failing.
|
|
318
319
|
"""
|
|
319
320
|
excludes = " ".join(f"-not -name '{n}'" for n in _WS_MARKER_NAMES)
|
|
320
321
|
cmd = (
|
|
321
|
-
f"find /workspace -mindepth 1 -maxdepth 1 {excludes} 2>/dev/null
|
|
322
|
-
"| head -5"
|
|
322
|
+
f"find /workspace -mindepth 1 -maxdepth 1 {excludes} 2>/dev/null"
|
|
323
323
|
)
|
|
324
324
|
_, out, _ = session.exec(cmd, stream=False)
|
|
325
|
-
|
|
325
|
+
return [line for line in out.splitlines() if line.strip()]
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def _ensure_workspace_empty_on_pod(session: RemoteSession) -> None:
|
|
329
|
+
"""Raise if /workspace/ on the pod contains any non-marker files.
|
|
330
|
+
|
|
331
|
+
Retained for callers that want the strict behavior. Most call sites
|
|
332
|
+
should prefer ``_workspace_leftover_files`` and seed-by-pushing.
|
|
333
|
+
"""
|
|
334
|
+
leftover = _workspace_leftover_files(session)
|
|
326
335
|
if leftover:
|
|
327
336
|
sample = ", ".join(p.rsplit("/", 1)[-1] for p in leftover[:3])
|
|
328
337
|
more = "" if len(leftover) <= 3 else f" (+ more)"
|
|
@@ -381,14 +390,49 @@ def bootstrap_workspace_on_pod(
|
|
|
381
390
|
if storage_ok:
|
|
382
391
|
try:
|
|
383
392
|
if is_new:
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
393
|
+
# Docker images commonly seed /workspace with content
|
|
394
|
+
# (e.g. pytorch base image). Rather than fail bootstrap
|
|
395
|
+
# and leave the pod without autosync — the historical
|
|
396
|
+
# failure mode that caused silent data loss — seed the
|
|
397
|
+
# new workspace by uploading whatever is already on the
|
|
398
|
+
# pod as the initial baseline.
|
|
399
|
+
leftover = _workspace_leftover_files(session)
|
|
400
|
+
if leftover:
|
|
401
|
+
sample = ", ".join(
|
|
402
|
+
p.rsplit("/", 1)[-1] for p in leftover[:3]
|
|
403
|
+
)
|
|
404
|
+
more = (
|
|
405
|
+
"" if len(leftover) <= 3
|
|
406
|
+
else f" (+ {len(leftover) - 3} more)"
|
|
407
|
+
)
|
|
408
|
+
_con.print(
|
|
409
|
+
f" [yellow]/workspace contains existing files "
|
|
410
|
+
f"(e.g. {sample}{more}) — uploading them as the "
|
|
411
|
+
f"new workspace baseline before starting "
|
|
412
|
+
f"auto-sync.[/yellow]"
|
|
413
|
+
)
|
|
414
|
+
from swm.sync.push import workspace_push
|
|
415
|
+
rc = workspace_push(
|
|
416
|
+
session, storage_slug, bucket, workspace,
|
|
417
|
+
extra_excludes=extra_excludes, force=True,
|
|
418
|
+
)
|
|
419
|
+
if rc != 0:
|
|
420
|
+
raise RuntimeError(
|
|
421
|
+
f"Initial baseline push failed (s5cmd exit "
|
|
422
|
+
f"{rc}). Re-run `swm sync push "
|
|
423
|
+
f"{qualified_id} --force` then "
|
|
424
|
+
f"`swm sync auto {qualified_id}`."
|
|
425
|
+
)
|
|
426
|
+
else:
|
|
427
|
+
_con.print(" [dim]New workspace — skipping pull[/dim]")
|
|
428
|
+
session.exec(
|
|
429
|
+
f": > {WATCH_LOG} 2>/dev/null; touch {PUSH_STAMP}",
|
|
430
|
+
stream=False,
|
|
431
|
+
)
|
|
432
|
+
if start_watcher(session, "/workspace"):
|
|
433
|
+
_con.print(
|
|
434
|
+
" [dim]Watcher started for change tracking[/dim]"
|
|
435
|
+
)
|
|
392
436
|
else:
|
|
393
437
|
workspace_pull(
|
|
394
438
|
session, storage_slug, bucket, workspace,
|
|
@@ -32,7 +32,11 @@ Workflow:
|
|
|
32
32
|
@click.group(epilog=_WORKFLOW_EPILOG)
|
|
33
33
|
@click.version_option(__version__, prog_name="swm")
|
|
34
34
|
def main():
|
|
35
|
-
"""swm — Cloud GPU workflow manager
|
|
35
|
+
"""swm — Cloud GPU workflow manager across 10 providers.
|
|
36
|
+
|
|
37
|
+
Search GPUs, provision pods, install frameworks (ComfyUI, vLLM, Ollama, …),
|
|
38
|
+
sync workspaces, manage models, and automate lifecycle with guard policies.
|
|
39
|
+
"""
|
|
36
40
|
|
|
37
41
|
|
|
38
42
|
# ── GPU search (top-level, kept here for brevity) ──────────────────
|
|
@@ -103,7 +107,10 @@ def gpus(gpu: str | None, gpu_count: int | None, max_price: float | None,
|
|
|
103
107
|
try:
|
|
104
108
|
sources = [get_provider(provider)]
|
|
105
109
|
except ValueError:
|
|
106
|
-
|
|
110
|
+
known = ", ".join(sorted(_SLUG_TO_NAME))
|
|
111
|
+
raise click.UsageError(
|
|
112
|
+
f"Unknown provider {provider!r}. Known slugs: {known}"
|
|
113
|
+
) from None
|
|
107
114
|
else:
|
|
108
115
|
configured = get_configured_providers()
|
|
109
116
|
unconfigured_slugs = (
|
|
@@ -63,16 +63,29 @@ def resolve_active_pod(instance_id: str | None) -> str:
|
|
|
63
63
|
|
|
64
64
|
|
|
65
65
|
def _iter_configured_pod_ids() -> list[str]:
|
|
66
|
-
"""Return pod ids known to swm's config (`
|
|
66
|
+
"""Return qualified pod ids known to swm's config (`provider:id`)."""
|
|
67
67
|
pods = cfg.get("pods") or {}
|
|
68
68
|
if not isinstance(pods, dict):
|
|
69
69
|
return []
|
|
70
|
-
|
|
70
|
+
out: list[str] = []
|
|
71
|
+
for pid, meta in pods.items():
|
|
72
|
+
prov = (meta or {}).get("provider", "")
|
|
73
|
+
out.append(f"{prov}:{pid}" if prov else pid)
|
|
74
|
+
return sorted(out)
|
|
71
75
|
|
|
72
76
|
|
|
73
77
|
def complete_pod_id(ctx, param, incomplete: str):
|
|
74
78
|
"""Click shell completion callback for pod id arguments."""
|
|
75
|
-
|
|
79
|
+
pods = cfg.get("pods") or {}
|
|
80
|
+
if not isinstance(pods, dict):
|
|
81
|
+
return []
|
|
82
|
+
candidates: set[str] = set()
|
|
83
|
+
for pid, meta in pods.items():
|
|
84
|
+
prov = (meta or {}).get("provider", "")
|
|
85
|
+
qid = f"{prov}:{pid}" if prov else pid
|
|
86
|
+
candidates.add(qid)
|
|
87
|
+
candidates.add(pid)
|
|
88
|
+
return sorted(c for c in candidates if c.startswith(incomplete))
|
|
76
89
|
|
|
77
90
|
|
|
78
91
|
def pod_arg_callback(ctx, param, value):
|
|
@@ -94,7 +107,8 @@ def looks_like_pod_id(value: str) -> bool:
|
|
|
94
107
|
return False
|
|
95
108
|
if any(value.startswith(p) for p in _PROVIDER_PREFIXES):
|
|
96
109
|
return True
|
|
97
|
-
|
|
110
|
+
pods = cfg.get("pods") or {}
|
|
111
|
+
return isinstance(pods, dict) and value in pods
|
|
98
112
|
|
|
99
113
|
|
|
100
114
|
def split_pod_and_command(
|