gipper 0.3.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.
Files changed (76) hide show
  1. gipper-0.3.0/.env.example +5 -0
  2. gipper-0.3.0/AGENTS.md +89 -0
  3. gipper-0.3.0/CHANGELOG.md +107 -0
  4. gipper-0.3.0/CONTRIBUTING.md +53 -0
  5. gipper-0.3.0/LICENSE +21 -0
  6. gipper-0.3.0/MANIFEST.in +10 -0
  7. gipper-0.3.0/PKG-INFO +275 -0
  8. gipper-0.3.0/README.md +248 -0
  9. gipper-0.3.0/RELEASING.md +67 -0
  10. gipper-0.3.0/ROADMAP.md +12 -0
  11. gipper-0.3.0/SECURITY.md +43 -0
  12. gipper-0.3.0/artifact.toml +25 -0
  13. gipper-0.3.0/deploy/gipper-reap.service +8 -0
  14. gipper-0.3.0/deploy/gipper-reap.timer +9 -0
  15. gipper-0.3.0/docs/clients.md +63 -0
  16. gipper-0.3.0/docs/lessons.md +52 -0
  17. gipper-0.3.0/docs/placement.md +169 -0
  18. gipper-0.3.0/docs/quickstart.md +81 -0
  19. gipper-0.3.0/docs/sources/vast/README.md +13 -0
  20. gipper-0.3.0/examples/smoke/hello.txt +1 -0
  21. gipper-0.3.0/examples/smoke.toml +22 -0
  22. gipper-0.3.0/examples/udp-echo.toml +29 -0
  23. gipper-0.3.0/gipper/__init__.py +2 -0
  24. gipper-0.3.0/gipper/billing.py +85 -0
  25. gipper-0.3.0/gipper/bootstrap.sh +25 -0
  26. gipper-0.3.0/gipper/cli.py +216 -0
  27. gipper-0.3.0/gipper/config.py +91 -0
  28. gipper-0.3.0/gipper/costs.py +125 -0
  29. gipper-0.3.0/gipper/cuda_probe.py +78 -0
  30. gipper-0.3.0/gipper/history.py +232 -0
  31. gipper-0.3.0/gipper/http.py +40 -0
  32. gipper-0.3.0/gipper/job.py +153 -0
  33. gipper-0.3.0/gipper/latency.py +50 -0
  34. gipper-0.3.0/gipper/ledger.py +68 -0
  35. gipper-0.3.0/gipper/lifecycle.py +60 -0
  36. gipper-0.3.0/gipper/market.py +179 -0
  37. gipper-0.3.0/gipper/network_probe.py +78 -0
  38. gipper-0.3.0/gipper/placement.py +211 -0
  39. gipper-0.3.0/gipper/providers/__init__.py +50 -0
  40. gipper-0.3.0/gipper/providers/runpod.py +225 -0
  41. gipper-0.3.0/gipper/providers/shadeform.py +143 -0
  42. gipper-0.3.0/gipper/providers/vast.py +170 -0
  43. gipper-0.3.0/gipper/reap.py +95 -0
  44. gipper-0.3.0/gipper/run.py +358 -0
  45. gipper-0.3.0/gipper/runpod/__init__.py +2 -0
  46. gipper-0.3.0/gipper/runpod/api.py +10 -0
  47. gipper-0.3.0/gipper/runpod/run.py +24 -0
  48. gipper-0.3.0/gipper/ssh.py +82 -0
  49. gipper-0.3.0/gipper.egg-info/PKG-INFO +275 -0
  50. gipper-0.3.0/gipper.egg-info/SOURCES.txt +74 -0
  51. gipper-0.3.0/gipper.egg-info/dependency_links.txt +1 -0
  52. gipper-0.3.0/gipper.egg-info/entry_points.txt +2 -0
  53. gipper-0.3.0/gipper.egg-info/requires.txt +3 -0
  54. gipper-0.3.0/gipper.egg-info/top_level.txt +1 -0
  55. gipper-0.3.0/pyproject.toml +56 -0
  56. gipper-0.3.0/scripts/build_site.py +65 -0
  57. gipper-0.3.0/scripts/check_dist.py +33 -0
  58. gipper-0.3.0/setup.cfg +4 -0
  59. gipper-0.3.0/site/favicon.svg +1 -0
  60. gipper-0.3.0/site/index.html +47 -0
  61. gipper-0.3.0/site/style.css +7 -0
  62. gipper-0.3.0/tests/conftest.py +28 -0
  63. gipper-0.3.0/tests/test_billing.py +85 -0
  64. gipper-0.3.0/tests/test_compat.py +43 -0
  65. gipper-0.3.0/tests/test_config.py +50 -0
  66. gipper-0.3.0/tests/test_costs.py +72 -0
  67. gipper-0.3.0/tests/test_cuda_probe.py +67 -0
  68. gipper-0.3.0/tests/test_history.py +66 -0
  69. gipper-0.3.0/tests/test_job.py +85 -0
  70. gipper-0.3.0/tests/test_latency.py +59 -0
  71. gipper-0.3.0/tests/test_lifecycle.py +71 -0
  72. gipper-0.3.0/tests/test_market.py +44 -0
  73. gipper-0.3.0/tests/test_network_probe.py +51 -0
  74. gipper-0.3.0/tests/test_providers.py +93 -0
  75. gipper-0.3.0/tests/test_reap.py +34 -0
  76. gipper-0.3.0/uv.lock +136 -0
