From cddbd2ff3e547f4388e4852a84bfa94063fdac7c Mon Sep 17 00:00:00 2001 From: alpha1122x Date: Mon, 27 Jul 2026 06:02:16 +0000 Subject: [PATCH 1/2] fix(lium): rent full offer gpu_count to avoid split 400 Lium returns HTTP 400 "Provider doesn't allow GPU splitting" when the rent body requests gpu_count=1 on multi-GPU non-splittable executors. InstanceSpec.gpu_count is a minimum-need filter; the rent body must use the selected offer's full capacity instead. --- src/base/compute/lium.py | 8 +++++++- tests/unit/test_compute_lium_client.py | 24 ++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/src/base/compute/lium.py b/src/base/compute/lium.py index 121945a6..2d7644f4 100644 --- a/src/base/compute/lium.py +++ b/src/base/compute/lium.py @@ -175,12 +175,18 @@ async def provision( ) template_id = await self._resolve_template(spec) + # InstanceSpec.gpu_count is a minimum-need filter for offer selection. + # Sending it as the rent gpu_count requests a *partial* split; Lium + # returns HTTP 400 "Provider doesn't allow GPU splitting" on most + # multi-GPU executors. Rent the full selected offer capacity instead + # (omit the field only when the offer itself has no gpu_count). rent_body: dict[str, Any] = { "pod_name": spec.name, "user_public_key": list(spec.ssh_public_keys), "termination_hours": int(lifetime), - "gpu_count": spec.gpu_count, } + if selected.gpu_count and selected.gpu_count > 0: + rent_body["gpu_count"] = selected.gpu_count if template_id is not None: rent_body["template_id"] = template_id if spec.dockerfile_content is not None: diff --git a/tests/unit/test_compute_lium_client.py b/tests/unit/test_compute_lium_client.py index 60a32d06..0188c069 100644 --- a/tests/unit/test_compute_lium_client.py +++ b/tests/unit/test_compute_lium_client.py @@ -198,6 +198,30 @@ async def test_provision_sends_termination_hours_and_ssh_key() -> None: assert body["template_id"] == "tpl-1" +@respx.mock +async def test_provision_rents_full_offer_gpu_count_not_spec_minimum() -> None: + """Lium rejects partial GPU rents on non-splittable executors. + + Live API evidence (2026-07): POST .../rent with gpu_count=1 on an 8-GPU + machine returns HTTP 400 "Provider doesn't allow GPU splitting.". + ``InstanceSpec.gpu_count`` is a minimum-need filter; the rent body must + request the selected offering's full gpu_count (or omit the field). + """ + routes = _mock_happy_path() + multi = Offer( + id="exec-1", + gpu_type="RTX A4000", + gpu_count=8, + price_per_hour=0.12, + ) + await LiumClient("k").provision(_spec(gpu_count=1), offer=multi) + body = json.loads(routes["rent"].calls.last.request.content) + assert body.get("gpu_count") != 1 + assert body.get("gpu_count") in (8, None) + if "gpu_count" in body: + assert body["gpu_count"] == multi.gpu_count + + @respx.mock async def test_provision_rejects_overpriced_offer_without_rent() -> None: rent = respx.post(f"{BASE}/executors/exec-1/rent") From cfdec9f88e5494e544087cf3f3f3d4562b3438f2 Mon Sep 17 00:00:00 2001 From: alpha1122x Date: Mon, 27 Jul 2026 06:32:00 +0000 Subject: [PATCH 2/2] fix(tests): stop concurrent seal race flake on wall-clock TTL MasterWeightsResponse validates expires_at against real datetime.now, so freshness_seconds=1 failed when concurrent seal latency exceeded 1s on CI. Use a longer FakeClock TTL and expire via clock.advance. --- tests/unit/test_master_weights_sealer.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/unit/test_master_weights_sealer.py b/tests/unit/test_master_weights_sealer.py index 00c8ac48..d2ea2334 100644 --- a/tests/unit/test_master_weights_sealer.py +++ b/tests/unit/test_master_weights_sealer.py @@ -320,10 +320,14 @@ async def test_zero_miner_empty_slate_still_seals( async def test_concurrent_get_seal_race_safe( session_factory: async_sessionmaker, ) -> None: + # MasterWeightsResponse.expires_at is validated against wall-clock now() + # (not FakeClock). freshness_seconds=1 flakes under concurrent DB load on + # CI when seal latency > 1s. Keep TTL large vs wall time, expire via clock. clock = FakeClock() + freshness = 120 service = _service( session_factory, - freshness_seconds=1, + freshness_seconds=freshness, epoch_interval_seconds=60, clock=clock, ) @@ -350,7 +354,7 @@ async def one_get() -> Any: assert all(r.uids == [0] and r.weights == [1.0] for r in results) # Expire + concurrent heal reseals once. - clock.advance(5) + clock.advance(freshness + 5) healed = await asyncio.gather(*[one_get() for _ in range(6)]) healed_ids = {r.vector_id for r in healed} assert len(healed_ids) == 1