omnirun 0.1.0__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.
- omnirun-0.1.0/.envrc +3 -0
- omnirun-0.1.0/.github/workflows/checks.yml +36 -0
- omnirun-0.1.0/.github/workflows/ci.yml +10 -0
- omnirun-0.1.0/.github/workflows/publish.yml +38 -0
- omnirun-0.1.0/.gitignore +20 -0
- omnirun-0.1.0/.python-version +1 -0
- omnirun-0.1.0/DESIGN.md +289 -0
- omnirun-0.1.0/PKG-INFO +312 -0
- omnirun-0.1.0/README.md +292 -0
- omnirun-0.1.0/TESTING.md +212 -0
- omnirun-0.1.0/flake.lock +113 -0
- omnirun-0.1.0/flake.nix +81 -0
- omnirun-0.1.0/main.py +6 -0
- omnirun-0.1.0/pyproject.toml +49 -0
- omnirun-0.1.0/research/colab-kaggle.md +187 -0
- omnirun-0.1.0/research/gpu-marketplaces.md +196 -0
- omnirun-0.1.0/research/launcher-landscape.md +75 -0
- omnirun-0.1.0/research/ml-job-queues.md +79 -0
- omnirun-0.1.0/research/slurm-ssh.md +156 -0
- omnirun-0.1.0/src/omnirun/__init__.py +3 -0
- omnirun-0.1.0/src/omnirun/backends/__init__.py +3 -0
- omnirun-0.1.0/src/omnirun/backends/base.py +118 -0
- omnirun-0.1.0/src/omnirun/backends/colab.py +605 -0
- omnirun-0.1.0/src/omnirun/backends/jobdir.py +225 -0
- omnirun-0.1.0/src/omnirun/backends/kaggle.py +605 -0
- omnirun-0.1.0/src/omnirun/backends/local.py +193 -0
- omnirun-0.1.0/src/omnirun/backends/marketplace.py +464 -0
- omnirun-0.1.0/src/omnirun/backends/runpod.py +171 -0
- omnirun-0.1.0/src/omnirun/backends/slurm.py +500 -0
- omnirun-0.1.0/src/omnirun/backends/ssh.py +312 -0
- omnirun-0.1.0/src/omnirun/backends/thunder.py +182 -0
- omnirun-0.1.0/src/omnirun/backends/vast.py +201 -0
- omnirun-0.1.0/src/omnirun/bootstrap.py +325 -0
- omnirun-0.1.0/src/omnirun/chooser.py +219 -0
- omnirun-0.1.0/src/omnirun/cli.py +893 -0
- omnirun-0.1.0/src/omnirun/config.py +150 -0
- omnirun-0.1.0/src/omnirun/daemon.py +486 -0
- omnirun-0.1.0/src/omnirun/execlayer/__init__.py +3 -0
- omnirun-0.1.0/src/omnirun/execlayer/base.py +97 -0
- omnirun-0.1.0/src/omnirun/execlayer/local.py +81 -0
- omnirun-0.1.0/src/omnirun/execlayer/ssh.py +259 -0
- omnirun-0.1.0/src/omnirun/models.py +196 -0
- omnirun-0.1.0/src/omnirun/queue.py +106 -0
- omnirun-0.1.0/src/omnirun/repo.py +160 -0
- omnirun-0.1.0/src/omnirun/store.py +107 -0
- omnirun-0.1.0/tests/__init__.py +0 -0
- omnirun-0.1.0/tests/conftest.py +80 -0
- omnirun-0.1.0/tests/test_bootstrap.py +123 -0
- omnirun-0.1.0/tests/test_chooser.py +243 -0
- omnirun-0.1.0/tests/test_cli.py +525 -0
- omnirun-0.1.0/tests/test_colab.py +385 -0
- omnirun-0.1.0/tests/test_kaggle.py +474 -0
- omnirun-0.1.0/tests/test_local_backend.py +182 -0
- omnirun-0.1.0/tests/test_marketplaces.py +687 -0
- omnirun-0.1.0/tests/test_queue.py +321 -0
- omnirun-0.1.0/tests/test_repo.py +175 -0
- omnirun-0.1.0/tests/test_slurm.py +613 -0
- omnirun-0.1.0/tests/test_ssh_backend.py +413 -0
- omnirun-0.1.0/tests/test_ssh_exec.py +327 -0
- omnirun-0.1.0/tests/test_store.py +85 -0
- omnirun-0.1.0/uv.lock +1321 -0
omnirun-0.1.0/.envrc
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
name: Checks
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_call:
|
|
5
|
+
|
|
6
|
+
jobs:
|
|
7
|
+
lint-typecheck-test:
|
|
8
|
+
runs-on: ubuntu-latest
|
|
9
|
+
env:
|
|
10
|
+
UV_CACHE_DIR: /tmp/.uv-cache
|
|
11
|
+
steps:
|
|
12
|
+
- name: Checkout
|
|
13
|
+
uses: actions/checkout@v7
|
|
14
|
+
|
|
15
|
+
- name: Install Nix
|
|
16
|
+
uses: DeterminateSystems/determinate-nix-action@v3
|
|
17
|
+
|
|
18
|
+
- name: Restore uv cache
|
|
19
|
+
uses: actions/cache@v4
|
|
20
|
+
with:
|
|
21
|
+
path: /tmp/.uv-cache
|
|
22
|
+
key: uv-${{ runner.os }}-${{ hashFiles('uv.lock') }}
|
|
23
|
+
restore-keys: |
|
|
24
|
+
uv-${{ runner.os }}-
|
|
25
|
+
|
|
26
|
+
- name: ruff + ruff-format
|
|
27
|
+
run: nix flake check -L
|
|
28
|
+
|
|
29
|
+
- name: basedpyright
|
|
30
|
+
run: nix develop --command basedpyright
|
|
31
|
+
|
|
32
|
+
- name: Tests
|
|
33
|
+
run: nix develop --command uv run pytest -q
|
|
34
|
+
|
|
35
|
+
- name: Minimize uv cache
|
|
36
|
+
run: nix develop --command uv cache prune --ci
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*"
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
checks:
|
|
10
|
+
uses: ./.github/workflows/checks.yml
|
|
11
|
+
|
|
12
|
+
publish:
|
|
13
|
+
needs: checks
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
environment:
|
|
16
|
+
name: pypi
|
|
17
|
+
url: https://pypi.org/project/omnirun/
|
|
18
|
+
permissions:
|
|
19
|
+
id-token: write
|
|
20
|
+
contents: read
|
|
21
|
+
steps:
|
|
22
|
+
- name: Checkout
|
|
23
|
+
uses: actions/checkout@v7
|
|
24
|
+
|
|
25
|
+
- name: Install uv
|
|
26
|
+
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
|
27
|
+
with:
|
|
28
|
+
enable-cache: true
|
|
29
|
+
cache-dependency-glob: "uv.lock"
|
|
30
|
+
|
|
31
|
+
- name: Install Python
|
|
32
|
+
run: uv python install 3.12
|
|
33
|
+
|
|
34
|
+
- name: Build
|
|
35
|
+
run: uv build
|
|
36
|
+
|
|
37
|
+
- name: Publish
|
|
38
|
+
run: uv publish
|
omnirun-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
__pycache__/
|
|
2
|
+
*.py[cod]
|
|
3
|
+
*.egg-info/
|
|
4
|
+
.venv/
|
|
5
|
+
.direnv/
|
|
6
|
+
.pytest_cache/
|
|
7
|
+
.hypothesis/
|
|
8
|
+
.ruff_cache/
|
|
9
|
+
.env
|
|
10
|
+
dist/
|
|
11
|
+
result
|
|
12
|
+
# generated by git-hooks.nix shellHook; bakes in local /nix/store paths
|
|
13
|
+
.pre-commit-config.yaml
|
|
14
|
+
|
|
15
|
+
# transient state from running examples/ against real R2
|
|
16
|
+
.dload-cache*/
|
|
17
|
+
.fsdd-download/
|
|
18
|
+
.cli-demo/
|
|
19
|
+
.dload-tmp/
|
|
20
|
+
.downloads/
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.12
|
omnirun-0.1.0/DESIGN.md
ADDED
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
# omnirun — design
|
|
2
|
+
|
|
3
|
+
> Run a job from your repo anywhere: uni Slurm cluster, a friend's gaming rig over SSH,
|
|
4
|
+
> Kaggle, Colab, or an auto-provisioned marketplace GPU — with one command, picking the
|
|
5
|
+
> cheapest/fastest option that fits.
|
|
6
|
+
|
|
7
|
+
Status: v0 design, finalized against the research reports in `research/`
|
|
8
|
+
(landscape verdict: no existing tool covers Slurm-over-SSH + Colab + Kaggle +
|
|
9
|
+
marketplaces + cost-vs-wait choice; closest are SkyPilot and dstack, both heavy
|
|
10
|
+
and structurally unable to reach notebooks or user-space-only clusters. We build,
|
|
11
|
+
stealing: dstack's offer-table UX, SkyPilot's slurm-as-a-cloud shape, ClearML's
|
|
12
|
+
job-envelope idea, submitit's two-layer resource config).
|
|
13
|
+
|
|
14
|
+
## 1. Philosophy
|
|
15
|
+
|
|
16
|
+
- **The repo is the unit of deployment.** A job is `(git revision, command, resources)`.
|
|
17
|
+
omnirun ensures the revision is pushed, materializes it on the worker (worktree per
|
|
18
|
+
branch), sets up the env, runs the command, captures outputs. No image building, no
|
|
19
|
+
data syncing — jobs own their data.
|
|
20
|
+
- **One bootstrap script, many wrappers.** Every backend ultimately executes the same
|
|
21
|
+
generated POSIX-ish bash payload. Backends differ only in *how* the payload gets
|
|
22
|
+
executed (nohup over ssh, sbatch, Kaggle kernel cell, Colab cell, provisioned VM).
|
|
23
|
+
- **No control plane.** State lives in `~/.local/share/omnirun/` on the client. Polling,
|
|
24
|
+
not callbacks. If the laptop is off, jobs still run; state re-syncs on next `omnirun ps`.
|
|
25
|
+
- **Choice is a first-class output.** `submit` produces *offers* (cost × wait × fit).
|
|
26
|
+
Clear winner → auto-submit. Genuine tradeoff → show the table, let the human pick.
|
|
27
|
+
|
|
28
|
+
## 2. Core model
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
JobSpec
|
|
32
|
+
name: str # human label; job_id = <name>-<hex6>
|
|
33
|
+
command: list[str] | str # executed via bash -c in repo root on worker
|
|
34
|
+
resources: ResourceSpec
|
|
35
|
+
env: EnvSpec # auto-detected, overridable
|
|
36
|
+
outputs: list[str] # globs relative to repo root, collected post-run
|
|
37
|
+
repo: RepoRef # remote url, sha, branch (captured at submit)
|
|
38
|
+
|
|
39
|
+
ResourceSpec
|
|
40
|
+
gpus: int = 0
|
|
41
|
+
gpu_type: str | None # "H100", "A100-80", "4090", ... (normalized names)
|
|
42
|
+
min_vram_gb: float | None # alternative to gpu_type
|
|
43
|
+
cpus: int | None
|
|
44
|
+
mem_gb: float | None
|
|
45
|
+
time: timedelta | None # est. duration — drives cost math + slurm --time
|
|
46
|
+
disk_gb: float | None
|
|
47
|
+
|
|
48
|
+
Offer
|
|
49
|
+
backend: str # config key, e.g. "uni", "runpod"
|
|
50
|
+
label: str # "uni: gpu partition (A100)", "runpod: H100 SXM $2.79/hr"
|
|
51
|
+
fits: bool
|
|
52
|
+
unfit_reasons: list[str]
|
|
53
|
+
cost_estimate: Money | None # None = free
|
|
54
|
+
wait_estimate: timedelta | None # None = unknown
|
|
55
|
+
attended: bool # True = requires a human click (Colab)
|
|
56
|
+
details: dict # backend-specific (instance id template, partition, ...)
|
|
57
|
+
|
|
58
|
+
JobStatus: QUEUED | PROVISIONING | STARTING | RUNNING | SUCCEEDED | FAILED |
|
|
59
|
+
CANCELLED | LOST # LOST = can't reach worker / handle stale
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Backend protocol
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
class Backend(Protocol):
|
|
66
|
+
name: str
|
|
67
|
+
def probe(self, res: ResourceSpec) -> list[Offer]: ...
|
|
68
|
+
def submit(self, spec: JobSpec, offer: Offer) -> JobHandle: ...
|
|
69
|
+
def status(self, h: JobHandle) -> JobStatus: ...
|
|
70
|
+
def logs(self, h: JobHandle, follow: bool) -> Iterator[str]: ...
|
|
71
|
+
def cancel(self, h: JobHandle) -> None: ...
|
|
72
|
+
def pull_outputs(self, h: JobHandle, dest: Path) -> None: ...
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
`probe` must be fast (<10s) and safe to run speculatively; probes run in parallel
|
|
76
|
+
with per-backend timeout. A backend that errors during probe yields a not-fit offer
|
|
77
|
+
with the error as reason — never crashes the chooser.
|
|
78
|
+
|
|
79
|
+
### Backend composition matrix
|
|
80
|
+
|
|
81
|
+
Two orthogonal layers for the SSH family:
|
|
82
|
+
|
|
83
|
+
- **Transport**: `LocalExec` (testing) | `SSHExec` (openssh binary, ControlMaster
|
|
84
|
+
multiplexing, respects `~/.ssh/config` so jump hosts/2FA/kerberos just work).
|
|
85
|
+
- **Runtime**: `detached` (setsid+nohup, pidfile) | `slurm` (sbatch codegen + sacct
|
|
86
|
+
polling).
|
|
87
|
+
|
|
88
|
+
Concrete backends:
|
|
89
|
+
|
|
90
|
+
| backend key | transport | runtime | provisioning | attended |
|
|
91
|
+
|---|---|---|---|---|
|
|
92
|
+
| `local` | local | detached | — | no |
|
|
93
|
+
| `ssh` (uncle's rig) | ssh | detached | — | no |
|
|
94
|
+
| `slurm` (uni) | ssh | slurm | — | no |
|
|
95
|
+
| `runpod` / `vast` / `thunder` | ssh | detached | REST API create→ssh→terminate | no |
|
|
96
|
+
| `kaggle` | kernels API | kernel wraps bootstrap | — | no |
|
|
97
|
+
| `colab` | `google-colab-cli` | exec cell wraps bootstrap | — | no (one-time OAuth) |
|
|
98
|
+
|
|
99
|
+
## 3. The bootstrap payload
|
|
100
|
+
|
|
101
|
+
Generated per job: `bootstrap.sh`, parameterized by a small `job.json` sidecar. Steps:
|
|
102
|
+
|
|
103
|
+
1. `mkdir -p $OMNIRUN_ROOT/{repos,jobs}` (default `~/.omnirun`, configurable per backend
|
|
104
|
+
— on clusters typically `$SCRATCH/omnirun`).
|
|
105
|
+
2. **Code**: bare-ish mirror clone per repo (`repos/<slug>.git`), `git fetch`, then
|
|
106
|
+
`git worktree add jobs/<job_id>/tree <sha>` (worktree per job, pruned on cleanup;
|
|
107
|
+
cheap because objects are shared via the mirror). Private repos: see §6.
|
|
108
|
+
3. **Env** (in `jobs/<job_id>/tree`): detection order
|
|
109
|
+
`uv.lock → uv sync`, `pyproject.toml → uv sync` (installs uv via standalone
|
|
110
|
+
installer if missing — static binary, works on old-glibc HPC),
|
|
111
|
+
`requirements.txt → uv venv + uv pip install -r`,
|
|
112
|
+
`environment.yml → micromamba` (static binary bootstrap).
|
|
113
|
+
Overridable: `env.setup = ["module load cuda/12.4", "uv sync"]` in config for
|
|
114
|
+
cluster quirks.
|
|
115
|
+
4. **Run**: `cd tree && <command>` with stdout+stderr tee'd to `jobs/<job_id>/logs/`,
|
|
116
|
+
heartbeat file touched every 30s, `result.json` written on exit
|
|
117
|
+
`{exit_code, started_at, finished_at, hostname}`.
|
|
118
|
+
5. **Outputs**: copy `outputs` globs → `jobs/<job_id>/outputs/`. Client pulls via
|
|
119
|
+
rsync/scp (ssh family), kernel output download (Kaggle), Drive (Colab).
|
|
120
|
+
|
|
121
|
+
Status without a control plane: the job dir *is* the status API — presence of
|
|
122
|
+
`result.json`, heartbeat freshness, plus runtime-native signals (slurm state, PID
|
|
123
|
+
alive, kernel status) are merged into one JobStatus.
|
|
124
|
+
|
|
125
|
+
## 4. Chooser
|
|
126
|
+
|
|
127
|
+
1. Probe all enabled backends in parallel (timeout 10s each).
|
|
128
|
+
2. Partition offers: fit / unfit.
|
|
129
|
+
3. Score fitting offers. Default policy (configurable weights):
|
|
130
|
+
- `total_cost = hourly × est_time` (0 for free backends)
|
|
131
|
+
- `time_to_result = wait_estimate + est_time`
|
|
132
|
+
- **Auto-pick** iff a free offer has `wait < auto_wait_threshold` (default 15m),
|
|
133
|
+
or exactly one offer fits, or `--yes` with a `--max-cost`.
|
|
134
|
+
- Otherwise render the table (rich): backend, GPU, $/hr, est. total $, est. wait,
|
|
135
|
+
time-to-result, notes — user picks by number. `--yes` picks the top-ranked.
|
|
136
|
+
|
|
137
|
+
Wait estimation is honest about uncertainty. Slurm: `sinfo` idle-node check
|
|
138
|
+
("likely immediate") → `sbatch --test-only` pre-submit estimate + script validation
|
|
139
|
+
→ post-submit `squeue --start`, always labeled "backfill estimate, usually
|
|
140
|
+
pessimistic"; plus a local history of our own (partition, resources)→actual-wait
|
|
141
|
+
medians. Marketplaces report provisioning latency (~1–3 min). Kaggle/Colab report
|
|
142
|
+
queue-free but quota/session-bounded (unfit if `resources.time` > 12h etc.).
|
|
143
|
+
|
|
144
|
+
## 5. State & config
|
|
145
|
+
|
|
146
|
+
- Client state: `~/.local/share/omnirun/jobs/<job_id>/meta.json` (spec, handle,
|
|
147
|
+
last-known status, offer chosen). Plain JSON, greppable, no daemon.
|
|
148
|
+
- Config: `~/.config/omnirun/config.toml` (backends, credentials refs, policy) +
|
|
149
|
+
optional per-repo `omnirun.toml` (resource defaults, outputs, env.setup overrides).
|
|
150
|
+
|
|
151
|
+
```toml
|
|
152
|
+
[policy]
|
|
153
|
+
auto_wait_threshold = "15m"
|
|
154
|
+
max_hourly_default = 5.0
|
|
155
|
+
|
|
156
|
+
[backends.uni]
|
|
157
|
+
type = "slurm"
|
|
158
|
+
host = "hpc-login" # ssh config alias; ProxyJump etc. live in ~/.ssh/config
|
|
159
|
+
partition = "gpu"
|
|
160
|
+
account = "myproject"
|
|
161
|
+
gpu_types = { "A100-80" = "a100:{n}", "V100" = "v100:{n}" } # → --gres
|
|
162
|
+
root = "$SCRATCH/omnirun"
|
|
163
|
+
env_setup = ["module load cuda/12.4"]
|
|
164
|
+
|
|
165
|
+
[backends.rig]
|
|
166
|
+
type = "ssh"
|
|
167
|
+
host = "uncle-gaming"
|
|
168
|
+
gpus = [{ type = "4090", count = 1 }] # static capability declaration
|
|
169
|
+
|
|
170
|
+
[backends.kaggle]
|
|
171
|
+
type = "kaggle" # creds from ~/.config/kaggle/kaggle.json
|
|
172
|
+
|
|
173
|
+
[backends.colab]
|
|
174
|
+
type = "colab"
|
|
175
|
+
drive_dir = "omnirun" # folder in My Drive used as mailbox
|
|
176
|
+
|
|
177
|
+
[backends.runpod]
|
|
178
|
+
type = "runpod" # RUNPOD_API_KEY env
|
|
179
|
+
max_hourly = 3.5
|
|
180
|
+
|
|
181
|
+
[backends.vast]
|
|
182
|
+
type = "vast" # VAST_API_KEY env
|
|
183
|
+
|
|
184
|
+
[backends.thunder]
|
|
185
|
+
type = "thunder" # TNR_API_TOKEN env
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## 6. Repos & credentials
|
|
189
|
+
|
|
190
|
+
Submit-time invariant: working tree clean (or `--dirty` to auto-stash-commit onto a
|
|
191
|
+
`omnirun/<job_id>` ref — v1), HEAD pushed to remote (offer to push).
|
|
192
|
+
|
|
193
|
+
Worker access to private repos — **no git credentials ever leave the laptop**:
|
|
194
|
+
- **ssh/slurm/marketplace**: at submit time the client `git push`es the exact sha
|
|
195
|
+
over its own SSH connection into the worker-side bare repo
|
|
196
|
+
(`repos/<slug>.git`, created on demand). Nothing on the worker can or needs to
|
|
197
|
+
reach the origin remote. (Documented alternatives: agent-forwarded
|
|
198
|
+
`git fetch origin` for huge repos; per-repo deploy keys.)
|
|
199
|
+
- **kaggle/colab**: the revision travels as a **`git bundle`** — uploaded via
|
|
200
|
+
`colab upload` (Colab) or packed into a private per-job Kaggle dataset attached
|
|
201
|
+
through `dataset_sources` (Kaggle). The bootstrap clones from the bundle.
|
|
202
|
+
Optional GitHub-PAT-in-Kaggle-secret flow only if the user prefers fetching.
|
|
203
|
+
|
|
204
|
+
## 7. Notebook backends
|
|
205
|
+
|
|
206
|
+
- **Kaggle**: fully automated via the `kaggle` Python API (`kernels_push` /
|
|
207
|
+
`kernels_status` / `kernels_output`). Per job: a private script kernel
|
|
208
|
+
(`title == slug == omnirun-<job_id>`, `enable_internet`, `machine_shape` mapped
|
|
209
|
+
from ResourceSpec: free set = P100 / 2×T4; L4/A100/H100 exist but are
|
|
210
|
+
Colab-Pro-gated → surfaced as conditional offers) + a private per-job dataset
|
|
211
|
+
carrying the git bundle. The kernel script unpacks the bundle and runs the
|
|
212
|
+
bootstrap; outputs land in `/kaggle/working` (≤20 GB, ≤500 files → tar).
|
|
213
|
+
Probe constraints: 12h GPU / 9h TPU session cap → unfit if `resources.time`
|
|
214
|
+
exceeds; ~30 GPU-h/week quota is not queryable → tracked locally in state as a
|
|
215
|
+
budget. Poll ≥30s with backoff honoring 429/Retry-After.
|
|
216
|
+
- **Colab**: fully automated via the official `google-colab-cli` (v0.6+, June 2026;
|
|
217
|
+
one-time OAuth). Submit = `colab new --gpu <T4|L4|G4|A100|H100>` → `colab upload`
|
|
218
|
+
bundle+bootstrap → `colab exec` a launcher cell that starts `bootstrap.sh`
|
|
219
|
+
detached under the kernel and returns → CLI keep-alive daemon holds the VM.
|
|
220
|
+
Status = short `colab exec` beacon reads (or heartbeat-file check); logs via
|
|
221
|
+
incremental file reads; `colab download` outputs; `colab stop` to release.
|
|
222
|
+
Probe honesty: free tier = T4 lottery + ~12h cap; paid = compute-unit burn
|
|
223
|
+
surfaced as approximate cost. Caveat: keep-alive daemon runs on the client — a
|
|
224
|
+
sleeping laptop may lose idle sessions (the running kernel itself counts as
|
|
225
|
+
activity, so mid-job this is mostly moot).
|
|
226
|
+
|
|
227
|
+
## 8. Marketplace backends
|
|
228
|
+
|
|
229
|
+
`probe` = price/availability query filtered by `gpu_type`/`min_vram`, returns
|
|
230
|
+
cheapest few as offers. `submit` = create instance (stock CUDA+ssh image) → wait for
|
|
231
|
+
ssh → run bootstrap detached → poll. Auto-terminate on completion (configurable
|
|
232
|
+
`keep_alive`), plus `omnirun gc` to reap leaked instances (safety net against
|
|
233
|
+
billing surprises). Idle-timeout watchdog baked into the payload wrapper.
|
|
234
|
+
|
|
235
|
+
## 9. CLI
|
|
236
|
+
|
|
237
|
+
```
|
|
238
|
+
omnirun submit [--name N] [--gpus 1] [--gpu-type H100 | --vram 40] [--time 15h]
|
|
239
|
+
[--backend uni] [--yes] [--max-cost 30] [--dirty] -- python train.py ...
|
|
240
|
+
omnirun offers [same resource flags] # probe & table, no submit
|
|
241
|
+
omnirun ps # all known jobs, refreshed statuses
|
|
242
|
+
omnirun status <job> | logs [-f] <job> | cancel <job> | pull <job> [dest]
|
|
243
|
+
omnirun backends check # config + connectivity sanity per backend
|
|
244
|
+
omnirun gc # reap finished worktrees, leaked instances
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
## 10. Implementation notes
|
|
248
|
+
|
|
249
|
+
- Python ≥3.12, deps: `typer`, `rich`, `httpx`, `pydantic`, `kaggle` (thin API
|
|
250
|
+
client), `google-colab-cli` (optional extra). Marketplaces via plain REST
|
|
251
|
+
(httpx): RunPod REST (`rest.runpod.io/v1`) + one GraphQL pricing query; Vast
|
|
252
|
+
`console.vast.ai/api/v0` (`POST /bundles/` search → `PUT /asks/{id}/` rent);
|
|
253
|
+
Thunder `api.thundercompute.com:8443/v1` (public `/pricing`, `/v2/status`).
|
|
254
|
+
No vendor SDKs, no gpuhunt (live price calls are single cheap requests).
|
|
255
|
+
- SSH: shell out to `ssh`/`rsync` binaries with tool-managed ControlMaster sockets
|
|
256
|
+
(`ControlPath` under `~/.ssh/` with `%C` hash, `ControlPersist=10m`,
|
|
257
|
+
`BatchMode=yes` for background polls only). Only the OpenSSH binary rides
|
|
258
|
+
existing 2FA/Kerberos sessions and honors ProxyJump/Match from `~/.ssh/config` —
|
|
259
|
+
no paramiko/asyncssh.
|
|
260
|
+
- Slurm specifics: submit via `ssh host 'sbatch --parsable' < script`; prefer
|
|
261
|
+
`--gres=gpu:<type>:{n}` (with per-site `--constraint` alternative in gpu_map);
|
|
262
|
+
explicit `--output/--error` under the job dir; namespaced `--job-name`;
|
|
263
|
+
batch-poll all live jobs in one `squeue`/`sacct -X --parsable2` call ≥30s;
|
|
264
|
+
`echo $? > result` backstop for accounting-less clusters; `--dry-run` prints the
|
|
265
|
+
exact sbatch script.
|
|
266
|
+
- Everything async-free: parallel probing via `concurrent.futures`. Simplicity > perf.
|
|
267
|
+
- Module layout:
|
|
268
|
+
```
|
|
269
|
+
src/omnirun/
|
|
270
|
+
models.py # JobSpec, ResourceSpec, Offer, JobStatus, JobHandle
|
|
271
|
+
config.py # TOML load, backend registry, policy
|
|
272
|
+
repo.py # git state, clean/pushed checks, RepoRef, bundle creation
|
|
273
|
+
bootstrap.py # bootstrap.sh generation (shared payload)
|
|
274
|
+
store.py # ~/.local/share/omnirun/jobs/<id>/meta.json
|
|
275
|
+
chooser.py # parallel probing, ranking, offer table
|
|
276
|
+
execlayer/ # base.py (Exec protocol), local.py, ssh.py (ControlMaster)
|
|
277
|
+
backends/ # base.py, local.py, ssh.py, slurm.py, kaggle.py, colab.py,
|
|
278
|
+
# marketplace.py (shared provision→ssh→run), runpod.py,
|
|
279
|
+
# vast.py, thunder.py
|
|
280
|
+
cli.py # typer app
|
|
281
|
+
```
|
|
282
|
+
- Testing tiers: (1) pure unit (codegen, ranking, repo state); (2) e2e through
|
|
283
|
+
`local` backend — full submit→run→pull without network; (3) dockerized sshd and
|
|
284
|
+
slurm cluster integration tests, opt-in; (4) live backends — needs user creds.
|
|
285
|
+
|
|
286
|
+
## 11. Non-goals (v0)
|
|
287
|
+
|
|
288
|
+
Data syncing, artifact versioning, DAGs/pipelines, multi-node jobs, spot-preemption
|
|
289
|
+
recovery, image building, a web UI.
|