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.
Files changed (96) hide show
  1. {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/PKG-INFO +1 -1
  2. {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/docs/CONFIG.md +1 -0
  3. {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/jambu_gpu/__init__.py +1 -1
  4. {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/jambu_gpu/core/config.py +5 -0
  5. {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/jambu_gpu/core/engine.py +13 -0
  6. {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/jambu_gpu/core/models.py +8 -0
  7. {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/jambu_gpu/providers/vast/mapper.py +18 -0
  8. {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/jambu_gpu/runtime/base.py +10 -0
  9. {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/jambu_gpu/runtime/vllm.py +10 -0
  10. {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/pyproject.toml +1 -1
  11. {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/tests/test_engine.py +12 -0
  12. {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/tests/test_runtime_vllm.py +13 -0
  13. {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/tests/test_vast_mapper.py +35 -3
  14. jambu_gpu-0.3.1/uv.lock +870 -0
  15. {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/.env.example +0 -0
  16. {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/.gitignore +0 -0
  17. {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/CONTRIBUTING.md +0 -0
  18. {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/LICENSE +0 -0
  19. {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/README.md +0 -0
  20. {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/docs/ACCEPTANCE.md +0 -0
  21. {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/docs/ADDING_A_PROVIDER.md +0 -0
  22. {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/docs/CHOOSING_A_MODEL.md +0 -0
  23. {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/docs/INTEGRATION.md +0 -0
  24. {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/docs/LIFECYCLE.md +0 -0
  25. {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/docs/USAGE.md +0 -0
  26. {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/docs/assets/jambu-logo.jpg +0 -0
  27. {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/examples/experiment.py +0 -0
  28. {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/examples/jambu.yaml +0 -0
  29. {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/examples/mastra-agent/README.md +0 -0
  30. {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/examples/mastra-agent/package-lock.json +0 -0
  31. {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/examples/mastra-agent/package.json +0 -0
  32. {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/examples/mastra-agent/src/agent.ts +0 -0
  33. {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/examples/mastra-agent/src/index.ts +0 -0
  34. {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/examples/mastra-agent/src/tools/weather-tool.ts +0 -0
  35. {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/examples/mastra-agent/tsconfig.json +0 -0
  36. {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/examples/typescript-client/README.md +0 -0
  37. {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/examples/typescript-client/package-lock.json +0 -0
  38. {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/examples/typescript-client/package.json +0 -0
  39. {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/examples/typescript-client/src/index.ts +0 -0
  40. {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/examples/typescript-client/src/jambuGpu.ts +0 -0
  41. {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/examples/typescript-client/tsconfig.json +0 -0
  42. {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/jambu.example.yaml +0 -0
  43. {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/jambu.models.yaml +0 -0
  44. {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/jambu.yaml +0 -0
  45. {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/jambu_gpu/__main__.py +0 -0
  46. {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/jambu_gpu/cli/__init__.py +0 -0
  47. {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/jambu_gpu/cli/_common.py +0 -0
  48. {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/jambu_gpu/cli/account.py +0 -0
  49. {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/jambu_gpu/cli/inspect.py +0 -0
  50. {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/jambu_gpu/cli/lifecycle.py +0 -0
  51. {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/jambu_gpu/cli/main.py +0 -0
  52. {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/jambu_gpu/cli/providers.py +0 -0
  53. {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/jambu_gpu/cli/run.py +0 -0
  54. {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/jambu_gpu/cli/setup.py +0 -0
  55. {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/jambu_gpu/cli/status.py +0 -0
  56. {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/jambu_gpu/cli/timer.py +0 -0
  57. {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/jambu_gpu/cli/validate.py +0 -0
  58. {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/jambu_gpu/core/__init__.py +0 -0
  59. {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/jambu_gpu/core/beacon.py +0 -0
  60. {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/jambu_gpu/core/credentials.py +0 -0
  61. {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/jambu_gpu/core/durations.py +0 -0
  62. {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/jambu_gpu/core/errors.py +0 -0
  63. {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/jambu_gpu/core/events.py +0 -0
  64. {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/jambu_gpu/core/execution.py +0 -0
  65. {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/jambu_gpu/core/health.py +0 -0
  66. {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/jambu_gpu/core/hosts.py +0 -0
  67. {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/jambu_gpu/core/model_inspect.py +0 -0
  68. {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/jambu_gpu/core/paths.py +0 -0
  69. {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/jambu_gpu/core/policy.py +0 -0
  70. {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/jambu_gpu/core/state.py +0 -0
  71. {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/jambu_gpu/core/sweep.py +0 -0
  72. {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/jambu_gpu/core/tool_parsers.py +0 -0
  73. {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/jambu_gpu/providers/__init__.py +0 -0
  74. {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/jambu_gpu/providers/base.py +0 -0
  75. {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/jambu_gpu/providers/registry.py +0 -0
  76. {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/jambu_gpu/providers/vast/__init__.py +0 -0
  77. {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/jambu_gpu/providers/vast/client.py +0 -0
  78. {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/jambu_gpu/providers/vast/config.py +0 -0
  79. {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/jambu_gpu/providers/vast/provider.py +0 -0
  80. {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/jambu_gpu/runtime/__init__.py +0 -0
  81. {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/jambu_gpu/watchdog/__init__.py +0 -0
  82. {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/jambu_gpu/watchdog/agent_body.py +0 -0
  83. {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/jambu_gpu/watchdog/builder.py +0 -0
  84. {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/tests/conftest.py +0 -0
  85. {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/tests/fakes.py +0 -0
  86. {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/tests/test_cli.py +0 -0
  87. {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/tests/test_config.py +0 -0
  88. {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/tests/test_engine_sessions.py +0 -0
  89. {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/tests/test_integration.py +0 -0
  90. {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/tests/test_model_inspect.py +0 -0
  91. {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/tests/test_paths.py +0 -0
  92. {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/tests/test_policy.py +0 -0
  93. {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/tests/test_state.py +0 -0
  94. {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/tests/test_sweep.py +0 -0
  95. {jambu_gpu-0.3.0 → jambu_gpu-0.3.1}/tests/test_vast_provider.py +0 -0
  96. {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.0
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. |
@@ -1,5 +1,5 @@
1
1
  """gpu: provider-agnostic GPU runtime provisioning and lifecycle CLI."""
2
2
 
3
- __version__ = "0.3.0"
3
+ __version__ = "0.3.1"
4
4
 
5
5
  __all__ = ["__version__"]
@@ -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
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "jambu-gpu"
7
- version = "0.3.0"
7
+ version = "0.3.1"
8
8
  description = "Provider-agnostic CLI for provisioning and managing temporary GPU runtimes"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -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(gpu=GpuSpec(min_vram_gb=24, cuda_min=cuda_min), disk_gb=80)
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