jambu-gpu 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.
- jambu_gpu-0.1.0/.env.example +11 -0
- jambu_gpu-0.1.0/.gitignore +15 -0
- jambu_gpu-0.1.0/CONTRIBUTING.md +75 -0
- jambu_gpu-0.1.0/LICENSE +21 -0
- jambu_gpu-0.1.0/PKG-INFO +82 -0
- jambu_gpu-0.1.0/README.md +59 -0
- jambu_gpu-0.1.0/docs/ACCEPTANCE.md +28 -0
- jambu_gpu-0.1.0/docs/ADDING_A_PROVIDER.md +121 -0
- jambu_gpu-0.1.0/docs/CHOOSING_A_MODEL.md +198 -0
- jambu_gpu-0.1.0/docs/CONFIG.md +223 -0
- jambu_gpu-0.1.0/docs/LIFECYCLE.md +75 -0
- jambu_gpu-0.1.0/docs/USAGE.md +91 -0
- jambu_gpu-0.1.0/docs/assets/jambu-logo.jpg +0 -0
- jambu_gpu-0.1.0/examples/experiment.py +48 -0
- jambu_gpu-0.1.0/examples/jambu.yaml +36 -0
- jambu_gpu-0.1.0/examples/mastra-agent/README.md +70 -0
- jambu_gpu-0.1.0/examples/mastra-agent/package-lock.json +2235 -0
- jambu_gpu-0.1.0/examples/mastra-agent/package.json +19 -0
- jambu_gpu-0.1.0/examples/mastra-agent/src/agent.ts +42 -0
- jambu_gpu-0.1.0/examples/mastra-agent/src/index.ts +11 -0
- jambu_gpu-0.1.0/examples/mastra-agent/src/tools/weather-tool.ts +36 -0
- jambu_gpu-0.1.0/examples/mastra-agent/tsconfig.json +15 -0
- jambu_gpu-0.1.0/jambu.example.yaml +77 -0
- jambu_gpu-0.1.0/jambu.models.yaml +102 -0
- jambu_gpu-0.1.0/jambu.yaml +42 -0
- jambu_gpu-0.1.0/jambu_gpu/__init__.py +5 -0
- jambu_gpu-0.1.0/jambu_gpu/__main__.py +4 -0
- jambu_gpu-0.1.0/jambu_gpu/cli/__init__.py +0 -0
- jambu_gpu-0.1.0/jambu_gpu/cli/_common.py +117 -0
- jambu_gpu-0.1.0/jambu_gpu/cli/inspect.py +207 -0
- jambu_gpu-0.1.0/jambu_gpu/cli/lifecycle.py +127 -0
- jambu_gpu-0.1.0/jambu_gpu/cli/main.py +103 -0
- jambu_gpu-0.1.0/jambu_gpu/cli/providers.py +96 -0
- jambu_gpu-0.1.0/jambu_gpu/cli/run.py +37 -0
- jambu_gpu-0.1.0/jambu_gpu/cli/setup.py +165 -0
- jambu_gpu-0.1.0/jambu_gpu/cli/status.py +214 -0
- jambu_gpu-0.1.0/jambu_gpu/cli/validate.py +57 -0
- jambu_gpu-0.1.0/jambu_gpu/core/__init__.py +0 -0
- jambu_gpu-0.1.0/jambu_gpu/core/beacon.py +124 -0
- jambu_gpu-0.1.0/jambu_gpu/core/config.py +596 -0
- jambu_gpu-0.1.0/jambu_gpu/core/credentials.py +75 -0
- jambu_gpu-0.1.0/jambu_gpu/core/durations.py +47 -0
- jambu_gpu-0.1.0/jambu_gpu/core/engine.py +951 -0
- jambu_gpu-0.1.0/jambu_gpu/core/errors.py +68 -0
- jambu_gpu-0.1.0/jambu_gpu/core/events.py +115 -0
- jambu_gpu-0.1.0/jambu_gpu/core/execution.py +169 -0
- jambu_gpu-0.1.0/jambu_gpu/core/health.py +62 -0
- jambu_gpu-0.1.0/jambu_gpu/core/model_inspect.py +368 -0
- jambu_gpu-0.1.0/jambu_gpu/core/models.py +283 -0
- jambu_gpu-0.1.0/jambu_gpu/core/policy.py +233 -0
- jambu_gpu-0.1.0/jambu_gpu/core/state.py +361 -0
- jambu_gpu-0.1.0/jambu_gpu/core/tool_parsers.py +63 -0
- jambu_gpu-0.1.0/jambu_gpu/providers/__init__.py +4 -0
- jambu_gpu-0.1.0/jambu_gpu/providers/base.py +125 -0
- jambu_gpu-0.1.0/jambu_gpu/providers/registry.py +63 -0
- jambu_gpu-0.1.0/jambu_gpu/providers/vast/__init__.py +3 -0
- jambu_gpu-0.1.0/jambu_gpu/providers/vast/client.py +230 -0
- jambu_gpu-0.1.0/jambu_gpu/providers/vast/config.py +51 -0
- jambu_gpu-0.1.0/jambu_gpu/providers/vast/mapper.py +221 -0
- jambu_gpu-0.1.0/jambu_gpu/providers/vast/provider.py +422 -0
- jambu_gpu-0.1.0/jambu_gpu/runtime/__init__.py +6 -0
- jambu_gpu-0.1.0/jambu_gpu/runtime/base.py +99 -0
- jambu_gpu-0.1.0/jambu_gpu/runtime/vllm.py +224 -0
- jambu_gpu-0.1.0/jambu_gpu/watchdog/__init__.py +3 -0
- jambu_gpu-0.1.0/jambu_gpu/watchdog/agent_body.py +547 -0
- jambu_gpu-0.1.0/jambu_gpu/watchdog/builder.py +178 -0
- jambu_gpu-0.1.0/pyproject.toml +47 -0
- jambu_gpu-0.1.0/tests/conftest.py +72 -0
- jambu_gpu-0.1.0/tests/fakes.py +156 -0
- jambu_gpu-0.1.0/tests/test_cli.py +240 -0
- jambu_gpu-0.1.0/tests/test_config.py +322 -0
- jambu_gpu-0.1.0/tests/test_engine.py +366 -0
- jambu_gpu-0.1.0/tests/test_integration.py +289 -0
- jambu_gpu-0.1.0/tests/test_model_inspect.py +219 -0
- jambu_gpu-0.1.0/tests/test_policy.py +157 -0
- jambu_gpu-0.1.0/tests/test_runtime_vllm.py +168 -0
- jambu_gpu-0.1.0/tests/test_state.py +155 -0
- jambu_gpu-0.1.0/tests/test_vast_mapper.py +119 -0
- jambu_gpu-0.1.0/tests/test_vast_provider.py +298 -0
- jambu_gpu-0.1.0/tests/test_watchdog.py +327 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Credentials are resolved from the environment, this file, or
|
|
2
|
+
# ~/.jambu/credentials - never from jambu.yaml.
|
|
3
|
+
|
|
4
|
+
# Required by the vast provider.
|
|
5
|
+
VAST_API_KEY=
|
|
6
|
+
|
|
7
|
+
# Optional: needed for gated HuggingFace models.
|
|
8
|
+
HF_TOKEN=
|
|
9
|
+
|
|
10
|
+
# Optional: per-profile override, used when provider.profile: staging
|
|
11
|
+
# VAST_API_KEY__staging=
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
`jambu-gpu` provisions a temporary GPU runtime and guarantees it stops billing
|
|
4
|
+
when nothing is using it. Contributions are welcome. By submitting a change you
|
|
5
|
+
agree it is licensed under the [MIT License](LICENSE).
|
|
6
|
+
|
|
7
|
+
## Command name
|
|
8
|
+
|
|
9
|
+
The published command is `gpu`. The package name stays `jambu-gpu`.
|
|
10
|
+
|
|
11
|
+
Do not add a second console script. One command, in `pyproject.toml`:
|
|
12
|
+
|
|
13
|
+
```toml
|
|
14
|
+
[project.scripts]
|
|
15
|
+
gpu = "jambu_gpu.cli.main:main"
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Setup
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
python3 -m venv .venv
|
|
22
|
+
.venv/bin/pip install -e ".[dev]"
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Python 3.10 or newer. Tests do not need `VAST_API_KEY`, a GPU, or network access.
|
|
26
|
+
|
|
27
|
+
## Tests
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
.venv/bin/pytest
|
|
31
|
+
.venv/bin/pytest tests/test_policy.py # shutdown rule
|
|
32
|
+
.venv/bin/pytest tests/test_watchdog.py # generated guard, as a real process
|
|
33
|
+
.venv/bin/pytest tests/test_integration.py # setup -> boot -> run -> auto-stop
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
A change to lifecycle behaviour needs a test that fails before the change and
|
|
37
|
+
passes after it. The policy in `jambu_gpu/core/policy.py` and the watchdog that
|
|
38
|
+
runs on the instance must keep agreeing: the watchdog inlines that module at
|
|
39
|
+
provision time.
|
|
40
|
+
|
|
41
|
+
`tests/test_integration.py` does not mock the boot. It runs the onstart script
|
|
42
|
+
the engine generated, starts the real watchdog, waits on a real health endpoint,
|
|
43
|
+
runs a workload with real heartbeats, and asserts the guard stops the instance
|
|
44
|
+
with the right reason.
|
|
45
|
+
|
|
46
|
+
## What you may change
|
|
47
|
+
|
|
48
|
+
| Change | Where it goes |
|
|
49
|
+
| --- | --- |
|
|
50
|
+
| New provider | `jambu_gpu/providers/<name>/` plus one registry line. See [docs/ADDING_A_PROVIDER.md](docs/ADDING_A_PROVIDER.md). |
|
|
51
|
+
| Shutdown rule | `jambu_gpu/core/policy.py` and `tests/test_policy.py`. |
|
|
52
|
+
| How the model server starts | `jambu_gpu/runtime/`. |
|
|
53
|
+
| CLI surface | `jambu_gpu/cli/`. Keep `--json` and `--config` on every command. |
|
|
54
|
+
|
|
55
|
+
The core must not branch on a provider name. Branch on `provider.capabilities`.
|
|
56
|
+
If a provider feature requires an `if provider == "..."` in `core/`, the contract
|
|
57
|
+
is wrong — extend the contract instead.
|
|
58
|
+
|
|
59
|
+
Out of scope: multi-instance scheduling, multi-GPU
|
|
60
|
+
orchestration, Kubernetes, distributed inference, automatic provider selection,
|
|
61
|
+
job queues, a web dashboard. See [docs/ACCEPTANCE.md](docs/ACCEPTANCE.md).
|
|
62
|
+
|
|
63
|
+
## Secrets
|
|
64
|
+
|
|
65
|
+
Never commit credentials, `.env`, `.jambu/state.json`, or provider instance
|
|
66
|
+
ids from a real run. Keys are read from the environment, a gitignored `.env`,
|
|
67
|
+
or `~/.jambu/credentials`. `.env.example` ships with empty values only.
|
|
68
|
+
|
|
69
|
+
## Pull requests
|
|
70
|
+
|
|
71
|
+
- Explain why the behaviour should change, not only what moved.
|
|
72
|
+
- Run `.venv/bin/pytest` and report the result.
|
|
73
|
+
- Keep commits focused. A provider adapter and a policy change are two PRs.
|
|
74
|
+
- Match the surrounding style: English identifiers and docs, type hints on new
|
|
75
|
+
public functions, no new dependency unless the standard library cannot do the job.
|
jambu_gpu-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jambu.ai and João Gabriel Lima
|
|
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.
|
jambu_gpu-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: jambu-gpu
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Provider-agnostic CLI for provisioning and managing temporary GPU runtimes
|
|
5
|
+
Project-URL: Homepage, https://github.com/jambuai/jambu-gpu
|
|
6
|
+
Project-URL: Repository, https://github.com/jambuai/jambu-gpu
|
|
7
|
+
Project-URL: Issues, https://github.com/jambuai/jambu-gpu/issues
|
|
8
|
+
Author: Jambu.ai
|
|
9
|
+
Author-email: João Gabriel Lima <joao.gabriel@jambu.ai>
|
|
10
|
+
License: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Requires-Python: >=3.10
|
|
13
|
+
Requires-Dist: httpx>=0.27.0
|
|
14
|
+
Requires-Dist: pydantic>=2.6.0
|
|
15
|
+
Requires-Dist: pyyaml>=6.0.1
|
|
16
|
+
Requires-Dist: rich>=13.7.0
|
|
17
|
+
Requires-Dist: typer>=0.12.0
|
|
18
|
+
Provides-Extra: dev
|
|
19
|
+
Requires-Dist: pytest-cov>=5.0.0; extra == 'dev'
|
|
20
|
+
Requires-Dist: pytest>=8.0.0; extra == 'dev'
|
|
21
|
+
Requires-Dist: respx>=0.21.0; extra == 'dev'
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# gpu
|
|
25
|
+
|
|
26
|
+
Temporary GPU runtimes that stop billing when the work stops.
|
|
27
|
+
|
|
28
|
+
You declare the model and how long it may live. `gpu` provisions a machine, starts the model server, and runs your job against it. Shutdown happens on the instance, so closing the laptop does not leave the GPU billing.
|
|
29
|
+
|
|
30
|
+
Vast.ai is the first provider. Another provider is a new adapter. The lifecycle rule stays the same.
|
|
31
|
+
|
|
32
|
+
## Run
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install jambu-gpu
|
|
36
|
+
|
|
37
|
+
cd your-experiment
|
|
38
|
+
gpu init --model empero-ai/Qwythos-9B-v2
|
|
39
|
+
export VAST_API_KEY=...
|
|
40
|
+
|
|
41
|
+
gpu validate
|
|
42
|
+
gpu setup
|
|
43
|
+
gpu run python experiment.py
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
When you are finished, `gpu destroy`. If you walk away, the idle timeout does it.
|
|
47
|
+
|
|
48
|
+
The API key is read from the environment, a project `.env`, or `~/.jambu/credentials`. It never goes in the manifest.
|
|
49
|
+
|
|
50
|
+
## Declare
|
|
51
|
+
|
|
52
|
+
```yaml
|
|
53
|
+
gpu_runtime:
|
|
54
|
+
provider: {name: vast}
|
|
55
|
+
model_profile: qwythos-9b
|
|
56
|
+
lifecycle:
|
|
57
|
+
idle_timeout: 15m
|
|
58
|
+
max_lifetime: 6h
|
|
59
|
+
budget:
|
|
60
|
+
max_hourly_cost_usd: 1.50
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
The file is `jambu.yaml`. This tool reads only the `gpu_runtime:` key, so other Jambu tools can share the same file. A full example is [`jambu.example.yaml`](jambu.example.yaml).
|
|
64
|
+
|
|
65
|
+
## Docs
|
|
66
|
+
|
|
67
|
+
- [Commands, workloads, and agents](docs/USAGE.md)
|
|
68
|
+
- [Every config field](docs/CONFIG.md)
|
|
69
|
+
- [Choosing a model](docs/CHOOSING_A_MODEL.md)
|
|
70
|
+
- [How shutdown works](docs/LIFECYCLE.md)
|
|
71
|
+
- [Adding a provider](docs/ADDING_A_PROVIDER.md)
|
|
72
|
+
- [Contributing](CONTRIBUTING.md)
|
|
73
|
+
|
|
74
|
+
## License
|
|
75
|
+
|
|
76
|
+
[MIT](LICENSE).
|
|
77
|
+
|
|
78
|
+
<p align="center">
|
|
79
|
+
<a href="https://jambu.ai">
|
|
80
|
+
<img src="docs/assets/jambu-logo.jpg" alt="Jambu.ai — Applied Intelligence Lab" width="280">
|
|
81
|
+
</a>
|
|
82
|
+
</p>
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# gpu
|
|
2
|
+
|
|
3
|
+
Temporary GPU runtimes that stop billing when the work stops.
|
|
4
|
+
|
|
5
|
+
You declare the model and how long it may live. `gpu` provisions a machine, starts the model server, and runs your job against it. Shutdown happens on the instance, so closing the laptop does not leave the GPU billing.
|
|
6
|
+
|
|
7
|
+
Vast.ai is the first provider. Another provider is a new adapter. The lifecycle rule stays the same.
|
|
8
|
+
|
|
9
|
+
## Run
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install jambu-gpu
|
|
13
|
+
|
|
14
|
+
cd your-experiment
|
|
15
|
+
gpu init --model empero-ai/Qwythos-9B-v2
|
|
16
|
+
export VAST_API_KEY=...
|
|
17
|
+
|
|
18
|
+
gpu validate
|
|
19
|
+
gpu setup
|
|
20
|
+
gpu run python experiment.py
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
When you are finished, `gpu destroy`. If you walk away, the idle timeout does it.
|
|
24
|
+
|
|
25
|
+
The API key is read from the environment, a project `.env`, or `~/.jambu/credentials`. It never goes in the manifest.
|
|
26
|
+
|
|
27
|
+
## Declare
|
|
28
|
+
|
|
29
|
+
```yaml
|
|
30
|
+
gpu_runtime:
|
|
31
|
+
provider: {name: vast}
|
|
32
|
+
model_profile: qwythos-9b
|
|
33
|
+
lifecycle:
|
|
34
|
+
idle_timeout: 15m
|
|
35
|
+
max_lifetime: 6h
|
|
36
|
+
budget:
|
|
37
|
+
max_hourly_cost_usd: 1.50
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The file is `jambu.yaml`. This tool reads only the `gpu_runtime:` key, so other Jambu tools can share the same file. A full example is [`jambu.example.yaml`](jambu.example.yaml).
|
|
41
|
+
|
|
42
|
+
## Docs
|
|
43
|
+
|
|
44
|
+
- [Commands, workloads, and agents](docs/USAGE.md)
|
|
45
|
+
- [Every config field](docs/CONFIG.md)
|
|
46
|
+
- [Choosing a model](docs/CHOOSING_A_MODEL.md)
|
|
47
|
+
- [How shutdown works](docs/LIFECYCLE.md)
|
|
48
|
+
- [Adding a provider](docs/ADDING_A_PROVIDER.md)
|
|
49
|
+
- [Contributing](CONTRIBUTING.md)
|
|
50
|
+
|
|
51
|
+
## License
|
|
52
|
+
|
|
53
|
+
[MIT](LICENSE).
|
|
54
|
+
|
|
55
|
+
<p align="center">
|
|
56
|
+
<a href="https://jambu.ai">
|
|
57
|
+
<img src="docs/assets/jambu-logo.jpg" alt="Jambu.ai — Applied Intelligence Lab" width="280">
|
|
58
|
+
</a>
|
|
59
|
+
</p>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Acceptance criteria — where each one is implemented and tested
|
|
2
|
+
|
|
3
|
+
Twelve guarantees, mapped to the code and the test that holds each one.
|
|
4
|
+
|
|
5
|
+
| # | Guarantee | Implementation | Test |
|
|
6
|
+
| --- | --- | --- | --- |
|
|
7
|
+
| 1 | Repeated `setup` does not duplicate infrastructure | `core/engine.py::Engine.setup` reconciles first, compares `config.fingerprint()`, and holds an exclusive `.jambu/runtime.lock` | `test_engine.py::test_setup_twice_does_not_create_two_instances`, `::test_setup_refuses_to_reuse_an_incompatible_instance` |
|
|
8
|
+
| 2 | Provider APIs do not leak into core orchestration | `providers/base.py` contract; all Vast wire format confined to `providers/vast/{client,mapper}.py` | `test_engine.py::test_the_core_never_branches_on_a_provider_name` (greps `core/`) |
|
|
9
|
+
| 3 | Missing credentials fail before provisioning | `VastProvider.validate` returns before any API call; `CredentialResolver` never reads jambu.yaml | `test_vast_provider.py::test_validate_fails_before_provisioning_without_a_key` (asserts zero HTTP calls) |
|
|
10
|
+
| 4 | Failed provisioning cleans up | `Engine.setup` persists state *before* the long wait, then destroys on failure when `cleanup_on_setup_failure` | `test_engine.py::test_failed_setup_cleans_up_partially_created_resources` |
|
|
11
|
+
| 5 | Active workloads are never stopped by the idle guard | `core/policy.py::evaluate` returns `workload_active` before any idle check | `test_policy.py::test_active_workload_is_never_stopped_by_the_idle_guard`, `test_watchdog.py::test_a_heartbeating_workload_is_never_stopped` |
|
|
12
|
+
| 6 | An idle unlocked instance stops within `idle_timeout + watchdog_interval` | remote watchdog loop, `watchdog/agent_body.py::guard_loop` | `test_watchdog.py::test_idle_instance_is_stopped_with_a_recorded_reason` (real subprocess) |
|
|
13
|
+
| 7 | `max_lifetime` bounds forgotten instances | `policy.evaluate` checks age before activity | `test_policy.py::test_max_lifetime_stops_even_when_busy`, `test_watchdog.py::test_max_lifetime_stops_a_busy_instance` |
|
|
14
|
+
| 8 | Locks prevent shutdown until expiry | `policy.Snapshot.lock_active` / `lock_expired`; TTL required unless `allow_indefinite_lock` | `test_policy.py::test_expired_lock_stops_protecting`, `test_watchdog.py::test_a_lock_prevents_shutdown_until_it_expires` |
|
|
15
|
+
| 9 | A crashed CLI does not disable protection | the guard runs *on the instance*; heartbeats go stale after `heartbeat_grace` | `test_watchdog.py::test_a_vanished_heartbeat_falls_back_to_idle`, `::test_startup_grace_expires_when_the_cli_never_confirms` |
|
|
16
|
+
| 10 | Remote state is reconciled | `Engine._reconcile` merges provider truth + watchdog truth and repairs local state | `test_engine.py::test_status_repairs_state_when_the_instance_vanished` |
|
|
17
|
+
| 11 | A new provider needs only the contract | `providers/registry.py`; the engine is driven end to end by `tests/fakes.py::FakeProvider` | `test_engine.py::test_a_new_provider_needs_only_the_contract` |
|
|
18
|
+
| 12 | Every automatic shutdown records its reason | `lifecycle_guard_triggered` + `instance_stopped` events carry `reason`, locally and on the instance | `test_engine.py::test_guard_stops_an_idle_unlocked_instance_and_records_the_reason`, `test_watchdog.py::test_idle_instance_is_stopped_with_a_recorded_reason` |
|
|
19
|
+
|
|
20
|
+
## The MVP command surface
|
|
21
|
+
|
|
22
|
+
`jambu.yaml` schema, CLI, provider contract, provider registry, Vast.ai adapter,
|
|
23
|
+
vLLM runtime, `setup`, `run`, `status`, `stop`, `destroy`, `lock`/`unlock`, idle
|
|
24
|
+
timeout, max lifetime, remote watchdog, structured logs — all present.
|
|
25
|
+
|
|
26
|
+
Deliberately not implemented: multi-instance scheduling, multi-GPU
|
|
27
|
+
orchestration, Kubernetes, distributed inference, provider optimization, automatic
|
|
28
|
+
provider selection, job queues, web dashboard.
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# Adding a provider
|
|
2
|
+
|
|
3
|
+
Adding a provider means adding `jambu_gpu/providers/<name>/` and one line in the
|
|
4
|
+
registry. **No lifecycle, execution or CLI code changes.** If you find yourself
|
|
5
|
+
editing `core/`, the contract is leaking.
|
|
6
|
+
|
|
7
|
+
## 1. Implement the contract
|
|
8
|
+
|
|
9
|
+
```python
|
|
10
|
+
# jambu_gpu/providers/runpod/provider.py
|
|
11
|
+
from ...core.models import ProviderCapabilities
|
|
12
|
+
from ..base import GPUProvider, WatchdogSpec
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class RunpodProvider(GPUProvider):
|
|
16
|
+
name = "runpod"
|
|
17
|
+
display_name = "RunPod"
|
|
18
|
+
required_credentials = ["RUNPOD_API_KEY"]
|
|
19
|
+
capabilities = ProviderCapabilities(
|
|
20
|
+
stop=True,
|
|
21
|
+
destroy=True,
|
|
22
|
+
ssh=True,
|
|
23
|
+
public_ports=True,
|
|
24
|
+
gpu_selection=True,
|
|
25
|
+
pricing_query=True,
|
|
26
|
+
remote_exec=True,
|
|
27
|
+
logs=True,
|
|
28
|
+
stop_eliminates_billing=True,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
def validate(self, config): ...
|
|
32
|
+
def setup(self, spec): ...
|
|
33
|
+
def start(self, instance): ...
|
|
34
|
+
def run(self, instance, workload): ...
|
|
35
|
+
def status(self, instance): ...
|
|
36
|
+
def stop(self, instance): ...
|
|
37
|
+
def destroy(self, instance): ...
|
|
38
|
+
def logs(self, instance): ...
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Suggested layout, mirroring the Vast adapter:
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
providers/runpod/
|
|
45
|
+
├── provider.py the contract implementation
|
|
46
|
+
├── client.py raw HTTP; the ONLY place that knows the wire format
|
|
47
|
+
├── mapper.py payloads <-> normalized models, state mapping
|
|
48
|
+
└── config.py provider.options schema and constants
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## 2. Declare capabilities honestly
|
|
52
|
+
|
|
53
|
+
The core never branches on `provider.name` — only on `provider.capabilities`. Set
|
|
54
|
+
`stop_eliminates_billing=False` if a stopped instance still costs money; the policy
|
|
55
|
+
then knows it may need to escalate to destroy.
|
|
56
|
+
|
|
57
|
+
## 3. Normalize state and errors
|
|
58
|
+
|
|
59
|
+
Map every provider status onto `InstanceState`
|
|
60
|
+
(`provisioning / starting / ready / busy / idle / stopped / failed / destroyed`),
|
|
61
|
+
and raise only the normalized errors from `core/errors.py`:
|
|
62
|
+
`AuthenticationError`, `CapacityUnavailableError`, `ProvisioningError`,
|
|
63
|
+
`ProviderTimeoutError`, `UnsupportedCapabilityError`. CLI behaviour must be
|
|
64
|
+
identical across providers.
|
|
65
|
+
|
|
66
|
+
## 4. Supply the watchdog actions
|
|
67
|
+
|
|
68
|
+
The lifecycle *policy* is the core's; the adapter contributes only the two calls
|
|
69
|
+
that shut the machine down from inside itself:
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
WATCHDOG_ACTIONS = '''
|
|
73
|
+
def provider_stop(ctx):
|
|
74
|
+
... # stdlib only: urllib, json, os
|
|
75
|
+
|
|
76
|
+
def provider_destroy(ctx):
|
|
77
|
+
...
|
|
78
|
+
'''
|
|
79
|
+
|
|
80
|
+
def watchdog_spec(self, spec_or_instance=None):
|
|
81
|
+
return WatchdogSpec(
|
|
82
|
+
actions_source=WATCHDOG_ACTIONS,
|
|
83
|
+
env={"RUNPOD_API_KEY": self.credentials.get("RUNPOD_API_KEY") or ""},
|
|
84
|
+
context={"provider": self.name},
|
|
85
|
+
)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
The instance id may not exist when the boot script is generated. Either resolve it
|
|
89
|
+
from the container's own environment (as the Vast adapter does with
|
|
90
|
+
`VAST_CONTAINERLABEL`), or rely on the CLI's `/identify` call — support both if you
|
|
91
|
+
can, so the guard works even if the CLI never reconnects.
|
|
92
|
+
|
|
93
|
+
Return `None` only if the provider genuinely cannot self-terminate; users then have
|
|
94
|
+
to run `gpu guard` externally, and `validate` will warn.
|
|
95
|
+
|
|
96
|
+
## 5. Publish the ports
|
|
97
|
+
|
|
98
|
+
`ComputeSpec.ports` lists the container ports the core needs published. Map them to
|
|
99
|
+
`Instance.endpoints` under the names `model` and `watchdog`. Without a published
|
|
100
|
+
watchdog port the remote guard is unreachable.
|
|
101
|
+
|
|
102
|
+
## 6. Register it
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
# providers/registry.py
|
|
106
|
+
def load_builtin_providers():
|
|
107
|
+
from .vast.provider import VastProvider
|
|
108
|
+
from .runpod.provider import RunpodProvider
|
|
109
|
+
|
|
110
|
+
registry.register(VastProvider)
|
|
111
|
+
registry.register(RunpodProvider)
|
|
112
|
+
return registry
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## 7. Test it
|
|
116
|
+
|
|
117
|
+
Copy `tests/test_vast_provider.py` and swap the mocked payloads. Then check the
|
|
118
|
+
adapter really is swappable — `tests/test_engine.py` drives the whole engine
|
|
119
|
+
through `tests/fakes.py::FakeProvider` with zero core changes, and
|
|
120
|
+
`test_the_core_never_branches_on_a_provider_name` fails the build if `core/` learns
|
|
121
|
+
a provider's name.
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# Where jambu.yaml's model-specific values come from
|
|
2
|
+
|
|
3
|
+
Every value in `model:`/`compute.gpu:`/`runtime:` that depends on *which model*
|
|
4
|
+
you're deploying (VRAM, dtype, context length, tool-call parser,
|
|
5
|
+
`trust_remote_code`) has a real source. This is that source, and the command
|
|
6
|
+
that reads it for you.
|
|
7
|
+
|
|
8
|
+
## `gpu inspect-model`
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
gpu inspect-model Qwen/Qwen2.5-7B-Instruct
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
```text
|
|
15
|
+
Qwen/Qwen2.5-7B-Instruct @ main
|
|
16
|
+
architecture Qwen2ForCausalLM (model_type: qwen2)
|
|
17
|
+
parameters 7.62B (BF16)
|
|
18
|
+
weight size ~15.2 GB -> suggested min_vram_gb: 22
|
|
19
|
+
context native 32768
|
|
20
|
+
tool calling hermes
|
|
21
|
+
|
|
22
|
+
Sources
|
|
23
|
+
https://huggingface.co/api/models/Qwen/Qwen2.5-7B-Instruct
|
|
24
|
+
https://huggingface.co/Qwen/Qwen2.5-7B-Instruct/raw/main/config.json
|
|
25
|
+
|
|
26
|
+
Suggested gpu_runtime: snippet
|
|
27
|
+
gpu_runtime:
|
|
28
|
+
model:
|
|
29
|
+
id: Qwen/Qwen2.5-7B-Instruct
|
|
30
|
+
|
|
31
|
+
compute:
|
|
32
|
+
gpu:
|
|
33
|
+
min_vram_gb: 22 # estimate, see notes
|
|
34
|
+
count: 1
|
|
35
|
+
|
|
36
|
+
runtime:
|
|
37
|
+
engine: vllm
|
|
38
|
+
context_length: 32768
|
|
39
|
+
tool_calling: auto # resolves to hermes automatically
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Paste the snippet under `gpu_runtime:` in your `jambu.yaml`, adjust
|
|
43
|
+
`lifecycle`/`health`/`budget` for your situation, and you have a starting
|
|
44
|
+
config. Add `--json` for machine-readable output, `--revision` for a
|
|
45
|
+
non-`main` branch/tag, `--hf-token-env` if the repo is gated and your token
|
|
46
|
+
lives under a different env var than `HF_TOKEN`.
|
|
47
|
+
|
|
48
|
+
**This is a starting point, not an oracle.** It never talks to Vast.ai and
|
|
49
|
+
never boots anything — it only reads two public HuggingFace documents and does
|
|
50
|
+
arithmetic. Confirm the real number with `gpu offers` (finds actual
|
|
51
|
+
capacity at that size) and a real `gpu setup` (the only way to know a
|
|
52
|
+
model genuinely boots and serves).
|
|
53
|
+
|
|
54
|
+
## The two documents, and exactly what each field feeds
|
|
55
|
+
|
|
56
|
+
### 1. `https://huggingface.co/api/models/<repo>` — the Hub API
|
|
57
|
+
|
|
58
|
+
The **real, on-disk** shape of the checkpoint — more reliable than anything in
|
|
59
|
+
prose, because it's read from the actual safetensors file headers, not
|
|
60
|
+
declared by the model's author.
|
|
61
|
+
|
|
62
|
+
| JSON field | Feeds | Notes |
|
|
63
|
+
| --- | --- | --- |
|
|
64
|
+
| `safetensors.total` | `compute.gpu.min_vram_gb` (via weight size) | Total parameter count. Absent for GGUF-only repos — size manually if so. |
|
|
65
|
+
| `safetensors.parameters` | `runtime.dtype`, byte width for the VRAM estimate | `{"BF16": N, "F8_E4M3": M}` style breakdown. `inspect-model` picks the dtype with the most parameters — a few embedding/norm tensors often stay bf16 even in an FP8 release. |
|
|
66
|
+
| `gated` | whether you need `model.hf_token_env` | `false` / `"auto"` / `"manual"` — any non-`false` value means a real token is required, not just recommended. |
|
|
67
|
+
| `tags`, `pipeline_tag` | sanity check | Confirms it's actually a text-generation checkpoint before you provision a GPU for it. |
|
|
68
|
+
|
|
69
|
+
### 2. `https://huggingface.co/<repo>/raw/<revision>/config.json` — the model's own HF config
|
|
70
|
+
|
|
71
|
+
The architecture's own declaration of itself — what `transformers`/vLLM read
|
|
72
|
+
to know how to load it.
|
|
73
|
+
|
|
74
|
+
| JSON field | Feeds | Notes |
|
|
75
|
+
| --- | --- | --- |
|
|
76
|
+
| `architectures` | recognizing the model family | e.g. `Qwen3_5ForConditionalGeneration`. Cross-reference against [vLLM's supported models](https://docs.vllm.ai/en/latest/models/supported_models/) — an unlisted architecture will fail to load regardless of VRAM. |
|
|
77
|
+
| `model_type` | hybrid-attention detection | gpu flags known linear-attention families (`qwen3_5`/`qwen3_6`/`qwen3_8`, `jamba`, `mamba`, `bamba`, `deltanet`/`gdn` in the string, ...) — these need an **Ampere-or-newer** GPU for their Triton/FLA kernels. Confirmed the hard way: a Turing card (Quadro RTX 8000) failed to boot vLLM at all for a `qwen3_5` model; Ampere (RTX A6000) worked immediately. |
|
|
78
|
+
| `torch_dtype` | `runtime.dtype`, VRAM estimate (fallback) | Used only when the Hub API's `safetensors.parameters` is absent — the API's real on-disk dtype is preferred when both exist, since a repo can ship a different dtype than it trained in. |
|
|
79
|
+
| `quantization_config.quant_method` | VRAM estimate, `runtime.dtype: auto` | `fp8`/`awq`/`gptq`/`bitsandbytes_4bit` change the byte width per parameter; when present, set `runtime.dtype: auto` and let vLLM read the quantization from the checkpoint instead of forcing a dtype that fights it. |
|
|
80
|
+
| `max_position_embeddings` | `runtime.context_length` | The model's *native* window. `inspect-model` suggests `min(native, 32768)` — the native number is often a YaRN-extended marketing ceiling (e.g. 1,048,576), not a sane default: allocating KV cache for the full window is neither necessary nor cheap for typical agentic/tool-use turns. |
|
|
81
|
+
| `auto_map` | `model.trust_remote_code` | Its *presence* (regardless of value) is the actual signal `transformers` uses to know a repo ships custom modeling code. Absence doesn't guarantee you never need `trust_remote_code: true` — some very new architectures need it even with native library support during a transition period — but presence is a reliable "you definitely do." |
|
|
82
|
+
| `rope_scaling` | context length ceiling sanity-check | Confirms whether a large `max_position_embeddings` is native or extended (YaRN/linear/dynamic) — extended windows cost more KV cache per token than the base architecture would suggest. |
|
|
83
|
+
|
|
84
|
+
### Tool-call parser: a third, unwritten source
|
|
85
|
+
|
|
86
|
+
There is no machine-readable field anywhere that says "use `--tool-call-parser
|
|
87
|
+
X`" — this is `jambu_gpu/core/tool_parsers.py`'s own hand-maintained table,
|
|
88
|
+
matched against vLLM's [tool calling
|
|
89
|
+
docs](https://docs.vllm.ai/en/latest/features/tool_calling/) and, sometimes,
|
|
90
|
+
empirical testing when the docs don't cover a model family yet (the Qwen3.5+
|
|
91
|
+
`qwen3_xml` entry was added after a live test showed `hermes` left the model's
|
|
92
|
+
tool call as inert text in `content` instead of a parsed `tool_calls` array).
|
|
93
|
+
|
|
94
|
+
Consequences:
|
|
95
|
+
|
|
96
|
+
- It only matches on **repo name substrings** (`"qwen3.5"`, `"llama-3"`, ...).
|
|
97
|
+
A fine-tune with a custom name (`empero-ai/Qwythos-9B-v2`, based on Qwen3.5)
|
|
98
|
+
won't be recognized even though the underlying format is known — set
|
|
99
|
+
`runtime.tool_call_parser` explicitly once you know the base model.
|
|
100
|
+
- It can be wrong or incomplete for anything genuinely new. If `inspect-model`
|
|
101
|
+
reports no parser guessed, **or an explicit one you set doesn't actually
|
|
102
|
+
produce a `tool_calls` array**, the fix is the same: pick a candidate
|
|
103
|
+
parser (start from the model's base-architecture family, e.g. Qwen3.8 → try
|
|
104
|
+
what worked for Qwen3.5), restart the server with `--enable-auto-tool-choice
|
|
105
|
+
--tool-call-parser <candidate>`, and send one real tool-calling request —
|
|
106
|
+
a working parser puts the call in `message.tool_calls`, not `message.content`.
|
|
107
|
+
See [`docs/ADDING_A_PROVIDER.md`](ADDING_A_PROVIDER.md) for how the fingerprint
|
|
108
|
+
interacts with runtime-only changes like this if you're doing it through
|
|
109
|
+
`gpu setup` rather than by hand.
|
|
110
|
+
- When you do confirm one empirically, add it to `_TOOL_PARSER_HINTS` in
|
|
111
|
+
`jambu_gpu/core/tool_parsers.py` (a one-line PR) so `tool_calling: auto`
|
|
112
|
+
picks it up for everyone next time, the same way the Qwen3.5+ finding did.
|
|
113
|
+
|
|
114
|
+
## Don't write a new jambu.yaml per model - use the catalog
|
|
115
|
+
|
|
116
|
+
Every model you add this way is tempting to just copy the whole file: same
|
|
117
|
+
provider, same lifecycle, same budget, only `model:`/`compute.gpu:` actually
|
|
118
|
+
differ. Do that a handful of times and a repo ends up with
|
|
119
|
+
`jambu.qwythos.yaml`, `jambu.model-b.yaml`, `jambu.model-c.yaml`... duplicated
|
|
120
|
+
again in every sub-project directory that wants one - a pile of near-identical
|
|
121
|
+
files where changing a lifecycle default means editing all of them.
|
|
122
|
+
|
|
123
|
+
The fix: split "what varies by model" (the catalog) from "what varies by
|
|
124
|
+
project" (the active `jambu.yaml`), and reference the former by name.
|
|
125
|
+
|
|
126
|
+
```yaml
|
|
127
|
+
# jambu.models.yaml - one entry per model, shared by every project in the
|
|
128
|
+
# repo (found by walking up from a project's jambu.yaml, same as jambu.yaml
|
|
129
|
+
# itself is found - one catalog can sit at the repo root and serve every
|
|
130
|
+
# sub-project under it).
|
|
131
|
+
qwythos-9b:
|
|
132
|
+
model: {id: empero-ai/Qwythos-9B-v2, trust_remote_code: true}
|
|
133
|
+
compute: {gpu: {min_vram_gb: 40, name_filter: A6000}}
|
|
134
|
+
runtime: {tool_calling: "on", tool_call_parser: qwen3_xml}
|
|
135
|
+
|
|
136
|
+
qwen3.8-27b-uncensored:
|
|
137
|
+
model: {id: orcarouter/Qwen3.8-27B-Uncensored-FP8}
|
|
138
|
+
compute: {gpu: {min_vram_gb: 44, name_filter: A6000}}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
```yaml
|
|
142
|
+
# jambu.yaml - just this project's policy + which model
|
|
143
|
+
gpu_runtime:
|
|
144
|
+
version: 1
|
|
145
|
+
model_profile: qwythos-9b # the only thing that changes to switch models
|
|
146
|
+
lifecycle: {idle_timeout: 15m, max_lifetime: 6h}
|
|
147
|
+
budget: {max_hourly_cost_usd: 1.50}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
The project's own fields always win over the profile's - a profile is a set
|
|
151
|
+
of defaults, not a lock, so a project can still override just one leaf
|
|
152
|
+
(`compute.gpu.min_vram_gb: 80`, say) without redeclaring the whole model.
|
|
153
|
+
|
|
154
|
+
`gpu inspect-model <repo> --add-profile <name>` writes straight into
|
|
155
|
+
the catalog (creating it if it doesn't exist) instead of printing a snippet
|
|
156
|
+
to paste into a new file - the research and the config land in the same
|
|
157
|
+
command. `gpu profiles` lists what's in it.
|
|
158
|
+
|
|
159
|
+
This is the same base+overlay pattern as docker-compose's `extends`, Helm
|
|
160
|
+
`values.yaml`, or Kustomize overlays - one place owns "what", each consumer
|
|
161
|
+
only says "which one, and what's different here."
|
|
162
|
+
|
|
163
|
+
**When a full standalone file still makes sense:** a genuinely one-off
|
|
164
|
+
experiment nobody else will reuse, or a config for a completely different
|
|
165
|
+
project that will never share this repo's catalog. Most "I need to try model
|
|
166
|
+
X" situations are the catalog case, not this one.
|
|
167
|
+
|
|
168
|
+
## Dynamic / un-modeled vLLM flags
|
|
169
|
+
|
|
170
|
+
`jambu.yaml`'s schema deliberately models the fields that affect *lifecycle,
|
|
171
|
+
cost, and provider portability* — it does not, and cannot, keep up with every
|
|
172
|
+
flag vLLM ships (vLLM adds flags faster than any fixed contract could track).
|
|
173
|
+
For everything else, `runtime:` has two escape hatches, applied in this order
|
|
174
|
+
(later wins on conflict, matching how vLLM's own argparse takes the last
|
|
175
|
+
occurrence of a repeated flag):
|
|
176
|
+
|
|
177
|
+
```yaml
|
|
178
|
+
runtime:
|
|
179
|
+
# Structured: {flag: value} -> --flag value. Handles the common shapes:
|
|
180
|
+
# true -> a bare flag --enable-prefix-caching
|
|
181
|
+
# false -> the negated flag --no-enforce-eager
|
|
182
|
+
# a dict/list -> JSON-encoded (several vLLM flags take JSON)
|
|
183
|
+
# --limit-mm-per-prompt '{"image":0}'
|
|
184
|
+
# anything else -> --flag str(value) --max-num-seqs 64
|
|
185
|
+
vllm_args:
|
|
186
|
+
max_num_seqs: 64
|
|
187
|
+
enable-prefix-caching: true
|
|
188
|
+
|
|
189
|
+
# Raw argv, appended last, verbatim - for anything the structured form
|
|
190
|
+
# can't express (repeated flags, exotic syntax).
|
|
191
|
+
extra_args: ["--kv-cache-dtype", "fp8"]
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Setting a flag gpu already manages through another field (`--model`,
|
|
195
|
+
`--port`, `--enable-auto-tool-choice`, ...) via `vllm_args` still works — it
|
|
196
|
+
wins, since it's applied after those — but `gpu validate` warns about it,
|
|
197
|
+
because the config field that's supposed to control that behavior (e.g.
|
|
198
|
+
`runtime.tool_calling`) will silently stop reflecting what's actually running.
|