jambu-gpu 0.1.0__py3-none-any.whl

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 (46) hide show
  1. jambu_gpu/__init__.py +5 -0
  2. jambu_gpu/__main__.py +4 -0
  3. jambu_gpu/cli/__init__.py +0 -0
  4. jambu_gpu/cli/_common.py +117 -0
  5. jambu_gpu/cli/inspect.py +207 -0
  6. jambu_gpu/cli/lifecycle.py +127 -0
  7. jambu_gpu/cli/main.py +103 -0
  8. jambu_gpu/cli/providers.py +96 -0
  9. jambu_gpu/cli/run.py +37 -0
  10. jambu_gpu/cli/setup.py +165 -0
  11. jambu_gpu/cli/status.py +214 -0
  12. jambu_gpu/cli/validate.py +57 -0
  13. jambu_gpu/core/__init__.py +0 -0
  14. jambu_gpu/core/beacon.py +124 -0
  15. jambu_gpu/core/config.py +596 -0
  16. jambu_gpu/core/credentials.py +75 -0
  17. jambu_gpu/core/durations.py +47 -0
  18. jambu_gpu/core/engine.py +951 -0
  19. jambu_gpu/core/errors.py +68 -0
  20. jambu_gpu/core/events.py +115 -0
  21. jambu_gpu/core/execution.py +169 -0
  22. jambu_gpu/core/health.py +62 -0
  23. jambu_gpu/core/model_inspect.py +368 -0
  24. jambu_gpu/core/models.py +283 -0
  25. jambu_gpu/core/policy.py +233 -0
  26. jambu_gpu/core/state.py +361 -0
  27. jambu_gpu/core/tool_parsers.py +63 -0
  28. jambu_gpu/providers/__init__.py +4 -0
  29. jambu_gpu/providers/base.py +125 -0
  30. jambu_gpu/providers/registry.py +63 -0
  31. jambu_gpu/providers/vast/__init__.py +3 -0
  32. jambu_gpu/providers/vast/client.py +230 -0
  33. jambu_gpu/providers/vast/config.py +51 -0
  34. jambu_gpu/providers/vast/mapper.py +221 -0
  35. jambu_gpu/providers/vast/provider.py +422 -0
  36. jambu_gpu/runtime/__init__.py +6 -0
  37. jambu_gpu/runtime/base.py +99 -0
  38. jambu_gpu/runtime/vllm.py +224 -0
  39. jambu_gpu/watchdog/__init__.py +3 -0
  40. jambu_gpu/watchdog/agent_body.py +547 -0
  41. jambu_gpu/watchdog/builder.py +178 -0
  42. jambu_gpu-0.1.0.dist-info/METADATA +82 -0
  43. jambu_gpu-0.1.0.dist-info/RECORD +46 -0
  44. jambu_gpu-0.1.0.dist-info/WHEEL +4 -0
  45. jambu_gpu-0.1.0.dist-info/entry_points.txt +2 -0
  46. jambu_gpu-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,178 @@
