jambu-gpu 0.2.0__tar.gz → 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.
- {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/.gitignore +1 -0
- {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/CONTRIBUTING.md +2 -2
- {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/PKG-INFO +2 -2
- {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/README.md +1 -1
- {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/docs/ACCEPTANCE.md +2 -2
- {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/docs/CONFIG.md +1 -1
- {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/docs/INTEGRATION.md +6 -5
- {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/docs/LIFECYCLE.md +1 -1
- {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/docs/USAGE.md +5 -5
- {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/examples/typescript-client/README.md +1 -1
- {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/jambu_gpu/__init__.py +1 -1
- {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/jambu_gpu/cli/_common.py +1 -1
- {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/jambu_gpu/cli/account.py +2 -2
- {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/jambu_gpu/cli/main.py +1 -1
- {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/jambu_gpu/cli/timer.py +6 -4
- {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/jambu_gpu/core/config.py +2 -2
- {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/jambu_gpu/core/credentials.py +4 -2
- {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/jambu_gpu/core/events.py +1 -1
- {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/jambu_gpu/core/hosts.py +1 -1
- jambu_gpu-0.3.0/jambu_gpu/core/paths.py +42 -0
- {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/jambu_gpu/core/state.py +2 -2
- {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/jambu_gpu/core/sweep.py +6 -6
- {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/pyproject.toml +2 -2
- {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/tests/conftest.py +4 -3
- {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/tests/test_config.py +1 -1
- jambu_gpu-0.3.0/tests/test_paths.py +50 -0
- {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/tests/test_state.py +1 -1
- {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/tests/test_sweep.py +5 -5
- {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/.env.example +0 -0
- {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/LICENSE +0 -0
- {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/docs/ADDING_A_PROVIDER.md +0 -0
- {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/docs/CHOOSING_A_MODEL.md +0 -0
- {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/docs/assets/jambu-logo.jpg +0 -0
- {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/examples/experiment.py +0 -0
- {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/examples/jambu.yaml +0 -0
- {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/examples/mastra-agent/README.md +0 -0
- {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/examples/mastra-agent/package-lock.json +0 -0
- {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/examples/mastra-agent/package.json +0 -0
- {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/examples/mastra-agent/src/agent.ts +0 -0
- {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/examples/mastra-agent/src/index.ts +0 -0
- {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/examples/mastra-agent/src/tools/weather-tool.ts +0 -0
- {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/examples/mastra-agent/tsconfig.json +0 -0
- {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/examples/typescript-client/package-lock.json +0 -0
- {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/examples/typescript-client/package.json +0 -0
- {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/examples/typescript-client/src/index.ts +0 -0
- {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/examples/typescript-client/src/jambuGpu.ts +0 -0
- {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/examples/typescript-client/tsconfig.json +0 -0
- {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/jambu.example.yaml +0 -0
- {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/jambu.models.yaml +0 -0
- {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/jambu.yaml +0 -0
- {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/jambu_gpu/__main__.py +0 -0
- {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/jambu_gpu/cli/__init__.py +0 -0
- {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/jambu_gpu/cli/inspect.py +0 -0
- {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/jambu_gpu/cli/lifecycle.py +0 -0
- {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/jambu_gpu/cli/providers.py +0 -0
- {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/jambu_gpu/cli/run.py +0 -0
- {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/jambu_gpu/cli/setup.py +0 -0
- {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/jambu_gpu/cli/status.py +0 -0
- {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/jambu_gpu/cli/validate.py +0 -0
- {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/jambu_gpu/core/__init__.py +0 -0
- {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/jambu_gpu/core/beacon.py +0 -0
- {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/jambu_gpu/core/durations.py +0 -0
- {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/jambu_gpu/core/engine.py +0 -0
- {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/jambu_gpu/core/errors.py +0 -0
- {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/jambu_gpu/core/execution.py +0 -0
- {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/jambu_gpu/core/health.py +0 -0
- {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/jambu_gpu/core/model_inspect.py +0 -0
- {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/jambu_gpu/core/models.py +0 -0
- {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/jambu_gpu/core/policy.py +0 -0
- {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/jambu_gpu/core/tool_parsers.py +0 -0
- {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/jambu_gpu/providers/__init__.py +0 -0
- {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/jambu_gpu/providers/base.py +0 -0
- {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/jambu_gpu/providers/registry.py +0 -0
- {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/jambu_gpu/providers/vast/__init__.py +0 -0
- {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/jambu_gpu/providers/vast/client.py +0 -0
- {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/jambu_gpu/providers/vast/config.py +0 -0
- {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/jambu_gpu/providers/vast/mapper.py +0 -0
- {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/jambu_gpu/providers/vast/provider.py +0 -0
- {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/jambu_gpu/runtime/__init__.py +0 -0
- {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/jambu_gpu/runtime/base.py +0 -0
- {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/jambu_gpu/runtime/vllm.py +0 -0
- {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/jambu_gpu/watchdog/__init__.py +0 -0
- {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/jambu_gpu/watchdog/agent_body.py +0 -0
- {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/jambu_gpu/watchdog/builder.py +0 -0
- {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/tests/fakes.py +0 -0
- {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/tests/test_cli.py +0 -0
- {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/tests/test_engine.py +0 -0
- {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/tests/test_engine_sessions.py +0 -0
- {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/tests/test_integration.py +0 -0
- {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/tests/test_model_inspect.py +0 -0
- {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/tests/test_policy.py +0 -0
- {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/tests/test_runtime_vllm.py +0 -0
- {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/tests/test_vast_mapper.py +0 -0
- {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/tests/test_vast_provider.py +0 -0
- {jambu_gpu-0.2.0 → jambu_gpu-0.3.0}/tests/test_watchdog.py +0 -0
|
@@ -62,9 +62,9 @@ job queues, a web dashboard. See [docs/ACCEPTANCE.md](docs/ACCEPTANCE.md).
|
|
|
62
62
|
|
|
63
63
|
## Secrets
|
|
64
64
|
|
|
65
|
-
Never commit credentials, `.env`, `.
|
|
65
|
+
Never commit credentials, `.env`, `.gpu/state.json`, or provider instance
|
|
66
66
|
ids from a real run. Keys are read from the environment, a gitignored `.env`,
|
|
67
|
-
or `~/.
|
|
67
|
+
or `~/.gpu/credentials`. `.env.example` ships with empty values only.
|
|
68
68
|
|
|
69
69
|
## Pull requests
|
|
70
70
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: jambu-gpu
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Provider-agnostic CLI for provisioning and managing temporary GPU runtimes
|
|
5
5
|
Project-URL: Homepage, https://github.com/jambuai/jambu-gpu
|
|
6
6
|
Project-URL: Repository, https://github.com/jambuai/jambu-gpu
|
|
@@ -49,7 +49,7 @@ From an application in any language: `gpu --json setup` returns the OpenAI-compa
|
|
|
49
49
|
|
|
50
50
|
As a last line of defense, `gpu ls` shows every jambu instance in the account and `gpu sweep --install-timer` sets up a systemd user timer that guards every project on the machine by its own rules and cleans up the orphans no project tracks. See [Keeping the account clean](docs/USAGE.md#keeping-the-account-clean).
|
|
51
51
|
|
|
52
|
-
The API key is read from the environment, a project `.env`, or `~/.
|
|
52
|
+
The API key is read from the environment, a project `.env`, or `~/.gpu/credentials`. It never goes in the manifest.
|
|
53
53
|
|
|
54
54
|
## Declare
|
|
55
55
|
|
|
@@ -26,7 +26,7 @@ From an application in any language: `gpu --json setup` returns the OpenAI-compa
|
|
|
26
26
|
|
|
27
27
|
As a last line of defense, `gpu ls` shows every jambu instance in the account and `gpu sweep --install-timer` sets up a systemd user timer that guards every project on the machine by its own rules and cleans up the orphans no project tracks. See [Keeping the account clean](docs/USAGE.md#keeping-the-account-clean).
|
|
28
28
|
|
|
29
|
-
The API key is read from the environment, a project `.env`, or `~/.
|
|
29
|
+
The API key is read from the environment, a project `.env`, or `~/.gpu/credentials`. It never goes in the manifest.
|
|
30
30
|
|
|
31
31
|
## Declare
|
|
32
32
|
|
|
@@ -4,7 +4,7 @@ Eighteen guarantees, mapped to the code and the test that holds each one.
|
|
|
4
4
|
|
|
5
5
|
| # | Guarantee | Implementation | Test |
|
|
6
6
|
| --- | --- | --- | --- |
|
|
7
|
-
| 1 | Repeated `setup` does not duplicate infrastructure | `core/engine.py::Engine.setup` reconciles first, compares `config.fingerprint()`, and holds an exclusive `.
|
|
7
|
+
| 1 | Repeated `setup` does not duplicate infrastructure | `core/engine.py::Engine.setup` reconciles first, compares `config.fingerprint()`, and holds an exclusive `.gpu/runtime.lock` | `test_engine.py::test_setup_twice_does_not_create_two_instances`, `::test_setup_refuses_to_reuse_an_incompatible_instance` |
|
|
8
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
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
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` |
|
|
@@ -19,7 +19,7 @@ Eighteen guarantees, mapped to the code and the test that holds each one.
|
|
|
19
19
|
| 13 | A restarted instance stays guarded | `Engine._reconcile` takes running/stopped from the provider; the watchdog's `terminated` flag only counts when the provider cannot tell. `RuntimeState.session_started_at` restarts on every start, and `max_lifetime` and the session budget are measured from it | `test_engine_sessions.py::test_reconcile_trusts_the_provider_over_a_stale_terminated_flag`, `::test_a_stale_terminated_flag_does_not_disable_the_guard`, `::test_restarting_a_stopped_instance_starts_a_new_session`, `::test_guard_budget_resets_with_a_new_session`; watchdog-side session tests in `test_policy.py` / `test_watchdog.py` |
|
|
20
20
|
| 14 | Traffic on the model endpoint is activity | `VllmRuntime.activity_probe` hands the watchdog vLLM's `/metrics` gauges and counters; `lifecycle.traffic_counts_as_activity` turns it off | `test_engine_sessions.py::test_the_watchdog_is_told_how_to_read_vllm_traffic`, `::test_traffic_can_be_excluded_from_activity`, `test_runtime_vllm.py::test_activity_probe_reads_vllm_metrics_on_loopback`; the watchdog's metrics-reading tests in `test_watchdog.py` |
|
|
21
21
|
| 15 | A stopped instance does not bill storage forever | `lifecycle.stopped_ttl` -> `PolicyConfig.stopped_ttl_s`; `RuntimeState.stopped_since`; `Engine.guard_tick` destroys with reason `stopped_ttl`; `validate` warns when nothing would remove it | `test_engine_sessions.py::test_guard_destroys_an_instance_stopped_longer_than_stopped_ttl`, `::test_validate_warns_when_nothing_removes_a_billing_stopped_instance`; the stopped-TTL policy tests in `test_policy.py` |
|
|
22
|
-
| 16 | Orphans are found account-wide, each project keeps its own rules, and nothing else is touched | `GPUProvider.list_instances` (capability `list_instances`) returns only `jambu-` labels; `gpu setup` registers the project in `~/.
|
|
22
|
+
| 16 | Orphans are found account-wide, each project keeps its own rules, and nothing else is touched | `GPUProvider.list_instances` (capability `list_instances`) returns only `jambu-` labels; `gpu setup` registers the project in `~/.gpu/projects.json`; `core/sweep.py::guard_projects` runs each registered project's own `Engine.guard_tick`; only untracked instances are orphans: running past `--orphan-max-age` is stopped (never destroyed), stopped past `--stopped-ttl` (from the sweep's own first sighting in `~/.gpu/sweep.json`) is destroyed; `gpu sweep --install-timer` schedules one working-directory-independent unit and refuses a key that is only in the shell | `test_sweep.py::test_vast_lists_only_jambu_labelled_instances`, `::test_non_jambu_instances_are_never_touched`, `::test_another_projects_restarted_and_locked_instance_is_not_destroyed`, `::test_each_project_is_judged_by_its_own_max_lifetime`, `::test_a_running_orphan_past_orphan_max_age_is_stopped_never_destroyed`, `::test_a_stopped_orphan_is_destroyed_once_seen_stopped_past_stopped_ttl`, `::test_install_timer_writes_one_unit_independent_of_the_project`, `::test_install_timer_refuses_a_key_only_in_the_shell_environment` |
|
|
23
23
|
| 17 | The CLI guard never stops a serving endpoint it cannot see | `Engine._idle_blind`: with the watchdog enabled and installed but unreachable, `guard_tick` evaluates without `idle_timeout` and records `idle_skipped`; `max_lifetime`, budget and `stopped_ttl` still apply | `test_engine_sessions.py::test_guard_leaves_idle_to_an_unreachable_watchdog`, `::test_max_lifetime_still_applies_when_the_watchdog_is_unreachable` |
|
|
24
24
|
| 18 | A fresh stop or a short API outage never costs the disk | `RuntimeState.mark_stopped` restarts `stopped_since` on evidence the instance ran since; the watchdog escalates a failing stop to destroy only after `escalate_after_s` (30m) of continuous failure and resets on renewed use | `test_engine_sessions.py::test_a_stale_stopped_since_does_not_destroy_a_fresh_stop`, `test_watchdog.py::test_an_api_blip_does_not_destroy_the_disk`, `::test_failures_reset_when_the_instance_is_back_in_use` |
|
|
25
25
|
|
|
@@ -89,7 +89,7 @@ each number comes from.
|
|
|
89
89
|
| `disk_gb` | `80` | Disk. Must fit the model weights plus the container. |
|
|
90
90
|
| `region` | – | Comma-separated geolocations. |
|
|
91
91
|
| `spot` | `false` | Use interruptible/bid capacity where supported. |
|
|
92
|
-
| `host_attempts` | `3` | How many machines `setup` tries. A host that fails to boot (the provider reports it failed, it vanishes, or it is not running within `health.boot_timeout`) is destroyed, replaced from fresh offers (each held to the budget), and remembered in `$
|
|
92
|
+
| `host_attempts` | `3` | How many machines `setup` tries. A host that fails to boot (the provider reports it failed, it vanishes, or it is not running within `health.boot_timeout`) is destroyed, replaced from fresh offers (each held to the budget), and remembered in `$GPU_HOME/bad_hosts.json` so no setup rents it again for 24h. A model server that dies once its container runs fails at once, since the same config would fail on any machine, unless its log shows the host's network failed it (a dropped connection or timeout while downloading weights): that counts as a host failure too. Needs `lifecycle.cleanup_on_setup_failure`; without it the first failed host is kept for inspection. |
|
|
93
93
|
| `image` | – | Override the runtime's container image. vLLM's default is `vllm/vllm-openai:v0.32.0`. Pin a tag: `validate` warns on `latest` or an untagged image, because the CUDA it needs can change under you. On Vast, `vastai/vllm:v0.32.0-cuda-13.0` (the same vLLM, packaged by Vast) is often cached on the host and boots in minutes where a 12 GB Docker Hub pull can stall. |
|
|
94
94
|
|
|
95
95
|
---
|
|
@@ -15,16 +15,17 @@ The app never has to stop the GPU. The watchdog on the machine stops it once not
|
|
|
15
15
|
pipx install jambu-gpu # or: uv tool install jambu-gpu / pip install jambu-gpu
|
|
16
16
|
cd /srv/my-app # the directory the app runs gpu from
|
|
17
17
|
gpu init --model Qwen/Qwen2.5-7B-Instruct
|
|
18
|
-
echo "VAST_API_KEY=..." >> .env # or export it, or ~/.
|
|
18
|
+
echo "VAST_API_KEY=..." >> .env # or export it, or ~/.gpu/credentials
|
|
19
19
|
gpu validate # fails here, not in production, on a bad config
|
|
20
20
|
gpu sweep --install-timer # optional safety net, see USAGE.md
|
|
21
21
|
```
|
|
22
22
|
|
|
23
23
|
Requirements:
|
|
24
24
|
|
|
25
|
-
- `jambu-gpu` 0.
|
|
26
|
-
- The directory holding `jambu.yaml` must be **persistent**. `gpu` keeps what it rented in `.
|
|
27
|
-
-
|
|
25
|
+
- `jambu-gpu` 0.3.0 or later, on Python 3.10+. The app can be in any language.
|
|
26
|
+
- The directory holding `jambu.yaml` must be **persistent**. `gpu` keeps what it rented in `.gpu/state.json` next to it. In a container, mount it as a volume: a container that loses `.gpu/` forgets its instance, rents a second one, and leaves the first to the watchdog and the sweep.
|
|
27
|
+
- Before 0.3.0 these directories were `.jambu/` and `~/.jambu`. The first command after an upgrade renames them, so an instance already rented is kept, not rented again.
|
|
28
|
+
- `.gpu/state.json` holds the endpoint key and the watchdog token. It is written `0600`. Add `.gpu/` and `.env` to `.gitignore` and keep them out of images.
|
|
28
29
|
|
|
29
30
|
Tune the lifecycle in `jambu.yaml` for the app. For a service with bursty traffic, a longer idle timeout avoids paying a restart on every burst:
|
|
30
31
|
|
|
@@ -218,7 +219,7 @@ Run `gpu --json setup` as a subprocess, parse the JSON, and point the language's
|
|
|
218
219
|
- **The endpoint requires a key.** Unless `runtime.api_key` is set, `setup` generates one per machine (43 random URL-safe characters) and starts vLLM with it. Requests without it get `401`. `/health` and `/metrics` stay open: they reveal no data and the watchdog reads `/metrics`. Instances provisioned before this existed run without a key until they are recreated.
|
|
219
220
|
- **The endpoint is plain HTTP** on a public IP and port. The key and the prompts travel unencrypted. For sensitive data, put the app and the GPU on a private network or a tunnel, or front the endpoint with a TLS proxy.
|
|
220
221
|
- **The watchdog holds the provider key** on the instance so it can stop its own machine. A restricted key is the safer choice; see [LIFECYCLE.md](LIFECYCLE.md).
|
|
221
|
-
- `jambu.yaml` never holds credentials. `.env` and `.
|
|
222
|
+
- `jambu.yaml` never holds credentials. `.env` and `.gpu/` do not belong in version control.
|
|
222
223
|
|
|
223
224
|
## 6. Troubleshooting
|
|
224
225
|
|
|
@@ -105,7 +105,7 @@ gpu
|
|
|
105
105
|
│
|
|
106
106
|
├── Lifecycle core/policy.py the only shutdown rule
|
|
107
107
|
├── Runtime runtime/vllm.py how the model runs
|
|
108
|
-
└── State core/state.py .
|
|
108
|
+
└── State core/state.py .gpu/state.json
|
|
109
109
|
│
|
|
110
110
|
Provider contract providers/base.py
|
|
111
111
|
│
|
|
@@ -111,9 +111,9 @@ Vast.ai instances
|
|
|
111
111
|
|
|
112
112
|
### Every project by its own rules
|
|
113
113
|
|
|
114
|
-
`gpu setup` (and `gpu run`, which can provision) registers the project's absolute config path in `~/.
|
|
114
|
+
`gpu setup` (and `gpu run`, which can provision) registers the project's absolute config path in `~/.gpu/projects.json` (`$GPU_HOME` overrides `~/.gpu`; a `~/.jambu` from before 0.3.0 is renamed to `~/.gpu` on first use, and `$JAMBU_HOME` is still read). `gpu sweep` then works in two steps:
|
|
115
115
|
|
|
116
|
-
1. **Tracked instances.** For every registered project whose config still loads, it runs that project's own `gpu guard`: its own `jambu.yaml` and `.
|
|
116
|
+
1. **Tracked instances.** For every registered project whose config still loads, it runs that project's own `gpu guard`: its own `jambu.yaml` and `.gpu/state.json`, so its own `max_lifetime`, lock, session start, `stopped_ttl` and budget. The project's events land in its own log. A registered project whose config file is gone is dropped from the registry. The project in the current directory, or the one given with `--config`, is guarded too for that run.
|
|
117
117
|
2. **Orphans.** An instance no project tracks has no known policy and nobody knows whether it is in use, so the rules are blunt but never destroy a running machine:
|
|
118
118
|
|
|
119
119
|
| Orphan | Rule | Action |
|
|
@@ -134,7 +134,7 @@ $ gpu sweep
|
|
|
134
134
|
! 1 action(s) pending; re-run with --apply
|
|
135
135
|
```
|
|
136
136
|
|
|
137
|
-
Providers do not report when an instance stopped, so the sweep keeps the time it first saw each orphan stopped in `~/.
|
|
137
|
+
Providers do not report when an instance stopped, so the sweep keeps the time it first saw each orphan stopped in `~/.gpu/sweep.json`. An entry goes away when the instance runs again, disappears, or is tracked by a project. `stopped_ttl` for an orphan therefore counts from the first sweep that saw it stopped, never from a project's state. Every orphan action is recorded as a `sweep_action` event with its reason in `~/.gpu/logs/events.jsonl`. Credentials for the orphan step resolve the usual way (the environment, `.env` in the working directory, `~/.gpu/credentials`), then from the `.env` of registered projects.
|
|
138
138
|
|
|
139
139
|
### Run it on a timer
|
|
140
140
|
|
|
@@ -143,9 +143,9 @@ gpu sweep --install-timer # every 5m; --every 15m to change
|
|
|
143
143
|
systemctl --user daemon-reload && systemctl --user enable --now jambu-gpu-sweep.timer
|
|
144
144
|
```
|
|
145
145
|
|
|
146
|
-
`--install-timer` writes `~/.config/systemd/user/jambu-gpu-sweep.service` and `.timer`, and registers the current project if there is one. The service is a single `ExecStart=<python> -m jambu_gpu sweep --apply` with the interpreter that installed it, run from `~`: which projects get guarded comes from the registry, not from the working directory, so installing from a second project changes nothing in the unit. The oneshot has a 10 minute start timeout, a failing sweep shows as a failed unit, and `
|
|
146
|
+
`--install-timer` writes `~/.config/systemd/user/jambu-gpu-sweep.service` and `.timer`, and registers the current project if there is one. The service is a single `ExecStart=<python> -m jambu_gpu sweep --apply` with the interpreter that installed it, run from `~`: which projects get guarded comes from the registry, not from the working directory, so installing from a second project changes nothing in the unit. The oneshot has a 10 minute start timeout, a failing sweep shows as a failed unit, and `GPU_HOME` is passed through when it is set. A unit written by an older version, with one `guard` line per project, is rewritten and those projects are registered.
|
|
147
147
|
|
|
148
|
-
A user unit does not inherit your shell's environment. `--install-timer` therefore refuses unless the provider key is in a file: the project `.env` or `~/.
|
|
148
|
+
A user unit does not inherit your shell's environment. `--install-timer` therefore refuses unless the provider key is in a file: the project `.env` or `~/.gpu/credentials`. A key that is only exported in your shell is not enough. A user timer needs no root, but it stops at logout unless lingering is on (`loginctl enable-linger $USER`).
|
|
149
149
|
|
|
150
150
|
```bash
|
|
151
151
|
gpu sweep --uninstall-timer
|
|
@@ -6,7 +6,7 @@ Any Node/TypeScript application using a GPU that `gpu` rents and guards, through
|
|
|
6
6
|
|
|
7
7
|
## Run it
|
|
8
8
|
|
|
9
|
-
From the directory that holds `jambu.yaml` and `VAST_API_KEY` (in `.env` or the environment), with `jambu-gpu` 0.
|
|
9
|
+
From the directory that holds `jambu.yaml` and `VAST_API_KEY` (in `.env` or the environment), with `jambu-gpu` 0.3.0 or later on `PATH`:
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
12
|
npm --prefix examples/typescript-client install
|
|
@@ -118,7 +118,7 @@ def fmt_money(value: Optional[float], suffix: str = "") -> Optional[str]:
|
|
|
118
118
|
|
|
119
119
|
|
|
120
120
|
def register_project(context: Context) -> None:
|
|
121
|
-
"""Put this project in $
|
|
121
|
+
"""Put this project in $GPU_HOME/projects.json so `gpu sweep` guards it.
|
|
122
122
|
|
|
123
123
|
Done before provisioning: a setup that dies halfway must still leave an
|
|
124
124
|
instance the sweep judges by this project's policy, not as an orphan.
|
|
@@ -57,7 +57,7 @@ class Account:
|
|
|
57
57
|
|
|
58
58
|
|
|
59
59
|
def account_credentials(project_dir: Path, profile: str = "default") -> CredentialResolver:
|
|
60
|
-
"""The environment, ./.env, ~/.
|
|
60
|
+
"""The environment, ./.env, ~/.gpu/credentials, then registered projects' .env.
|
|
61
61
|
|
|
62
62
|
The timer runs from ~ without the shell's environment; a key kept only in
|
|
63
63
|
a project's .env must still reach the account-wide orphan sweep.
|
|
@@ -196,7 +196,7 @@ def _require_file_credentials(context: Context) -> None:
|
|
|
196
196
|
raise ConfigError(
|
|
197
197
|
f"{name} is {where}, so the timer could not use it for {who}: a systemd "
|
|
198
198
|
f"user unit does not inherit your shell. Put `{name}=...` in "
|
|
199
|
-
"~/.
|
|
199
|
+
"~/.gpu/credentials (chmod 600), then re-run --install-timer."
|
|
200
200
|
)
|
|
201
201
|
|
|
202
202
|
|
|
@@ -19,7 +19,7 @@ app = typer.Typer(
|
|
|
19
19
|
name="gpu",
|
|
20
20
|
help=(
|
|
21
21
|
"Provider-agnostic CLI for provisioning and managing temporary GPU runtimes.\n\n"
|
|
22
|
-
"jambu.yaml is the desired-state source of truth; .
|
|
22
|
+
"jambu.yaml is the desired-state source of truth; .gpu/state.json is what "
|
|
23
23
|
"was last observed."
|
|
24
24
|
),
|
|
25
25
|
no_args_is_help=True,
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
"""systemd *user* units that run `gpu sweep --apply` on a timer.
|
|
2
2
|
|
|
3
3
|
One ``ExecStart`` covers every project: the sweep guards each project in
|
|
4
|
-
``$
|
|
4
|
+
``$GPU_HOME/projects.json`` with its own config, so the unit does not depend
|
|
5
5
|
on its working directory. A user unit needs no root, but it does not inherit
|
|
6
|
-
the shell's environment: credentials must live in ``~/.
|
|
6
|
+
the shell's environment: credentials must live in ``~/.gpu/credentials`` or
|
|
7
7
|
the project ``.env``. Writing the files is all this module does; enabling them
|
|
8
8
|
is left to the printed ``systemctl`` command.
|
|
9
9
|
"""
|
|
@@ -64,7 +64,7 @@ def render_units(
|
|
|
64
64
|
f"TimeoutStartSec={START_TIMEOUT}",
|
|
65
65
|
]
|
|
66
66
|
if jambu_home:
|
|
67
|
-
lines.append(_environment("
|
|
67
|
+
lines.append(_environment("GPU_HOME", jambu_home))
|
|
68
68
|
# No "-" prefix: a failing sweep must show up as a failed unit.
|
|
69
69
|
lines.append("ExecStart=" + " ".join(_quote(a) for a in [python, "-m", "jambu_gpu", "sweep", "--apply"]))
|
|
70
70
|
lines.append("")
|
|
@@ -115,7 +115,9 @@ def install_timer(every_s: float, python: Optional[str] = None) -> dict:
|
|
|
115
115
|
service_path, timer_path = unit_paths()
|
|
116
116
|
legacy = legacy_guard_configs(service_path)
|
|
117
117
|
service, timer = render_units(
|
|
118
|
-
every_s=every_s,
|
|
118
|
+
every_s=every_s,
|
|
119
|
+
python=python,
|
|
120
|
+
jambu_home=os.environ.get("GPU_HOME") or os.environ.get("JAMBU_HOME") or None,
|
|
119
121
|
)
|
|
120
122
|
service_path.parent.mkdir(parents=True, exist_ok=True)
|
|
121
123
|
service_path.write_text(service)
|
|
@@ -33,12 +33,12 @@ from pydantic import BaseModel, ConfigDict, Field, field_validator, model_valida
|
|
|
33
33
|
|
|
34
34
|
from .durations import parse_duration
|
|
35
35
|
from .errors import ConfigError
|
|
36
|
+
from .paths import project_state_dir
|
|
36
37
|
|
|
37
38
|
# jambu.yaml (namespaced, shared with other Jambu Lab tools) is searched
|
|
38
39
|
# first; config.yml (flat, gpu-runtime-only) is the legacy fallback.
|
|
39
40
|
CONFIG_FILENAMES = ("jambu.yaml", "jambu.yml", "config.yml", "config.yaml")
|
|
40
41
|
GPU_RUNTIME_KEY = "gpu_runtime"
|
|
41
|
-
STATE_DIRNAME = ".jambu"
|
|
42
42
|
|
|
43
43
|
# The model catalog (spec: "Choosing a model" doc) - a project's jambu.yaml
|
|
44
44
|
# references one entry by name instead of redeclaring the whole model/compute/
|
|
@@ -319,7 +319,7 @@ class RuntimeConfig(_Base):
|
|
|
319
319
|
|
|
320
320
|
@property
|
|
321
321
|
def state_dir(self) -> Path:
|
|
322
|
-
return self.project_dir
|
|
322
|
+
return project_state_dir(self.project_dir)
|
|
323
323
|
|
|
324
324
|
def fingerprint(self) -> str:
|
|
325
325
|
"""Identity of the *desired* runtime.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""Credential resolution (spec section 5).
|
|
2
2
|
|
|
3
3
|
Credentials never live in jambu.yaml. They are resolved from, in order:
|
|
4
|
-
environment variables, a project-local ``.env``, then ``~/.
|
|
4
|
+
environment variables, a project-local ``.env``, then ``~/.gpu/credentials``.
|
|
5
5
|
The resolution strategy can grow (keychain, secret managers, CI secrets)
|
|
6
6
|
without touching jambu.yaml or any adapter.
|
|
7
7
|
"""
|
|
@@ -12,6 +12,8 @@ import os
|
|
|
12
12
|
from pathlib import Path
|
|
13
13
|
from typing import Optional
|
|
14
14
|
|
|
15
|
+
from .paths import gpu_home
|
|
16
|
+
|
|
15
17
|
_DOTENV_CACHE: dict[Path, dict[str, str]] = {}
|
|
16
18
|
|
|
17
19
|
|
|
@@ -46,7 +48,7 @@ class CredentialResolver:
|
|
|
46
48
|
self.profile = profile
|
|
47
49
|
self.sources: list[tuple[str, Path]] = [
|
|
48
50
|
("project .env", self.project_dir / ".env"),
|
|
49
|
-
("user credentials",
|
|
51
|
+
("user credentials", gpu_home() / "credentials"),
|
|
50
52
|
*(extra_sources or []),
|
|
51
53
|
]
|
|
52
54
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Structured lifecycle events (spec section 24).
|
|
2
2
|
|
|
3
|
-
Every lifecycle action appends one JSON object to ``.
|
|
3
|
+
Every lifecycle action appends one JSON object to ``.gpu/logs/events.jsonl``
|
|
4
4
|
so unexpected GPU cost can always be traced back to a decision and a reason.
|
|
5
5
|
"""
|
|
6
6
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
A host that could not pull the image or reach the network fails the next
|
|
4
4
|
setup the same way, and the cheapest-offer search keeps picking it. Each
|
|
5
|
-
failure is kept for ``BAD_HOST_TTL_S`` in ``$
|
|
5
|
+
failure is kept for ``BAD_HOST_TTL_S`` in ``$GPU_HOME/bad_hosts.json``.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
from __future__ import annotations
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Where gpu keeps its files: ``.gpu/`` in a project, ``~/.gpu`` per user.
|
|
2
|
+
|
|
3
|
+
Both were named ``.jambu`` before 0.3.0. A legacy directory is renamed in
|
|
4
|
+
place the first time it is needed, so an instance a project already tracks
|
|
5
|
+
is never forgotten (and never rented twice) after an upgrade.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
STATE_DIRNAME = ".gpu"
|
|
14
|
+
LEGACY_STATE_DIRNAME = ".jambu"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def adopt_legacy(path: Path, legacy: Path) -> Path:
|
|
18
|
+
"""``path``, after moving ``legacy`` there when only the legacy one exists."""
|
|
19
|
+
if not path.exists() and legacy.is_dir():
|
|
20
|
+
try:
|
|
21
|
+
legacy.rename(path)
|
|
22
|
+
except OSError:
|
|
23
|
+
# Read-only or racing another gpu process: keep using the old one.
|
|
24
|
+
return path if path.exists() else legacy
|
|
25
|
+
return path
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def project_state_dir(project_dir: Path) -> Path:
|
|
29
|
+
project_dir = Path(project_dir)
|
|
30
|
+
return adopt_legacy(project_dir / STATE_DIRNAME, project_dir / LEGACY_STATE_DIRNAME)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def gpu_home() -> Path:
|
|
34
|
+
"""Per-user files: the project registry, sweep sightings, bad hosts, credentials.
|
|
35
|
+
|
|
36
|
+
``$GPU_HOME`` overrides it (``$JAMBU_HOME`` is still honored).
|
|
37
|
+
"""
|
|
38
|
+
override = os.environ.get("GPU_HOME") or os.environ.get("JAMBU_HOME")
|
|
39
|
+
if override:
|
|
40
|
+
return Path(override).expanduser()
|
|
41
|
+
home = Path.home()
|
|
42
|
+
return adopt_legacy(home / STATE_DIRNAME, home / LEGACY_STATE_DIRNAME)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Observed runtime state (spec section 9).
|
|
2
2
|
|
|
3
|
-
jambu.yaml is the desired-state SoT; ``.
|
|
3
|
+
jambu.yaml is the desired-state SoT; ``.gpu/state.json`` is what we last
|
|
4
4
|
observed. This module owns reading/writing it atomically, plus the on-disk
|
|
5
5
|
process lock that keeps two CLI invocations from provisioning twice.
|
|
6
6
|
"""
|
|
@@ -312,7 +312,7 @@ class RuntimeState:
|
|
|
312
312
|
|
|
313
313
|
|
|
314
314
|
class StateStore:
|
|
315
|
-
"""Atomic reader/writer for ``.
|
|
315
|
+
"""Atomic reader/writer for ``.gpu/state.json``."""
|
|
316
316
|
|
|
317
317
|
def __init__(self, state_dir: Path) -> None:
|
|
318
318
|
self._depth = 0 # flock is per-open-file-description: keep it reentrant
|
|
@@ -4,7 +4,7 @@ The watchdog guards one instance from the inside and `gpu guard` guards the
|
|
|
4
4
|
one instance a project tracks. The sweep covers every project on this machine
|
|
5
5
|
and whatever is left over:
|
|
6
6
|
|
|
7
|
-
- every project registered in ``$
|
|
7
|
+
- every project registered in ``$GPU_HOME/projects.json`` (by `gpu setup`)
|
|
8
8
|
gets its own ``Engine.guard_tick``, with its own config and state: its own
|
|
9
9
|
max_lifetime, lock, session and stopped_ttl. Those instances are *tracked*;
|
|
10
10
|
- an instance no project tracks is an *orphan*. Nobody knows its policy or
|
|
@@ -14,7 +14,7 @@ and whatever is left over:
|
|
|
14
14
|
ago is destroyed.
|
|
15
15
|
|
|
16
16
|
Providers do not report when an instance stopped, so the first time the sweep
|
|
17
|
-
saw each orphan stopped is kept in ``$
|
|
17
|
+
saw each orphan stopped is kept in ``$GPU_HOME/sweep.json``.
|
|
18
18
|
"""
|
|
19
19
|
|
|
20
20
|
from __future__ import annotations
|
|
@@ -30,7 +30,8 @@ from typing import Any, Callable, Iterator, Optional
|
|
|
30
30
|
|
|
31
31
|
from . import events as ev
|
|
32
32
|
from . import policy
|
|
33
|
-
from .config import
|
|
33
|
+
from .config import load_config
|
|
34
|
+
from .paths import gpu_home, project_state_dir
|
|
34
35
|
from .errors import JambuError, ProviderError
|
|
35
36
|
from .events import EventLog
|
|
36
37
|
from .models import Instance, InstanceState, is_jambu_label
|
|
@@ -46,8 +47,7 @@ SWEEP_ACTION = "sweep_action"
|
|
|
46
47
|
|
|
47
48
|
|
|
48
49
|
def jambu_home() -> Path:
|
|
49
|
-
|
|
50
|
-
return Path(override).expanduser() if override else Path.home() / ".jambu"
|
|
50
|
+
return gpu_home()
|
|
51
51
|
|
|
52
52
|
|
|
53
53
|
class SweepRegistry:
|
|
@@ -198,7 +198,7 @@ class ProjectGuard:
|
|
|
198
198
|
|
|
199
199
|
def _state_beside(config_path: Path) -> Optional[RuntimeState]:
|
|
200
200
|
"""The project's state read straight from disk (no API call)."""
|
|
201
|
-
store = StateStore(Path(config_path).parent
|
|
201
|
+
store = StateStore(project_state_dir(Path(config_path).parent))
|
|
202
202
|
try:
|
|
203
203
|
return store.load() if store.exists() else None
|
|
204
204
|
except JambuError:
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "jambu-gpu"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.0"
|
|
8
8
|
description = "Provider-agnostic CLI for provisioning and managing temporary GPU runtimes"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -36,7 +36,7 @@ gpu = "jambu_gpu.cli.main:main"
|
|
|
36
36
|
packages = ["jambu_gpu"]
|
|
37
37
|
|
|
38
38
|
[tool.hatch.build.targets.sdist]
|
|
39
|
-
exclude = ["/.env", "/.venv", "/.jambu"]
|
|
39
|
+
exclude = ["/.env", "/.venv", "/.gpu", "/.jambu"]
|
|
40
40
|
|
|
41
41
|
[tool.pytest.ini_options]
|
|
42
42
|
testpaths = ["tests"]
|
|
@@ -29,9 +29,10 @@ BASE_CONFIG = {
|
|
|
29
29
|
|
|
30
30
|
@pytest.fixture(autouse=True)
|
|
31
31
|
def jambu_home(tmp_path, monkeypatch):
|
|
32
|
-
"""Keep the project registry and sweep state out of the real ~/.
|
|
33
|
-
home = tmp_path / "
|
|
34
|
-
monkeypatch.setenv("
|
|
32
|
+
"""Keep the project registry and sweep state out of the real ~/.gpu."""
|
|
33
|
+
home = tmp_path / "gpu-home"
|
|
34
|
+
monkeypatch.setenv("GPU_HOME", str(home))
|
|
35
|
+
monkeypatch.delenv("JAMBU_HOME", raising=False)
|
|
35
36
|
return home
|
|
36
37
|
|
|
37
38
|
|
|
@@ -100,7 +100,7 @@ def test_fingerprint_tracks_runtime_identity(write_config):
|
|
|
100
100
|
|
|
101
101
|
|
|
102
102
|
def test_state_dir_sits_next_to_the_config(config):
|
|
103
|
-
assert config.state_dir == config.source_path.parent / ".
|
|
103
|
+
assert config.state_dir == config.source_path.parent / ".gpu"
|
|
104
104
|
|
|
105
105
|
|
|
106
106
|
# -- jambu.yaml shared-manifest namespacing ----------------------------------
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Files live in .gpu/ and ~/.gpu; a legacy .jambu is adopted, never forgotten."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from jambu_gpu.core.paths import gpu_home, project_state_dir
|
|
6
|
+
from jambu_gpu.core.state import RuntimeState, StateStore
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def test_a_new_project_keeps_its_state_in_dot_gpu(tmp_path):
|
|
10
|
+
assert project_state_dir(tmp_path) == tmp_path / ".gpu"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_a_project_from_before_0_3_keeps_its_instance(tmp_path):
|
|
14
|
+
StateStore(tmp_path / ".jambu").save(RuntimeState(provider="vast", instance_id="42", state="ready"))
|
|
15
|
+
|
|
16
|
+
state_dir = project_state_dir(tmp_path)
|
|
17
|
+
|
|
18
|
+
assert state_dir == tmp_path / ".gpu"
|
|
19
|
+
assert not (tmp_path / ".jambu").exists()
|
|
20
|
+
assert StateStore(state_dir).load().instance_id == "42"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_both_present_means_dot_gpu_wins_and_the_legacy_is_left_alone(tmp_path):
|
|
24
|
+
(tmp_path / ".gpu").mkdir()
|
|
25
|
+
(tmp_path / ".jambu").mkdir()
|
|
26
|
+
assert project_state_dir(tmp_path) == tmp_path / ".gpu"
|
|
27
|
+
assert (tmp_path / ".jambu").is_dir()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_the_user_home_moves_from_dot_jambu(tmp_path, monkeypatch):
|
|
31
|
+
monkeypatch.delenv("GPU_HOME", raising=False)
|
|
32
|
+
monkeypatch.delenv("JAMBU_HOME", raising=False)
|
|
33
|
+
monkeypatch.setenv("HOME", str(tmp_path))
|
|
34
|
+
(tmp_path / ".jambu").mkdir()
|
|
35
|
+
(tmp_path / ".jambu" / "projects.json").write_text("[]")
|
|
36
|
+
|
|
37
|
+
assert gpu_home() == tmp_path / ".gpu"
|
|
38
|
+
assert (tmp_path / ".gpu" / "projects.json").read_text() == "[]"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_gpu_home_and_the_legacy_jambu_home_override_it(tmp_path, monkeypatch):
|
|
42
|
+
monkeypatch.delenv("GPU_HOME", raising=False)
|
|
43
|
+
monkeypatch.setenv("JAMBU_HOME", str(tmp_path / "old"))
|
|
44
|
+
assert gpu_home() == tmp_path / "old"
|
|
45
|
+
monkeypatch.setenv("GPU_HOME", str(tmp_path / "new"))
|
|
46
|
+
assert gpu_home() == tmp_path / "new"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_a_config_resolves_its_state_dir_to_dot_gpu(config):
|
|
50
|
+
assert config.state_dir == config.project_dir / ".gpu"
|
|
@@ -11,7 +11,7 @@ from jambu_gpu.core.state import RuntimeState, StateStore, WorkloadRecord, utc_n
|
|
|
11
11
|
|
|
12
12
|
@pytest.fixture
|
|
13
13
|
def store(tmp_path) -> StateStore:
|
|
14
|
-
return StateStore(tmp_path / ".
|
|
14
|
+
return StateStore(tmp_path / ".gpu")
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
def test_empty_state_is_not_an_error(store):
|
|
@@ -549,8 +549,8 @@ def home(tmp_path, monkeypatch):
|
|
|
549
549
|
|
|
550
550
|
|
|
551
551
|
@pytest.fixture
|
|
552
|
-
def credentials_file(home):
|
|
553
|
-
path =
|
|
552
|
+
def credentials_file(home, jambu_home):
|
|
553
|
+
path = jambu_home / "credentials"
|
|
554
554
|
path.parent.mkdir(parents=True)
|
|
555
555
|
path.write_text("VAST_API_KEY=from-file\n")
|
|
556
556
|
return path
|
|
@@ -570,7 +570,7 @@ def test_install_timer_writes_one_unit_independent_of_the_project(
|
|
|
570
570
|
assert execs == [f"ExecStart={sys.executable} -m jambu_gpu sweep --apply"] # no "-"
|
|
571
571
|
assert "WorkingDirectory=~" in service
|
|
572
572
|
assert "TimeoutStartSec=10min" in service
|
|
573
|
-
assert f'Environment="
|
|
573
|
+
assert f'Environment="GPU_HOME={jambu_home}"' in service
|
|
574
574
|
assert "Type=oneshot" in service
|
|
575
575
|
assert "OnUnitActiveSec=600s" in timer
|
|
576
576
|
assert "WantedBy=timers.target" in timer
|
|
@@ -623,7 +623,7 @@ def test_install_timer_refuses_a_key_only_in_the_shell_environment(config, home,
|
|
|
623
623
|
result = runner.invoke(app, ["sweep", "--install-timer"])
|
|
624
624
|
assert result.exit_code == 2
|
|
625
625
|
message = " ".join(result.output.split())
|
|
626
|
-
assert "~/.
|
|
626
|
+
assert "~/.gpu/credentials" in message and "VAST_API_KEY" in message
|
|
627
627
|
assert not (home / ".config/systemd/user/jambu-gpu-sweep.service").exists()
|
|
628
628
|
|
|
629
629
|
|
|
@@ -641,7 +641,7 @@ def test_unit_values_escape_percent_and_dollar_and_quote_spaces():
|
|
|
641
641
|
every_s=300, python="/opt/my env/100%/py$thon", jambu_home="/data/50% \"x\""
|
|
642
642
|
)
|
|
643
643
|
assert 'ExecStart="/opt/my env/100%%/py$$thon" -m jambu_gpu sweep --apply' in service
|
|
644
|
-
assert 'Environment="
|
|
644
|
+
assert 'Environment="GPU_HOME=/data/50%% \\"x\\""' in service
|
|
645
645
|
assert "Environment" not in timer_mod.render_units(every_s=300, python="/p")[0]
|
|
646
646
|
|
|
647
647
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|