jambu-gpu 0.3.0__tar.gz → 0.3.1__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.3.0 → jambu_gpu-0.3.1}/PKG-INFO +1 -1
- {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/docs/CONFIG.md +1 -0
- {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/jambu_gpu/__init__.py +1 -1
- {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/jambu_gpu/core/config.py +5 -0
- {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/jambu_gpu/core/engine.py +13 -0
- {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/jambu_gpu/core/models.py +8 -0
- {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/jambu_gpu/providers/vast/mapper.py +18 -0
- {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/jambu_gpu/runtime/base.py +10 -0
- {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/jambu_gpu/runtime/vllm.py +10 -0
- {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/pyproject.toml +1 -1
- {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/tests/test_engine.py +12 -0
- {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/tests/test_runtime_vllm.py +13 -0
- {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/tests/test_vast_mapper.py +35 -3
- jambu_gpu-0.3.1/uv.lock +870 -0
- {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/.env.example +0 -0
- {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/.gitignore +0 -0
- {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/CONTRIBUTING.md +0 -0
- {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/LICENSE +0 -0
- {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/README.md +0 -0
- {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/docs/ACCEPTANCE.md +0 -0
- {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/docs/ADDING_A_PROVIDER.md +0 -0
- {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/docs/CHOOSING_A_MODEL.md +0 -0
- {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/docs/INTEGRATION.md +0 -0
- {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/docs/LIFECYCLE.md +0 -0
- {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/docs/USAGE.md +0 -0
- {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/docs/assets/jambu-logo.jpg +0 -0
- {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/examples/experiment.py +0 -0
- {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/examples/jambu.yaml +0 -0
- {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/examples/mastra-agent/README.md +0 -0
- {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/examples/mastra-agent/package-lock.json +0 -0
- {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/examples/mastra-agent/package.json +0 -0
- {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/examples/mastra-agent/src/agent.ts +0 -0
- {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/examples/mastra-agent/src/index.ts +0 -0
- {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/examples/mastra-agent/src/tools/weather-tool.ts +0 -0
- {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/examples/mastra-agent/tsconfig.json +0 -0
- {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/examples/typescript-client/README.md +0 -0
- {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/examples/typescript-client/package-lock.json +0 -0
- {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/examples/typescript-client/package.json +0 -0
- {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/examples/typescript-client/src/index.ts +0 -0
- {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/examples/typescript-client/src/jambuGpu.ts +0 -0
- {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/examples/typescript-client/tsconfig.json +0 -0
- {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/jambu.example.yaml +0 -0
- {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/jambu.models.yaml +0 -0
- {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/jambu.yaml +0 -0
- {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/jambu_gpu/__main__.py +0 -0
- {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/jambu_gpu/cli/__init__.py +0 -0
- {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/jambu_gpu/cli/_common.py +0 -0
- {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/jambu_gpu/cli/account.py +0 -0
- {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/jambu_gpu/cli/inspect.py +0 -0
- {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/jambu_gpu/cli/lifecycle.py +0 -0
- {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/jambu_gpu/cli/main.py +0 -0
- {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/jambu_gpu/cli/providers.py +0 -0
- {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/jambu_gpu/cli/run.py +0 -0
- {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/jambu_gpu/cli/setup.py +0 -0
- {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/jambu_gpu/cli/status.py +0 -0
- {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/jambu_gpu/cli/timer.py +0 -0
- {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/jambu_gpu/cli/validate.py +0 -0
- {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/jambu_gpu/core/__init__.py +0 -0
- {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/jambu_gpu/core/beacon.py +0 -0
- {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/jambu_gpu/core/credentials.py +0 -0
- {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/jambu_gpu/core/durations.py +0 -0
- {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/jambu_gpu/core/errors.py +0 -0
- {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/jambu_gpu/core/events.py +0 -0
- {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/jambu_gpu/core/execution.py +0 -0
- {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/jambu_gpu/core/health.py +0 -0
- {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/jambu_gpu/core/hosts.py +0 -0
- {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/jambu_gpu/core/model_inspect.py +0 -0
- {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/jambu_gpu/core/paths.py +0 -0
- {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/jambu_gpu/core/policy.py +0 -0
- {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/jambu_gpu/core/state.py +0 -0
- {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/jambu_gpu/core/sweep.py +0 -0
- {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/jambu_gpu/core/tool_parsers.py +0 -0
- {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/jambu_gpu/providers/__init__.py +0 -0
- {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/jambu_gpu/providers/base.py +0 -0
- {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/jambu_gpu/providers/registry.py +0 -0
- {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/jambu_gpu/providers/vast/__init__.py +0 -0
- {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/jambu_gpu/providers/vast/client.py +0 -0
- {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/jambu_gpu/providers/vast/config.py +0 -0
- {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/jambu_gpu/providers/vast/provider.py +0 -0
- {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/jambu_gpu/runtime/__init__.py +0 -0
- {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/jambu_gpu/watchdog/__init__.py +0 -0
- {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/jambu_gpu/watchdog/agent_body.py +0 -0
- {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/jambu_gpu/watchdog/builder.py +0 -0
- {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/tests/conftest.py +0 -0
- {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/tests/fakes.py +0 -0
- {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/tests/test_cli.py +0 -0
- {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/tests/test_config.py +0 -0
- {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/tests/test_engine_sessions.py +0 -0
- {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/tests/test_integration.py +0 -0
- {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/tests/test_model_inspect.py +0 -0
- {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/tests/test_paths.py +0 -0
- {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/tests/test_policy.py +0 -0
- {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/tests/test_state.py +0 -0
- {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/tests/test_sweep.py +0 -0
- {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/tests/test_vast_provider.py +0 -0
- {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/tests/test_watchdog.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: jambu-gpu
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.1
|
|
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
|
|
@@ -86,6 +86,7 @@ each number comes from.
|
|
|
86
86
|
| `gpu.count` | `1` | Number of GPUs on one machine. |
|
|
87
87
|
| `gpu.name_filter` | – | Substring match, e.g. `A100`, `RTX 4090`. |
|
|
88
88
|
| `gpu.cuda_min` | – | Minimum host CUDA version. `gpu` raises it to what the runtime image needs, read from its tag (`-cu129` means 12.9, vLLM's bare `v0.32+` and `latest` mean 13.0), so an image never lands on a driver too old to run it. |
|
|
89
|
+
| `gpu.compute_cap_min` | – | Minimum GPU **compute capability** (architecture), e.g. `8.0` for Ampere. Distinct from `cuda_min`: a Pascal (GTX 10xx, 6.1) or Volta (V100, 7.0) card can sit behind a modern CUDA-13 driver — clearing `cuda_min` — yet still be too old for a runtime image whose kernels are built for newer architectures, which then crashes at engine startup. `gpu` raises this to what the runtime needs: a CUDA-13 vLLM image floors it at `8.0` (those wheels drop Volta), a CUDA-12 vLLM image at `7.0`. The cheapest Vast offers are often exactly these old cards, so without the floor the search keeps picking a GPU the image cannot run. |
|
|
89
90
|
| `disk_gb` | `80` | Disk. Must fit the model weights plus the container. |
|
|
90
91
|
| `region` | – | Comma-separated geolocations. |
|
|
91
92
|
| `spot` | `false` | Use interruptible/bid capacity where supported. |
|
|
@@ -79,6 +79,11 @@ class GpuConfig(_Base):
|
|
|
79
79
|
count: int = 1
|
|
80
80
|
name_filter: Optional[str] = None
|
|
81
81
|
cuda_min: Optional[str] = None
|
|
82
|
+
# Minimum GPU compute capability (architecture), e.g. 8.0 to require
|
|
83
|
+
# Ampere or newer. Raised to what the runtime needs; see
|
|
84
|
+
# ModelRuntime.required_compute_cap. Leave unset to accept the runtime's
|
|
85
|
+
# own floor.
|
|
86
|
+
compute_cap_min: Optional[float] = None
|
|
82
87
|
|
|
83
88
|
@field_validator("count")
|
|
84
89
|
@classmethod
|
|
@@ -366,6 +366,7 @@ class Engine:
|
|
|
366
366
|
count=cfg.compute.gpu.count,
|
|
367
367
|
name_filter=cfg.compute.gpu.name_filter,
|
|
368
368
|
cuda_min=self.cuda_min(),
|
|
369
|
+
compute_cap_min=self.compute_cap_min(),
|
|
369
370
|
),
|
|
370
371
|
disk_gb=cfg.compute.disk_gb,
|
|
371
372
|
image=self.runtime.image(),
|
|
@@ -389,6 +390,18 @@ class Engine:
|
|
|
389
390
|
return configured
|
|
390
391
|
return f"{needed:.1f}"
|
|
391
392
|
|
|
393
|
+
def compute_cap_min(self) -> Optional[float]:
|
|
394
|
+
"""The stricter of compute.gpu.compute_cap_min and the runtime's floor."""
|
|
395
|
+
candidates = [
|
|
396
|
+
c
|
|
397
|
+
for c in (
|
|
398
|
+
self.config.compute.gpu.compute_cap_min,
|
|
399
|
+
self.runtime.required_compute_cap(),
|
|
400
|
+
)
|
|
401
|
+
if c is not None
|
|
402
|
+
]
|
|
403
|
+
return max(candidates) if candidates else None
|
|
404
|
+
|
|
392
405
|
def _ports(self) -> list[int]:
|
|
393
406
|
ports = list(self.runtime.ports())
|
|
394
407
|
if self.config.lifecycle.watchdog.enabled:
|
|
@@ -91,6 +91,11 @@ class GpuSpec:
|
|
|
91
91
|
count: int = 1
|
|
92
92
|
name_filter: Optional[str] = None
|
|
93
93
|
cuda_min: Optional[str] = None
|
|
94
|
+
# Minimum GPU compute capability (architecture), e.g. 8.0 for Ampere. The
|
|
95
|
+
# host driver's CUDA (``cuda_min``) is not the same thing: a Pascal/Volta
|
|
96
|
+
# card can sit behind a CUDA-13 driver yet still be too old for a runtime
|
|
97
|
+
# image built for newer architectures, which then dies at engine startup.
|
|
98
|
+
compute_cap_min: Optional[float] = None
|
|
94
99
|
|
|
95
100
|
|
|
96
101
|
@dataclass
|
|
@@ -127,6 +132,9 @@ class Offer:
|
|
|
127
132
|
score: Optional[float] = None
|
|
128
133
|
# Highest CUDA version the host's driver supports, when the provider says.
|
|
129
134
|
cuda_max: Optional[float] = None
|
|
135
|
+
# The GPU's compute capability (architecture), e.g. 8.6 for an RTX 3090,
|
|
136
|
+
# when the provider says. Distinct from cuda_max (the driver's CUDA).
|
|
137
|
+
compute_cap: Optional[float] = None
|
|
130
138
|
# The physical machine behind the offer, when the provider says. Offers
|
|
131
139
|
# on one machine share its network, disk and Docker daemon.
|
|
132
140
|
host_id: Optional[str] = None
|
|
@@ -82,6 +82,12 @@ def _float_or_none(value: Any) -> Optional[float]:
|
|
|
82
82
|
return None
|
|
83
83
|
|
|
84
84
|
|
|
85
|
+
def _compute_cap(value: Any) -> Optional[float]:
|
|
86
|
+
"""Vast's ``compute_cap`` is capability ×100 (860 → 8.6); normalize it."""
|
|
87
|
+
raw = _float_or_none(value)
|
|
88
|
+
return raw / 100.0 if raw else None
|
|
89
|
+
|
|
90
|
+
|
|
85
91
|
def effective_min_cuda(spec: ComputeSpec, options: VastOptions) -> Optional[float]:
|
|
86
92
|
"""The stricter of the adapter's floor and what the spec (image) needs."""
|
|
87
93
|
known = [
|
|
@@ -110,6 +116,8 @@ def map_offer(raw: dict[str, Any]) -> Offer:
|
|
|
110
116
|
score=float(raw.get("score") or 0.0) if raw.get("score") is not None else None,
|
|
111
117
|
# Highest CUDA the host driver runs; an image needing more dies at boot.
|
|
112
118
|
cuda_max=_float_or_none(raw.get("cuda_max_good")),
|
|
119
|
+
# Vast reports compute capability ×100 (860 = 8.6); normalize to 8.6.
|
|
120
|
+
compute_cap=_compute_cap(raw.get("compute_cap")),
|
|
113
121
|
host_id=host_id(raw),
|
|
114
122
|
raw=raw,
|
|
115
123
|
)
|
|
@@ -193,6 +201,12 @@ def build_search_query(
|
|
|
193
201
|
min_cuda = effective_min_cuda(spec, options)
|
|
194
202
|
if min_cuda:
|
|
195
203
|
query["cuda_max_good"] = {"gte": min_cuda}
|
|
204
|
+
# Floor the GPU architecture, not just the driver's CUDA: Vast reports
|
|
205
|
+
# compute capability ×100, so 8.0 -> 800. Without this the cheapest offers
|
|
206
|
+
# (old Volta/Pascal cards behind modern drivers) pass cuda_max_good but
|
|
207
|
+
# crash a newer runtime image at engine startup.
|
|
208
|
+
if spec.gpu.compute_cap_min:
|
|
209
|
+
query["compute_cap"] = {"gte": int(round(float(spec.gpu.compute_cap_min) * 100))}
|
|
196
210
|
# gpu_name is deliberately NOT sent server-side: Vast's field only
|
|
197
211
|
# accepts an exact match ({"eq": ...}), but name_filter is documented and
|
|
198
212
|
# used everywhere else (offer_matches below) as a case-insensitive
|
|
@@ -258,6 +272,10 @@ def offer_matches(offer: Offer, spec: ComputeSpec, min_cuda: Optional[float] = N
|
|
|
258
272
|
# 1e-6: 12.9 must match a host reporting exactly 12.9.
|
|
259
273
|
if min_cuda and offer.cuda_max is not None and offer.cuda_max + 1e-6 < min_cuda:
|
|
260
274
|
return False
|
|
275
|
+
# Vast's compute_cap filter is advisory; verify the architecture floor too.
|
|
276
|
+
min_cc = spec.gpu.compute_cap_min
|
|
277
|
+
if min_cc and offer.compute_cap is not None and offer.compute_cap + 1e-6 < float(min_cc):
|
|
278
|
+
return False
|
|
261
279
|
if offer.gpu_count < spec.gpu.count:
|
|
262
280
|
return False
|
|
263
281
|
if spec.gpu.min_vram_gb and offer.gpu_vram_gb + 0.5 < float(spec.gpu.min_vram_gb):
|
|
@@ -116,6 +116,16 @@ class ModelRuntime(ABC):
|
|
|
116
116
|
"""
|
|
117
117
|
return cuda_from_tag(image_tag(self.image()))
|
|
118
118
|
|
|
119
|
+
def required_compute_cap(self) -> Optional[float]:
|
|
120
|
+
"""Minimum GPU compute capability (architecture) the runtime needs.
|
|
121
|
+
|
|
122
|
+
None if unknown. The offer filter is raised to this, so an image whose
|
|
123
|
+
kernels are built for newer architectures is never placed on a GPU too
|
|
124
|
+
old to run them (which the host driver's CUDA alone does not catch: a
|
|
125
|
+
Volta card behind a CUDA-13 driver still fails at engine startup).
|
|
126
|
+
"""
|
|
127
|
+
return None
|
|
128
|
+
|
|
119
129
|
# -- on-instance liveness -------------------------------------------------
|
|
120
130
|
|
|
121
131
|
def runtime_pid_path(self) -> Optional[str]:
|
|
@@ -123,6 +123,16 @@ class VllmRuntime(ModelRuntime):
|
|
|
123
123
|
def required_cuda(self) -> Optional[float]:
|
|
124
124
|
return vllm_image_cuda(self.image())
|
|
125
125
|
|
|
126
|
+
def required_compute_cap(self) -> Optional[float]:
|
|
127
|
+
# vLLM wheels built for CUDA 13+ drop Volta (sm_70) and need Ampere
|
|
128
|
+
# (8.0) or newer; those crash at engine-core init on a V100 or older.
|
|
129
|
+
# CUDA 12.x wheels still run Volta. Pascal (GTX 10xx, 6.1) is below
|
|
130
|
+
# vLLM's floor on any build.
|
|
131
|
+
cuda = self.required_cuda()
|
|
132
|
+
if cuda is None:
|
|
133
|
+
return None
|
|
134
|
+
return 8.0 if cuda >= 13.0 else 7.0
|
|
135
|
+
|
|
126
136
|
def runtime_pid_path(self) -> str:
|
|
127
137
|
return PID_PATH
|
|
128
138
|
|
|
@@ -462,6 +462,18 @@ def test_an_image_of_unknown_cuda_keeps_the_configured_floor(write_config):
|
|
|
462
462
|
assert _engine(write_config, compute=pinned).build_spec().gpu.cuda_min == "12.4"
|
|
463
463
|
|
|
464
464
|
|
|
465
|
+
def test_the_spec_floors_the_gpu_architecture_the_runtime_needs(write_config):
|
|
466
|
+
# Default CUDA-13 vLLM image -> Ampere (8.0).
|
|
467
|
+
assert _engine(write_config).build_spec().gpu.compute_cap_min == 8.0
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
def test_a_stricter_configured_compute_cap_wins_and_a_looser_one_loses(write_config):
|
|
471
|
+
strict = _engine(write_config, compute={"gpu": {"compute_cap_min": 8.9}})
|
|
472
|
+
assert strict.build_spec().gpu.compute_cap_min == 8.9 # e.g. force Ada
|
|
473
|
+
loose = _engine(write_config, compute={"gpu": {"compute_cap_min": 7.0}})
|
|
474
|
+
assert loose.build_spec().gpu.compute_cap_min == 8.0 # runtime floor still wins
|
|
475
|
+
|
|
476
|
+
|
|
465
477
|
def test_validate_warns_about_an_unpinned_image(write_config):
|
|
466
478
|
for image in ("vllm/vllm-openai:latest", "vllm/vllm-openai"):
|
|
467
479
|
result = _engine(write_config, compute={"image": image}).validate(check_provider=False)
|
|
@@ -241,6 +241,19 @@ def test_vast_published_vllm_images_carry_their_cuda(write_config):
|
|
|
241
241
|
assert VllmRuntime(write_config(compute={"image": image})).required_cuda() == cuda
|
|
242
242
|
|
|
243
243
|
|
|
244
|
+
def test_a_vllm_image_floors_the_gpu_architecture(write_config):
|
|
245
|
+
"""CUDA-13 vLLM wheels drop Volta and need Ampere (8.0); 12.x still runs Volta (7.0).
|
|
246
|
+
|
|
247
|
+
Caught on a live run: the CUDA-13 image kept landing on the cheapest offers
|
|
248
|
+
(V100, GTX 1080) whose driver cleared cuda_max_good but whose architecture
|
|
249
|
+
crashed vLLM's engine-core init."""
|
|
250
|
+
assert VllmRuntime(write_config(compute={"image": "vastai/vllm:v0.32.0-cuda-13.0"})).required_compute_cap() == 8.0
|
|
251
|
+
assert VllmRuntime(write_config(compute={"image": "vllm/vllm-openai:v0.32.0"})).required_compute_cap() == 8.0
|
|
252
|
+
assert VllmRuntime(write_config(compute={"image": "vastai/vllm:v0.29.0-cuda-12.9"})).required_compute_cap() == 7.0
|
|
253
|
+
# Unknown image CUDA -> no architecture claim.
|
|
254
|
+
assert VllmRuntime(write_config(compute={"image": "vllm/vllm-openai:nightly"})).required_compute_cap() is None
|
|
255
|
+
|
|
256
|
+
|
|
244
257
|
def test_the_start_command_finds_the_python_that_has_vllm(config, tmp_path):
|
|
245
258
|
"""vastai/vllm puts a shim first on PATH; vllm lives under /usr/bin/python3."""
|
|
246
259
|
import subprocess
|
|
@@ -122,13 +122,18 @@ def test_unknown_provider_options_are_rejected_early():
|
|
|
122
122
|
# -- CUDA: the image's floor and the adapter's floor, whichever is higher -----
|
|
123
123
|
|
|
124
124
|
|
|
125
|
-
def _spec(cuda_min=None):
|
|
126
|
-
return ComputeSpec(
|
|
125
|
+
def _spec(cuda_min=None, compute_cap_min=None):
|
|
126
|
+
return ComputeSpec(
|
|
127
|
+
gpu=GpuSpec(min_vram_gb=24, cuda_min=cuda_min, compute_cap_min=compute_cap_min),
|
|
128
|
+
disk_gb=80,
|
|
129
|
+
)
|
|
127
130
|
|
|
128
131
|
|
|
129
|
-
def _offer(cuda):
|
|
132
|
+
def _offer(cuda, compute_cap=None):
|
|
130
133
|
raw = {"id": 1, "gpu_name": "RTX 4090", "num_gpus": 1, "gpu_ram": 24564,
|
|
131
134
|
"disk_space": 100, "dph_total": 0.3, "cuda_max_good": cuda}
|
|
135
|
+
if compute_cap is not None:
|
|
136
|
+
raw["compute_cap"] = compute_cap
|
|
132
137
|
return mapper.map_offer(raw)
|
|
133
138
|
|
|
134
139
|
|
|
@@ -159,6 +164,33 @@ def test_a_host_whose_driver_is_too_old_for_the_image_is_rejected_client_side():
|
|
|
159
164
|
assert mapper.offer_matches(_offer(12.0), _spec())
|
|
160
165
|
|
|
161
166
|
|
|
167
|
+
def test_offers_carry_the_gpus_compute_capability():
|
|
168
|
+
# Vast reports compute_cap ×100 (860 = 8.6); we normalize it.
|
|
169
|
+
assert _offer(13.0, compute_cap=860).compute_cap == 8.6
|
|
170
|
+
assert _offer(13.0, compute_cap=700).compute_cap == 7.0
|
|
171
|
+
assert _offer(13.0).compute_cap is None
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def test_the_search_floors_the_gpu_architecture():
|
|
175
|
+
# ×100 the way Vast stores it: 8.0 -> 800.
|
|
176
|
+
opts = VastOptions()
|
|
177
|
+
assert mapper.build_search_query(_spec(compute_cap_min=8.0), opts)["compute_cap"] == {"gte": 800}
|
|
178
|
+
assert mapper.build_search_query(_spec(compute_cap_min=7.0), opts)["compute_cap"] == {"gte": 700}
|
|
179
|
+
assert "compute_cap" not in mapper.build_search_query(_spec(), opts)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def test_a_gpu_too_old_for_the_image_is_rejected_client_side():
|
|
183
|
+
"""Caught on a live run: a CUDA-13 vLLM image landed on a V100/GTX 1080
|
|
184
|
+
(whose driver cleared cuda_max_good) and crashed at engine startup."""
|
|
185
|
+
spec = _spec(compute_cap_min=8.0)
|
|
186
|
+
assert not mapper.offer_matches(_offer(13.0, compute_cap=700), spec) # V100 (7.0)
|
|
187
|
+
assert not mapper.offer_matches(_offer(13.0, compute_cap=610), spec) # GTX 1080 (6.1)
|
|
188
|
+
assert mapper.offer_matches(_offer(13.0, compute_cap=860), spec) # RTX 3090 (8.6)
|
|
189
|
+
assert mapper.offer_matches(_offer(13.0, compute_cap=800), spec) # exactly 8.0
|
|
190
|
+
assert mapper.offer_matches(_offer(13.0), spec) # unknown: trust the server filter
|
|
191
|
+
assert mapper.offer_matches(_offer(13.0, compute_cap=700), _spec()) # no floor set
|
|
192
|
+
|
|
193
|
+
|
|
162
194
|
def test_find_offers_applies_the_effective_floor(monkeypatch):
|
|
163
195
|
from jambu_gpu.providers.vast.provider import VastProvider
|
|
164
196
|
|