1
+ """Assembles the remote watchdog into a single self-contained script.
2
+
3
+ The lifecycle *policy* is owned by the core and inlined verbatim from
4
+ ``core/policy.py`` so the remote guard and `gpu status` can never
5
+ disagree. The *provider actions* are injected by the adapter. Neither side
6
+ knows about the other.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import base64
12
+ import json
13
+ import zlib
14
+ import secrets
15
+ from pathlib import Path
16
+ from typing import Optional
17
+
18
+ from ..core import policy as policy_module
19
+ from ..providers.base import WatchdogSpec
20
+
21
+ AGENT_REMOTE_PATH = "/opt/jambu/watchdog.py"
22
+ CONFIG_REMOTE_PATH = "/etc/jambu/watchdog.json"
23
+ STATE_REMOTE_PATH = "/var/lib/jambu/activity.json"
24
+ EVENTS_REMOTE_PATH = "/var/log/jambu/events.jsonl"
25
+ WATCHDOG_LOG_PATH = "/var/log/jambu/watchdog.log"
26
+
27
+ _HEADER = """#!/usr/bin/env python3
28
+ # Generated by jambu-gpu. Do not edit on the instance.
29
+ # Lifecycle policy is inlined from jambu_gpu/core/policy.py.
30
+ from __future__ import annotations
31
+ """
32
+
33
+ _FUTURE_IMPORT = "from __future__ import annotations"
34
+
35
+ _FALLBACK_ACTIONS = '''
36
+ def provider_stop(ctx):
37
+ raise RuntimeError("this provider supplied no stop action")
38
+
39
+
40
+ def provider_destroy(ctx):
41
+ raise RuntimeError("this provider supplied no destroy action")
42
+ '''
43
+
44
+
45
+ def generate_token() -> str:
46
+ """Shared secret protecting the watchdog's public control port."""
47
+ return secrets.token_urlsafe(32)
48
+
49
+
50
+ def _strip_future(source: str) -> str:
51
+ return "\n".join(
52
+ line for line in source.splitlines() if line.strip() != _FUTURE_IMPORT
53
+ )
54
+
55
+
56
+ def build_agent_source(actions_source: Optional[str] = None) -> str:
57
+ """Return the full stdlib-only watchdog script."""
58
+ policy_source = _strip_future(Path(policy_module.__file__).read_text())
59
+ body_source = _strip_future((Path(__file__).parent / "agent_body.py").read_text())
60
+ actions = _strip_future(actions_source) if actions_source else _FALLBACK_ACTIONS
61
+ return "\n\n".join(
62
+ [
63
+ _HEADER,
64
+ "# --- inlined: jambu_gpu/core/policy.py ---",
65
+ policy_source,
66
+ "# --- injected: provider actions ---",
67
+ actions,
68
+ "# --- inlined: jambu_gpu/watchdog/agent_body.py ---",
69
+ body_source,
70
+ ]
71
+ )
72
+
73
+
74
+ def build_agent_config(
75
+ *,
76
+ provider: str,
77
+ instance_id: str,
78
+ token: str,
79
+ port: int,
80
+ interval_s: float,
81
+ startup_grace_s: float,
82
+ policy: "policy_module.PolicyConfig",
83
+ created_at: float,
84
+ watchdog_spec: Optional[WatchdogSpec] = None,
85
+ ) -> dict:
86
+ return {
87
+ "provider": provider,
88
+ "instance_id": instance_id,
89
+ "token": token,
90
+ "port": int(port),
91
+ "interval_s": float(interval_s),
92
+ "startup_grace_s": float(startup_grace_s),
93
+ "created_at": float(created_at),
94
+ "policy": policy.to_dict(),
95
+ "env": dict(watchdog_spec.env) if watchdog_spec else {},
96
+ "provider_context": dict(watchdog_spec.context) if watchdog_spec else {},
97
+ }
98
+
99
+
100
+ def _b64(text: str) -> str:
101
+ return base64.b64encode(text.encode()).decode()
102
+
103
+
104
+ def _b64_gz_block(text: str, width: int = 76) -> str:
105
+ """Compressed, line-wrapped payload.
106
+
107
+ The boot script travels inside a provider API field, so the ~29 KB agent is
108
+ deflated to ~9 KB and wrapped, instead of riding on one enormous line.
109
+ """
110
+ blob = base64.b64encode(zlib.compress(text.encode(), 9)).decode()
111
+ return "\n".join(blob[i : i + width] for i in range(0, len(blob), width))
112
+
113
+
114
+ def build_onstart_script(
115
+ *,
116
+ agent_source: str,
117
+ agent_config: dict,
118
+ runtime_start_command: str,
119
+ runtime_env: Optional[dict] = None,
120
+ watchdog_enabled: bool = True,
121
+ ) -> str:
122
+ """Bash executed on the instance right after the container comes up.
123
+
124
+ Order matters: the watchdog starts FIRST so an instance whose model server
125
+ fails to boot is still protected by idle/max_lifetime rules.
126
+ """
127
+ lines = [
128
+ "#!/bin/bash",
129
+ "# Generated by jambu-gpu",
130
+ "set -u",
131
+ # mkdir first: a failed `exec` redirect would kill a non-interactive shell.
132
+ "mkdir -p /etc/jambu /var/lib/jambu /var/log/jambu /var/run/jambu /opt/jambu",
133
+ "exec >> /var/log/jambu/onstart.log 2>&1",
134
+ "date -u +'[%Y-%m-%dT%H:%M:%SZ] jambu onstart begin'",
135
+ ]
136
+
137
+ for key, value in (runtime_env or {}).items():
138
+ lines.append(f"export {key}={_shell_quote(str(value))}")
139
+ lines.append(f"echo {_shell_quote(f'{key}={value}')} >> /etc/jambu/runtime.env")
140
+
141
+ if watchdog_enabled:
142
+ decode = (
143
+ "import base64,zlib;"
144
+ f"open({AGENT_REMOTE_PATH!r},'wb').write("
145
+ f"zlib.decompress(base64.b64decode(open('{AGENT_REMOTE_PATH}.b64','rb').read())))"
146
+ )
147
+ lines += [
148
+ "",
149
+ "# --- lifecycle watchdog (survives local CLI death) ---",
150
+ f"echo {_b64(json.dumps(agent_config))} | base64 -d > {CONFIG_REMOTE_PATH}",
151
+ f"chmod 600 {CONFIG_REMOTE_PATH}",
152
+ "PYBIN=$(command -v python3 || command -v python)",
153
+ 'if [ -z "$PYBIN" ]; then',
154
+ ' echo "jambu: no python interpreter in this image; watchdog NOT installed"',
155
+ "else",
156
+ f" cat > {AGENT_REMOTE_PATH}.b64 <<'JAMBU_AGENT_EOF'",
157
+ _b64_gz_block(agent_source),
158
+ "JAMBU_AGENT_EOF",
159
+ f' "$PYBIN" -c {_shell_quote(decode)}',
160
+ f" rm -f {AGENT_REMOTE_PATH}.b64",
161
+ f" chmod 700 {AGENT_REMOTE_PATH}",
162
+ f' nohup "$PYBIN" -u {AGENT_REMOTE_PATH} >> {WATCHDOG_LOG_PATH} 2>&1 &',
163
+ " echo $! > /var/run/jambu/watchdog.pid",
164
+ ' echo "jambu: watchdog started pid $(cat /var/run/jambu/watchdog.pid)"',
165
+ "fi",
166
+ ]
167
+
168
+ lines += [
169
+ "",
170
+ "# --- model runtime ---",
171
+ runtime_start_command,
172
+ "date -u +'[%Y-%m-%dT%H:%M:%SZ] jambu onstart done'",
173
+ ]
174
+ return "\n".join(lines) + "\n"
175
+
176
+
177
+ def _shell_quote(value: str) -> str:
178
+ return "'" + value.replace("'", "'\"'\"'") + "'"
@@ -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,46 @@
1
+ jambu_gpu/__init__.py,sha256=P6KIiXguhDm9CeGcw3foqcxAKa7aFoolINxlBiNEBlo,123
2
+ jambu_gpu/__main__.py,sha256=dBCaXiNWd2Tv9A9tb_B9kJG2OOY-b5FpWYwRyV1yQso,66
3
+ jambu_gpu/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ jambu_gpu/cli/_common.py,sha256=GdE_B1PexSPA8ErlmtGbeih8BpSjnQKJ21OhUYfahLA,3308
5
+ jambu_gpu/cli/inspect.py,sha256=wXYuIKO_j6Hgmp7GJ_gwUo5_lPmsyDtRS-oKUtrh7GQ,8085
6
+ jambu_gpu/cli/lifecycle.py,sha256=zmgkUJNK9DSdzTumxDKW-gwyiIAjTaUOM00U8kpZy_M,3862
7
+ jambu_gpu/cli/main.py,sha256=1gLANCnlxQVDPlUpHI5-8zLm-SWv1XTgbE6t6m37w8c,3083
8
+ jambu_gpu/cli/providers.py,sha256=vDR5SgxsMVLWpZsdSLKuJy8qOUxv1M7-IHIJUCnAleI,3058
9
+ jambu_gpu/cli/run.py,sha256=Pcgyt-w76y01S1h8EXtPsUQBX9pDJQdwaPd_daf-T2Y,1180
10
+ jambu_gpu/cli/setup.py,sha256=a6uyzyNLWC8iLMifnJrbDqL9DTCveYKYR72ZNqBABj8,5473
11
+ jambu_gpu/cli/status.py,sha256=Veq5xcupz4XhR8GDvkoXvwkiPKpPz-5eOh9N1ayLG1k,6931
12
+ jambu_gpu/cli/validate.py,sha256=9DFh8TLIcvPIct5FQk78VPfW29PMqkOcij8N_F8yXHs,2081
13
+ jambu_gpu/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
+ jambu_gpu/core/beacon.py,sha256=9brx1I4vH-yIKHmKdNdB4qUSngSfW8mnKxrO7cbh0SY,4400
15
+ jambu_gpu/core/config.py,sha256=rTSHJDIaoulGWOmxX6cJ0rKOhOdEAbfhHSUqaCq9R5Q,20463
16
+ jambu_gpu/core/credentials.py,sha256=5sqo1rJoRS0Cu5ZijWuYYH8ZAJb6EVVBew4y0pMilcM,2532
17
+ jambu_gpu/core/durations.py,sha256=4-Gq--HNOBGXLLWQHssC08gkQwQjT_3oBHD4ootHKxs,1645
18
+ jambu_gpu/core/engine.py,sha256=EmLAVAIuHbETqOeF7ZvxB4yQfj52D2LeCYnbj2dsvhA,39545
19
+ jambu_gpu/core/errors.py,sha256=4TjV6YuJh7ocMLjER5WG6X6NPIH01K8zGJfUoYMOLOc,1435
20
+ jambu_gpu/core/events.py,sha256=vKO1GONEL6x-juaSlA717Z3n_vc_FkcXMabElBVGKV4,4013
21
+ jambu_gpu/core/execution.py,sha256=6cXL-QbYg7rDeIpMwgTl9uAUh9LhEwtfyST1l_QfADk,5689
22
+ jambu_gpu/core/health.py,sha256=nbd4JNvNF7-Lpd58Ce_48J7A5Sob3gek6ihv9Q36nkk,1909
23
+ jambu_gpu/core/model_inspect.py,sha256=vb22jQ0NaSOsKrKWE4BlxXgbh26_qMhRMsAHYLVofCA,15615
24
+ jambu_gpu/core/models.py,sha256=KkZMNduegE49Lcx9f4mo65Bj4o7KobrUH4zyrKawibk,7619
25
+ jambu_gpu/core/policy.py,sha256=H1-NxNgndmLMVxCE6UdwHbsdcY3JxTILXYLV-Uw25Ko,8214
26
+ jambu_gpu/core/state.py,sha256=DoLLc9giZW9qN20EYI-G9eOVi0Nlt3zRS6NHxdamJTY,13498
27
+ jambu_gpu/core/tool_parsers.py,sha256=zqaVJ0-m0H3B9gEhhe3-gWm7nhpwp9OBMAAsBx-Odu8,2475
28
+ jambu_gpu/providers/__init__.py,sha256=qM_BUMeabdamPdHTQHcSfp8okfjidkGhj2y_YUpBbMg,168
29
+ jambu_gpu/providers/base.py,sha256=UpPdyCvdk-hTdtXtHzdeo-Ut-kZPpIm4WurAHGxV5kQ,4582
30
+ jambu_gpu/providers/registry.py,sha256=tj63zPcEPmUxVLqHFiFIesS_rWes0B45mOwLmjilanE,2032
31
+ jambu_gpu/providers/vast/__init__.py,sha256=LPaQrruQaRu59u7IMrOkx2eNb_176vcN9DGvevL8E4g,63
32
+ jambu_gpu/providers/vast/client.py,sha256=MMRYcmoI8tZJ1DAQs2x9lgtPqjVxG7z2Wr469busm5I,9158
33
+ jambu_gpu/providers/vast/config.py,sha256=_R6fWMa4lTFKbPAlpyHZhzfE96OWuVTH803tjNL9mQo,1749
34
+ jambu_gpu/providers/vast/mapper.py,sha256=2Z0GW6MMAw8_rxKbwppPq7PLyvEZNvylYXzvUJCLalA,8284
35
+ jambu_gpu/providers/vast/provider.py,sha256=z17MRLtrJRaTmEWl9kgHCPVLtSCYwor547hbAqS1ttA,16123
36
+ jambu_gpu/runtime/__init__.py,sha256=ncpE8jTc_dX4cVlO3IBKg5vDv-n1TREKj4aksisqTDM,218
37
+ jambu_gpu/runtime/base.py,sha256=p3s1hyq_iKu5XTT9gwMj27m9iTTbIPw613nMO_C_M3U,2878
38
+ jambu_gpu/runtime/vllm.py,sha256=egfoGsRQiLQlr825wmZtIhsl0lLPGghWD-O85INaO-I,9117
39
+ jambu_gpu/watchdog/__init__.py,sha256=79yCptDKSE8dA-PowfQzcxrNaK6eEqhe_WDTi9BR_Fk,154
40
+ jambu_gpu/watchdog/agent_body.py,sha256=OET5tmEsG-cvb3T5af9XxP3GBlFAH-rvYQDakw0fp5E,18288
41
+ jambu_gpu/watchdog/builder.py,sha256=FAW_173EQYKA0Ffp-wHfzI-Gsil4WfjCXExvLePvUCs,5995
42
+ jambu_gpu-0.1.0.dist-info/METADATA,sha256=dJ68OrIKbvYgUxwflGI9fLAF9FD9gFB1KEA2R9J_YRY,2406
43
+ jambu_gpu-0.1.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
44
+ jambu_gpu-0.1.0.dist-info/entry_points.txt,sha256=y8w6NcSvrjLN56L88UuDEUWbryOp9aMr52rm_dESU1g,48
45
+ jambu_gpu-0.1.0.dist-info/licenses/LICENSE,sha256=7ofvt29ghOnzKapRr48K5JoCiQNnA5AcGFu6fSrtmSQ,1088
46
+ jambu_gpu-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.4
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ gpu = jambu_gpu.cli.main:main
@@ -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.