@@ -0,0 +1,5 @@
1
+ # Optional: copy to ~/.config/gipper/.env and chmod 600 that file.
2
+ # Set only the providers you intend to use. Do not commit real credentials.
3
+ RUNPOD_API_KEY=
4
+ VAST_API_KEY=
5
+ SHADEFORM_API_KEY=
gipper-0.3.0/AGENTS.md ADDED
@@ -0,0 +1,89 @@
1
+ # gipper: notes for agents
2
+
3
+ gipper rents GPUs (Runpod, Vast.ai; Shadeform wired in but unused), runs a job,
4
+ brings results home, gives the machine back, and records everything in an
5
+ append-only ledger. Read [README.md](README.md) for what it does and
6
+ [docs/placement.md](docs/placement.md) for how it chooses. This file covers
7
+ working *on* gipper.
8
+
9
+ ## Before you change anything
10
+
11
+ - **Protect running clients.** Editable installs import directly from their
12
+ checkout. Check for active runs before editing a shared installation:
13
+ `pgrep -af "gipper pod run|cloud.py|runner.run"`. Use a git worktree on a
14
+ branch for non-trivial changes, and integrate when no clients are running.
15
+ - **Keep the client surface stable** ([docs/clients.md](docs/clients.md)):
16
+ `run.run`, `run.plan`, `run.estimate`, `run.preflight`, `run.settings`,
17
+ `run.BadHost`, `job.load`, `providers.load(...)` with its methods, `ledger`,
18
+ `ssh.Remote`, and the `[placement]` fields client jobs use. Clients can wrap
19
+ `run.preflight`, `run.plan`, and `run.settings`; `run.py` must keep looking
20
+ these up by module-global name at call time.
21
+ - **Run the tests:** `uv run --extra test pytest -q` (no network or rentals).
22
+ Use `uv run` to select the project's Python environment.
23
+
24
+ ## Invariants
25
+
26
+ 1. **A rented machine is always given back, and only counted given back when
27
+ the provider stops listing it** (`lifecycle.terminate`). Teardown is in
28
+ `finally`. Never rent a replacement while a previous machine's closure is
29
+ unconfirmed.
30
+ 2. **Keys never leave a request header.** Not in argv, logs, receipts or errors;
31
+ `config.redact()` scrubs every provider key, and `http.request` applies it to
32
+ error bodies. Jobs' `[env]` refuses names ending in KEY, TOKEN or SECRET.
33
+ 3. **The ledger is append-only and is the memory.** Never rewrite receipts. New
34
+ facts are new receipt kinds or new payload fields, and readers must tolerate
35
+ old receipts without them: receipts before 2026-10-26 have no `provider` and
36
+ are Runpod's; before 0.2 there's no `machine` on `pod.create`, no `compute_s`
37
+ on `run.end` and no `kind` on `host.reject`.
38
+ 4. **Memory must not blame a host for what isn't its fault.** Job-specific
39
+ rejects raise `run.Unsuitable`; only broken hosts and lost connections count
40
+ as bad; compute time comes only from successful runs; link speed comes only
41
+ from real transfers of 20 MB or more; the billing factor only from rentals of
42
+ 10 minutes or more. A wrong memory steers every later run.
43
+ 5. **Never touch a machine not named `gipper-*`**, and no provider resource
44
+ gipper didn't create.
45
+ 6. **One provider failing never stops the others** (shopping, reaping, reconciling).
46
+ 7. **Stdlib only.** No runtime dependencies.
47
+
48
+ ## Map
49
+
50
+ | File | Job |
51
+ |---|---|
52
+ | `job.py`, `placement.py` | the job file and its `[placement]` hints |
53
+ | `market.py` | `Offer`, GPU-name matching, filters (`admit`), order (`rank`), `shop` |
54
+ | `costs.py` | the whole-run estimate and score for one offer |
55
+ | `history.py` | memory, read from the ledger: failure rates, ready times, link speeds, compute times, billing factors |
56
+ | `run.py` | the run: plan, rent, preflight, upload, detached job, log, checkpoints, collect, teardown |
57
+ | `lifecycle.py` | verified teardown |
58
+ | `reap.py` | the 15-minute sweep for forgotten machines |
59
+ | `billing.py` | providers' bills → `spend.billed`; spend for the daily cap |
60
+ | `cuda_probe.py`, `network_probe.py` | host checks before upload |
61
+ | `providers/{runpod,vast,shadeform}.py` | one module per market behind the interface in `providers/__init__.py` |
62
+ | `runpod/` | import paths kept for pre-0.2 scripts only |
63
+ | `http.py`, `ssh.py`, `ledger.py`, `config.py` | plumbing |
64
+
65
+ ## Adding a provider
66
+
67
+ Implement the interface documented in `providers/__init__.py`. Map locations to
68
+ ISO country codes, memory to decimal GB, and advertised link speeds (Mb/s) and
69
+ fees ($/GB) into `Offer.metrics` under the names `costs.py` reads. Set `udp`
70
+ honestly. Implement `billed()` if the provider has a billing API. Add it to
71
+ `providers.NAMES` and `config.DEFAULTS["providers"]`. Test the parsing with
72
+ response shapes captured from the real API. Then do a real smoke run
73
+ (`examples/smoke.toml --provider <name>`) and, if it claims UDP,
74
+ `examples/udp-echo.toml` plus a packet sent from here, and write what you learn
75
+ in `docs/lessons.md`.
76
+
77
+ ## Tests
78
+
79
+ Test decisions and parsing, not plumbing: what gets chosen, what counts as a bad
80
+ host, what gets billed, what a job file accepts, what the reaper ends. Build
81
+ histories from receipts (`tests/conftest.py: receipt`), and providers' answers
82
+ from real response shapes. Don't build tests that mock `run.run`'s internals
83
+ end to end: an explicitly authorized live smoke run qualifies that path separately.
84
+
85
+ ## Spending money
86
+
87
+ A real rental needs the user's go-ahead unless the task already asked for one.
88
+ Use `--dry-run` and `gipper pod estimate` first, keep `[budget]` tight, and check
89
+ `gipper pod ls` is empty afterwards.
@@ -0,0 +1,107 @@
1
+ # Changelog
2
+
3
+ ## 0.3.0 (2026-11-04)
4
+
5
+ ### Added
6
+
7
+ - Public documentation site, GitHub Pages deployment through Artoo, correctness
8
+ lint checks, and a trusted-publishing workflow for PyPI.
9
+
10
+ - MIT license, public installation/contribution/security guidance, release checklist,
11
+ package metadata, and Linux CI for Python 3.12–3.14 with distribution checks.
12
+ - User-config dotenv support beside `config.toml` for non-editable installs and
13
+ cleanup services; environment and explicit `GIPPER_ENV` keep precedence, and
14
+ the checkout `.env` remains a fallback.
15
+
16
+ ### Changed
17
+
18
+ - Present gipper as a standalone GPU job runner, with workload-neutral examples
19
+ and operational guidance based on each installation's own history.
20
+ - Replace internal planning with a public roadmap and full third-party research
21
+ captures with source references. Document billing and host-trust limitations.
22
+
23
+ ### Fixed
24
+
25
+ - Cost reports include spend receipts without a job ID instead of crashing and
26
+ hiding later rows.
27
+
28
+ ## 0.2.2 (2026-10-28)
29
+
30
+ Round trips are measured and remembered; geography only steers work that needs it.
31
+
32
+ - **Interactive jobs ping candidate hosts before renting** (`probe_rtt`, on by
33
+ default for `profile = "interactive"`): the 48 cheapest offers' public IPs,
34
+ from this machine, in about 10 seconds. About 60% of Vast hosts answer.
35
+ - **Round-trip order of trust:** the job's own measurement from `latency_target`,
36
+ then a ping from here, then memory of that machine, then the median for its
37
+ country, then `preferred_countries`. Pings from here pass `max_rtt_ms` only
38
+ with `latency_target = "here"`; otherwise they rank but don't qualify.
39
+ - **Every rental records `host.rtt`** (fastest of 5 TCP connects to its ssh
40
+ port), and `pod.create` records the offer's `country`. Shops for interactive
41
+ jobs record `market.rtt`.
42
+ - **Link-speed memory is per country** before per provider, so far hosts' slower
43
+ transfers are priced from experience.
44
+ - **Batch jobs ignore geography:** a cheap host on the far side of the world wins
45
+ whenever the whole run costs less.
46
+
47
+ ## 0.2.1 (2026-10-28)
48
+
49
+ Everything from the 0.1 placement work (the `codex/workload-placement-20261027`
50
+ branch) is available again, on top of 0.2's model:
51
+
52
+ - **`profile = "interactive"` with `objective = "latency"`:** offers rank by
53
+ measured round trip (`measured_rtt_ms_by_machine`, `latency_target`), then
54
+ `preferred_countries`, then distance (`near_latitude`/`near_longitude`), then
55
+ the usual score. `max_rtt_ms` and `max_distance_km` fail closed when nothing
56
+ was measured.
57
+ - **Restored fields:** `min_dlperf` (Vast's DLPerf as a filter),
58
+ `client_upload_mbps` and `client_download_mbps` (caps on this end's link),
59
+ `fallback_mbps`.
60
+ - **0.1 names in every estimate:** `estimated_total_usd`,
61
+ `estimated_bandwidth_usd`, `known_bandwidth_usd`, `bandwidth_fees_known`,
62
+ `cost_is_lower_bound`, `setup_download_s`, `upload_s`, `download_s`,
63
+ `compute_s`, `dlperf`, `assumptions` and the rest, so reports that read
64
+ estimate receipts keep their fields. `offer.placement_estimate`,
65
+ `placement.estimate_offer` and `placement.rank_for_job` work again.
66
+ - **Vast source references** behind the placement docs are in `docs/sources/vast/`.
67
+
68
+ ## 0.2.0 (2026-10-28)
69
+
70
+ Offers are judged on the whole run, and gipper learns from its own rentals.
71
+
72
+ - **Whole-run estimate and score for every job** (`costs.py`): ready, setup,
73
+ upload, the job's own downloads, compute and output download; rental plus
74
+ Vast's bandwidth fees, times the provider's billed-to-computed factor; plus the
75
+ chance of a bad host and the value of waiting. Short jobs favour reliable,
76
+ fast hosts and long ones favour price. Replaces the opt-in `[placement]`
77
+ profiles from 0.1; the fields client jobs used still load.
78
+ - **Memory from the ledger** (`history.py`, `gipper pod hosts`): host failure
79
+ rates per provider and per machine, ready times, measured link speeds, compute
80
+ time per workload and GPU, billing factors.
81
+ - **Billed spend** (`billing.py`, `gipper pod reconcile`, run by the reap timer):
82
+ Vast's itemised charges and Runpod's per-pod billing become `spend.billed`;
83
+ the daily cap and `gipper pod cost` use them, attributed to the machine's own day.
84
+ - **New receipts:** `transfer` (timed real uploads and downloads),
85
+ `host.requirements`; `pod.create` gains `machine`, `run.end` gains
86
+ `compute_s`, and `host.reject` gains `kind`.
87
+ - **`run.Unsuitable`** (a `BadHost`): rejects a host that works but doesn't suit
88
+ the job, without counting it against the host in memory.
89
+ - **Runpod:** GPU types its REST API won't create are skipped instead of ending
90
+ the run; `[placement] min_cuda` becomes `allowedCudaVersions`.
91
+ - **Vast:** advertised speeds, fees, CUDA and compute capability on every offer;
92
+ memory in decimal GB, so nominal 48 GB cards qualify; more machines per GPU
93
+ model are considered.
94
+ - **Kept from client contributions in 0.1:** verified teardown, the `cuInit` check
95
+ with its 804 retry, and the optional transfer probe.
96
+ - **Removed:** the default 2× hourly ceiling (the score prices the run; an
97
+ explicit `[gpu] max_usd_per_h` still caps it), and `docs/workload-placement.md`
98
+ (now `docs/placement.md`). Placement's latency and geography fields were
99
+ removed here and restored in 0.2.1.
100
+ - **Docs:** README, `docs/placement.md`, `docs/clients.md`, `docs/lessons.md`,
101
+ `AGENTS.md`.
102
+
103
+ ## 0.1.0 (2026-10-20 to 2026-10-27)
104
+
105
+ Rented GPUs from the CLI: Runpod, then Vast.ai and Shadeform behind one
106
+ interface; regions, UDP ports, preflight and host replacement, caps, reaper,
107
+ receipts.
@@ -0,0 +1,53 @@
1
+ # Contributing
2
+
3
+ Bug reports and focused pull requests are welcome. Include the Python version,
4
+ provider, command, expected behavior, and a redacted error. Never attach API
5
+ keys, SSH keys, raw ledgers, or private job data. See [SECURITY.md](SECURITY.md)
6
+ for vulnerabilities.
7
+
8
+ ## Development
9
+
10
+ On Linux with Python 3.12+ and uv:
11
+
12
+ ```sh
13
+ git clone https://github.com/lavallee/gipper.git
14
+ cd gipper
15
+ uv sync --locked --extra test
16
+ uv run --extra test pytest -q
17
+ ```
18
+
19
+ The tests use fake providers and a temporary ledger: no network or rental is
20
+ needed. Keep the runtime stdlib-only. Preserve the public interfaces in
21
+ [docs/clients.md](docs/clients.md), including module-global hooks used by clients.
22
+ Test provider parsing and placement/cleanup decisions with realistic fixtures.
23
+
24
+ If clients use an editable install of your checkout, check for active runs
25
+ before editing it and work on a branch in a separate worktree. See [AGENTS.md](AGENTS.md).
26
+
27
+ A real smoke run spends money and needs explicit operator authorization. Review
28
+ an estimate and dry run first, use tight budgets, and verify `gipper pod ls`
29
+ and the provider console after cleanup. Never touch resources outside the
30
+ `gipper-*` namespace. Qualify provider changes with live runs separately from
31
+ unit tests; record the result and any untested behavior in `docs/lessons.md`.
32
+
33
+ ## Before a pull request
34
+
35
+ - Run `uv run ruff check gipper tests scripts` and `uv run --extra test pytest -q`.
36
+ - Run `uv build` and `uv run --no-project scripts/check_dist.py`.
37
+ - Update documentation and add an Unreleased changelog entry for user changes.
38
+ - Run a redacted secret scan; never paste credential values into an issue.
39
+
40
+ CI checks Python 3.12, 3.13, and 3.14 on Linux, builds both distributions, and
41
+ smoke-tests the installed wheel. Ruff checks correctness and import errors; avoid unrelated formatting churn.
42
+
43
+ Contributions are made under the repository's MIT license. Only submit material
44
+ you have the right to license; link third-party research sources instead of
45
+ copying complete pages or repositories.
46
+
47
+ ## Documentation site
48
+
49
+ Edit `site/index.html`, `site/style.css`, or the Markdown guides in `docs/`.
50
+ `uv run --group site python scripts/build_site.py` deterministically renders
51
+ `docs/index.html` and `docs/guides/`. The root Artoo artifact owns publishing;
52
+ run `artoo build .`, `artoo verify .`, and `artoo deploy . --dry-run` before
53
+ `artoo deploy .`. GitHub Pages serves `main` / `docs`.
gipper-0.3.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Marc Lavallee
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,10 @@
1
+ include .env.example uv.lock AGENTS.md LICENSE README.md CHANGELOG.md CONTRIBUTING.md SECURITY.md RELEASING.md ROADMAP.md
2
+ recursive-include docs *.md
3
+ recursive-include examples *.toml *.txt
4
+ recursive-include deploy *.service *.timer
5
+ recursive-include tests *.py
6
+ recursive-include scripts *.py
7
+ prune plans
8
+ exclude CLAUDE.md
9
+ include artifact.toml
10
+ recursive-include site *.html *.css *.svg
gipper-0.3.0/PKG-INFO ADDED
@@ -0,0 +1,275 @@
1
+ Metadata-Version: 2.4
2
+ Name: gipper
3
+ Version: 0.3.0
4
+ Summary: Run GPU jobs across cloud markets with cost estimates, verified teardown, and an append-only ledger
5
+ Author: Marc Lavallee
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://lavallee.github.io/gipper/
8
+ Project-URL: Documentation, https://lavallee.github.io/gipper/guides/quickstart.html
9
+ Project-URL: Repository, https://github.com/lavallee/gipper
10
+ Project-URL: Issues, https://github.com/lavallee/gipper/issues
11
+ Project-URL: Changelog, https://github.com/lavallee/gipper/blob/main/CHANGELOG.md
12
+ Keywords: gpu,runpod,vast,cloud,batch
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Environment :: Console
15
+ Classifier: Operating System :: POSIX :: Linux
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Programming Language :: Python :: 3.14
20
+ Classifier: Topic :: System :: Distributed Computing
21
+ Requires-Python: >=3.12
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Provides-Extra: test
25
+ Requires-Dist: pytest; extra == "test"
26
+ Dynamic: license-file
27
+
28
+ # gipper
29
+
30
+ [Website](https://lavallee.github.io/gipper/) · [Quickstart](https://lavallee.github.io/gipper/guides/quickstart.html) · [Python API](https://lavallee.github.io/gipper/guides/clients.html)
31
+
32
+ gipper runs jobs on rented NVIDIA GPUs. Describe the hardware, command, input
33
+ files, outputs, and budget in a TOML file; gipper compares cloud offers, rents a
34
+ machine, checks it, runs the job, collects results, and verifies teardown.
35
+
36
+ Use it for model training and fine-tuning, batch inference, rendering, or other
37
+ GPU workloads that run from a Linux shell. Your job supplies its own software
38
+ and dependencies. The same CLI and Python API work across Runpod and Vast.ai.
39
+
40
+ Placement considers the whole job: startup, setup, transfers, compute, bandwidth
41
+ fees, and host failures. An append-only local ledger records runs and costs;
42
+ future estimates learn from **your own** workloads and provider history. A fresh
43
+ installation starts with stated defaults, not someone else's rental history.
44
+
45
+ **Status: 0.3.0, alpha.** Linux client, CLI and Python API, no daemon. Runpod and
46
+ Vast have live-run validation; Shadeform is experimental and has not had a live
47
+ smoke test. See the [roadmap](ROADMAP.md) for planned features and
48
+ [SECURITY.md](SECURITY.md) for trust and billing limits. Licensed under [MIT](LICENSE).
49
+
50
+ ## Install
51
+
52
+ Requires Linux, Python 3.12+, OpenSSH (`ssh`, `ssh-keygen`), and `tar` with
53
+ gzip support. `ping` is optional for interactive placement. The Python runtime
54
+ has no third-party dependencies. Remote machines need a Linux shell, `tar`,
55
+ `timeout`, `curl`, and NVIDIA tools; the bootstrap installs `uv` if needed.
56
+
57
+ Install from source (no PyPI release is assumed):
58
+
59
+ ```sh
60
+ git clone https://github.com/lavallee/gipper.git
61
+ cd gipper
62
+ uv tool install .
63
+ gipper --help
64
+ ```
65
+
66
+ Use `pip install .` in a virtual environment if you do not use `uv`.
67
+ For development, use `uv sync --extra test` (see [CONTRIBUTING.md](CONTRIBUTING.md)).
68
+
69
+ Configure only the providers you intend to use:
70
+
71
+ ```sh
72
+ mkdir -p ~/.config/gipper
73
+ (umask 077; cp -n .env.example ~/.config/gipper/.env)
74
+ chmod 600 ~/.config/gipper/.env
75
+ # Edit ~/.config/gipper/.env to add your provider keys.
76
+ ```
77
+
78
+ Keys can also come from environment variables: `RUNPOD_API_KEY`, `VAST_API_KEY`,
79
+ `SHADEFORM_API_KEY`. Environment variables take precedence. `GIPPER_ENV` selects
80
+ an exclusive dotenv file; otherwise the user-config dotenv overrides the legacy
81
+ checkout `.env`. A provider without a key is skipped when renting. Keys travel
82
+ in API request headers and are scrubbed from receipts and API error bodies.
83
+ Machines get gipper's own SSH key (`~/.config/gipper/id_ed25519`).
84
+
85
+ Check estimates before paying for a smoke run:
86
+
87
+ ```sh
88
+ gipper pod estimate examples/smoke.toml --provider vast
89
+ gipper pod run examples/smoke.toml --provider vast --dry-run
90
+ # The following command spends money:
91
+ gipper pod run examples/smoke.toml --provider vast
92
+ gipper pod ls
93
+ ```
94
+
95
+ A dry run contacts provider APIs but does not rent. Set provider-side spending
96
+ alerts too: estimates and client-side caps cannot guarantee a final bill.
97
+
98
+ Install the cleanup timer on the machine that runs jobs (recommended):
99
+
100
+ ```sh
101
+ mkdir -p ~/.config/systemd/user
102
+ cp deploy/gipper-reap.* ~/.config/systemd/user/
103
+ systemctl --user daemon-reload
104
+ systemctl --user enable --now gipper-reap.timer
105
+ systemctl --user list-timers gipper-reap.timer
106
+ ```
107
+
108
+ The service expects `~/.local/bin/gipper`; adjust `ExecStart` if installed
109
+ elsewhere. A service cannot see keys exported only in an interactive shell; use
110
+ the user-config dotenv above. User timers require a running user manager. If
111
+ jobs must outlive logout, configure user lingering or an equivalent scheduler.
112
+ See [SECURITY.md](SECURITY.md) for host trust and cleanup limitations.
113
+
114
+ ### Local configuration and data
115
+
116
+ | Path / override | Purpose |
117
+ |---|---|
118
+ | `~/.config/gipper/config.toml` / `GIPPER_CONFIG` | Settings such as `daily_cap_usd = 5.0`; see [`config.DEFAULTS`](gipper/config.py) |
119
+ | `.env` beside the config file / `GIPPER_ENV` | Provider keys (simple `NAME=value`, no shell expansion) |
120
+ | `~/.config/gipper/` / `GIPPER_KEYS` | SSH key directory |
121
+ | `~/.local/share/gipper/` / `GIPPER_DATA` | Append-only receipts used for history and spend |
122
+
123
+ Keep the ledger on durable storage and restrict access to it. Separate installs
124
+ sharing a provider account should coordinate: the reaper owns the `gipper-*`
125
+ namespace, not just the machines in one local ledger.
126
+
127
+ ## Use
128
+
129
+ ```
130
+ gipper pod gpus --min-mem 24 --region US,CA [--udp] # what's on offer, scored for a 30-minute job
131
+ gipper pod estimate job.toml # how long and how much, offer by offer
132
+ gipper pod run job.toml [--dry-run] [--provider vast] [--gpu "RTX 4090"]
133
+ gipper pod hosts # what gipper has learned about providers and machines
134
+ gipper pod cost [--since 2026-10-27] # per job: computed and billed
135
+ gipper pod ls | reap [--dry-run] | reconcile | account
136
+ gipper ledger [--since ...] [--job ID] # raw receipts
137
+ ```
138
+
139
+ A job ([`examples/smoke.toml`](examples/smoke.toml); every field in
140
+ [`gipper/job.py`](gipper/job.py) and [`gipper/placement.py`](gipper/placement.py)):
141
+
142
+ ```toml
143
+ name = "my-gpu-job"
144
+ workload = "my-workload" # jobs with the same workload share a memory of compute times
145
+
146
+ [gpu]
147
+ min_mem_gb = 24
148
+ prefer = ["RTX 4090", "RTX A6000"] # in order; names match across providers
149
+ regions = ["US", "CA"] # ISO country codes; default anywhere
150
+ providers = ["vast", "runpod"] # default: every provider with a key
151
+
152
+ [pod]
153
+ ports = ["5004/udp"] # published; UDP rules out Runpod
154
+
155
+ [[inputs]]
156
+ src = "~/gpu-project"
157
+ dst = "project"
158
+ exclude = [".git", "runs"]
159
+
160
+ [[outputs]]
161
+ src = "out"
162
+ dst = "~/gpu-project/runs/x/out"
163
+
164
+ [run]
165
+ command = "python project/main.py"
166
+ checkpoint = { src = "out", every_s = 300 }
167
+
168
+ [budget]
169
+ max_usd = 1.0
170
+ max_hours = 2.0
171
+
172
+ [placement] # optional, and better than memory's guess
173
+ compute_s = 900
174
+ setup_download_gb = 5 # weights and wheels the job fetches itself
175
+ ```
176
+
177
+ ## How gipper chooses
178
+
179
+ Every offer is judged on the whole run, not its hourly price
180
+ ([docs/placement.md](docs/placement.md) has the details):
181
+
182
+ - **Time:** machine ready + setup + upload + the job's own downloads + compute +
183
+ output download. Each figure comes from the job's `[placement]` hints, from
184
+ memory, from what the provider advertises (Vast publishes host link speeds),
185
+ or from a stated default, and `estimate` says which.
186
+ - **Dollars:** rental for that time, plus Vast's bandwidth fees, times what this
187
+ provider has billed compared with rate × lifetime in your ledger. Without
188
+ qualifying history, the billing factor is 1.0; storage and rounding can make
189
+ the final bill differ.
190
+ - **Risk:** the chance the host turns out bad, from memory: the machine's own
191
+ record, pulled towards its provider's, pulled towards 10% for a stranger.
192
+ A failure costs `failure_cost_usd` ($1) plus the time and rent it wastes.
193
+ - **Score** = dollars + risk + waiting, with an hour of waiting worth
194
+ `hour_value_usd` ($3).
195
+
196
+ For short jobs, startup time and the cost of a failed host can outweigh a lower
197
+ hourly price. For long jobs, the hourly rate has more influence. Provider names
198
+ do not determine the winner: the ranking follows the job and available history.
199
+ `prefer` comes first, and an offer whose expected cost exceeds `max_usd` is
200
+ left out. `[placement] objective = "cost"` removes the value of waiting while
201
+ still pricing failures; `"time"` values waiting at $50/h. For latency-sensitive
202
+ services, `profile = "interactive"` probes candidate hosts and ranks by round
203
+ trip first. Batch jobs account for transfer time without preferring nearby hosts.
204
+
205
+ Memory is the ledger itself. A rental is *good* once its job started, and *bad*
206
+ if the host was broken (busy GPU, CUDA that won't initialise, ssh that never came
207
+ up, a failed probe) or the connection was lost. A host that works but doesn't
208
+ suit the job (`run.Unsuitable`), a job that fails on a working host, and an
209
+ error on this side count against nobody.
210
+
211
+ ## Run lifecycle and safeguards
212
+
213
+ - **The machine is given back, and gipper checks.** Teardown runs in `finally`
214
+ after success, failure, Ctrl-C or a cap, and counts as done only when the
215
+ provider stops listing the machine. Until then it stays
216
+ `pod.termination_pending`, gipper rents no replacement, and the reaper (every
217
+ 15 minutes) retries it first. The reaper also ends any `gipper-*` machine the
218
+ ledger forgot or that passed its deadline, on every provider. The job runs
219
+ detached under `timeout max_hours`. This limits the command, not the rental;
220
+ the reaper must still terminate a machine if the client dies.
221
+ - **Hosts are checked before anything is uploaded:** the GPU is idle,
222
+ `cuInit` succeeds (with one retry without CUDA forward-compat libraries on
223
+ error 804), the compute capability meets the job's floor, and optionally a
224
+ measured transfer probe passes. A bad host is replaced, up to 3 times.
225
+ - **Caps:** per job (`max_usd`, `max_hours`, `[gpu] max_usd_per_h`) and per day
226
+ (`daily_cap_usd` in `~/.config/gipper/config.toml`, default $5). Daily spend
227
+ uses providers' bills where reconciled.
228
+ - **Outputs** sync down every `checkpoint.every_s` and are salvaged on failure.
229
+ - **Environment:** each provider's default image unless `[pod] image` says
230
+ otherwise. Each job sources a bootstrap providing `uv` and
231
+ `GIPPER_TORCH_INDEX`, the PyTorch wheel index for the host's driver. Published
232
+ ports arrive as `GIPPER_PUBLIC_IP` and `GIPPER_PORT_<n>_<PROTO>` (the outside
233
+ port, which on Vast differs from the inside one).
234
+
235
+ ## Markets
236
+
237
+ | | Runpod | Vast.ai | Shadeform (unused) |
238
+ |---|---|---|---|
239
+ | You rent | container, root | container on someone's machine, root | VM on one of 30+ clouds, user `shadeform` |
240
+ | Good for | secure cloud, network volumes | consumer GPUs everywhere, cheap, UDP | datacenter cards with one bill |
241
+ | Ports | TCP/HTTP, **no UDP** | TCP and UDP, random outside port | TCP and UDP (UFW; the cloud may filter) |
242
+ | Bills | per pod per day | itemised: GPU, disk, bandwidth | no billing API |
243
+
244
+ An RTC job that must stay on Runpod can relay media through Cloudflare's TURN
245
+ over TCP/TLS on 443, at some latency cost.
246
+
247
+ ## Python API
248
+
249
+ The CLI is the simplest surface. From Python (gipper is stdlib-only):
250
+
251
+ ```python
252
+ from gipper import run, ledger
253
+ from gipper.job import load
254
+ from gipper.providers import load as provider
255
+
256
+ job = load("job.toml")
257
+ offers, ceiling, notes = run.plan(job, provider="vast") # ranked; each has .estimate
258
+ run_id, outcome = run.run(job, provider="vast", log_dir="runs/x")
259
+ provider("vast").list_ours() # for your own guards
260
+ ```
261
+
262
+ `run.preflight(remote)` can be wrapped to add host checks of your own: raise
263
+ `run.Unsuitable` when the host works but doesn't suit the job, and `run.BadHost`
264
+ when it's broken. Both replace the host, but only `BadHost` counts against it in
265
+ memory. To move a job between providers, check [docs/clients.md](docs/clients.md).
266
+
267
+ ## More
268
+
269
+ - [docs/placement.md](docs/placement.md): the cost model, memory and every `[placement]` field
270
+ - [docs/clients.md](docs/clients.md): writing jobs that run on any provider; the Python surface
271
+ - [docs/lessons.md](docs/lessons.md): operational behavior and troubleshooting
272
+ - [CONTRIBUTING.md](CONTRIBUTING.md): development and tests
273
+ - [SECURITY.md](SECURITY.md): trust model and vulnerability reports
274
+ - [RELEASING.md](RELEASING.md): release checklist
275
+ - [CHANGELOG.md](CHANGELOG.md)