-
Notifications
You must be signed in to change notification settings - Fork 16
feat(prism): enable unattested Lium dispatch for PRISM jobs (T14) #54
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
echobt
wants to merge
3
commits into
main
Choose a base branch
from
feat/lium-dispatch
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+311
−10
Open
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,76 @@ | ||
| # PRISM → Lium dispatch (T14) | ||
|
|
||
| Master-owned path that admits Prism GPU training/eval work onto **our** Lium | ||
| machines. This is **compute only** — not TEE, not constation elevation. | ||
|
|
||
| ## Trigger (ops) | ||
|
|
||
| ```bash | ||
| # Master (preferred for "our machines") | ||
| export BASE_LIUM_TRAINING__ENABLED=true | ||
| export BASE_LIUM_TRAINING__API_KEY_FILE=/run/secrets/lium_api_key # never log | ||
| export BASE_LIUM_TRAINING__SSH_PUBLIC_KEY_FILE=/run/secrets/lium_ssh.pub | ||
| # Optional spend / concurrency guards (defaults in LiumTrainingSettings): | ||
| # BASE_LIUM_TRAINING__MAX_PRICE_PER_HOUR=1.50 | ||
| # BASE_LIUM_TRAINING__CONCURRENCY_CAP=3 | ||
| # BASE_LIUM_TRAINING__DAILY_SPEND_CEILING_USD=50 | ||
|
|
||
| # Optional worker label (default stays base_gpu) | ||
| export PRISM_EXECUTION_BACKEND=lium | ||
| ``` | ||
|
|
||
| Fail-closed: | ||
|
|
||
| - `lium_training.enabled=false` (default) → `try_build_lium_capacity_scheduler` returns `None`; no rentals. | ||
| - `enabled=true` without `api_key` / `api_key_file` → hard `build_*` raises `ValueError`; soft `try_build_*` returns `None` so master still boots. | ||
|
|
||
| ## Code path | ||
|
|
||
| 1. `cli_app/main.py` — `try_build_lium_capacity_scheduler(settings)` when master starts. | ||
| 2. `MasterOrchestrationDriver.bridge_pending_work` — Prism GPU units → `lium_scheduler.enqueue(submission_id, job_id)`. | ||
| 3. `run_once` — `lium_scheduler.tick()` admits FIFO when 1-GPU Blackwell inventory is free. | ||
| 4. `LiumCapacityScheduler` — provisions via training-locked `LiumClient.for_prism_training` (`lium_capacity.py` + `lium_training_wiring.py`). | ||
| 5. Worker construct — `PRISM_EXECUTION_BACKEND=lium` / `execution_backend=lium` is always allowed at `PrismWorker` construction (**no** constation bundle). Bundle remains optional API-compat only. | ||
|
|
||
| Empty inventory → lease stays `queued` with `reason=capacity_wait` (never a terminal fail for capacity). | ||
|
|
||
| ## Config keys (`LiumTrainingSettings`) | ||
|
|
||
| | Field | Env | Default | Notes | | ||
| |-------|-----|---------|-------| | ||
| | `enabled` | `BASE_LIUM_TRAINING__ENABLED` | `false` | Master plane off until ops flips | | ||
| | `api_key` | `BASE_LIUM_TRAINING__API_KEY` | `null` | Prefer file; never log | | ||
| | `api_key_file` | `BASE_LIUM_TRAINING__API_KEY_FILE` | `null` | File contents read lazily | | ||
| | `ssh_public_key_file` | `BASE_LIUM_TRAINING__SSH_PUBLIC_KEY_FILE` | `null` | Injected into pods | | ||
| | `max_price_per_hour` | `BASE_LIUM_TRAINING__MAX_PRICE_PER_HOUR` | `1.50` | Per-GPU hourly ceiling | | ||
| | `max_lifetime_hours` | `BASE_LIUM_TRAINING__MAX_LIFETIME_HOURS` | `4.0` | Pod lifetime guard | | ||
| | `concurrency_cap` | `BASE_LIUM_TRAINING__CONCURRENCY_CAP` | `3` | Max concurrent training pods | | ||
| | `daily_spend_ceiling_usd` | `BASE_LIUM_TRAINING__DAILY_SPEND_CEILING_USD` | `50.0` | Blocks **new** admissions only | | ||
| | `pod_name_prefix` | `BASE_LIUM_TRAINING__POD_NAME_PREFIX` | `prism-train-` | Recover matches this prefix | | ||
|
|
||
| Prism worker label: `execution_backend` / `PRISM_EXECUTION_BACKEND` (`base_gpu` default; `lium` allowed bare). | ||
|
|
||
| ## What this is not | ||
|
|
||
| - **Not TEE.** No attestation claim, no constation_ok elevation from selecting `lium`. | ||
| - **Not live billable by default.** Tests mock the client; live rentals require explicit ops env + real key. | ||
| - Constation modules stay in-tree for the separate ingestion elevation path (todos 19–22). | ||
|
|
||
| ## T15 handoff (live 1M e2e) | ||
|
|
||
| 1. Pack `examples/tiny-1m` (or equivalent tiny recipe) as the Prism work unit. | ||
| 2. Set `BASE_LIVE_PROVIDER_TESTS=1` **and** a real key via `BASE_LIUM_TRAINING__API_KEY_FILE` only in a controlled env. | ||
| 3. Run master with `BASE_LIUM_TRAINING__ENABLED=true`; confirm bridge enqueue + tick provisions a pod; tear down pods after. | ||
| 4. Do **not** claim TEE / constation from a successful rental. | ||
| 5. Capture spend + pod IDs (not API key material) in evidence. | ||
|
|
||
| ## Tests | ||
|
|
||
| ```bash | ||
| UV_CACHE_DIR=/var/tmp/uv-cache uv run pytest \ | ||
| packages/challenges/prism/tests/test_execution_backend_constation_gate.py \ | ||
| tests/unit/test_lium_dispatch_unattested.py \ | ||
| tests/unit/test_orchestration_lium.py \ | ||
| tests/unit/test_lium_training_wiring.py \ | ||
| -q | ||
| ``` |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,196 @@ | ||
| """T14 — unattested master-owned Lium dispatch path (mocked; no billable rent). | ||
|
|
||
| Proves the code path that submits Prism GPU work onto Lium capacity without | ||
| constation elevation or live provider calls. | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from dataclasses import dataclass, field | ||
| from datetime import UTC, datetime | ||
| from typing import Any | ||
|
|
||
| import pytest | ||
| from sqlalchemy import select | ||
|
|
||
| from base.compute.lium_capacity import ( | ||
| InMemoryLeaseStore, | ||
| LeaseState, | ||
| LiumCapacityScheduler, | ||
| ) | ||
| from base.compute.lium_training_wiring import ( | ||
| build_lium_capacity_scheduler, | ||
| try_build_lium_capacity_scheduler, | ||
| ) | ||
| from base.compute.provider import Instance, InstanceSpec, Offer | ||
| from base.config.settings import LiumTrainingSettings, Settings | ||
| from base.db import Base, create_engine, create_session_factory | ||
| from base.db.models import WorkAssignment | ||
| from base.master.assignment import AssignmentService | ||
| from base.master.orchestration import ChallengePendingWork, MasterOrchestrationDriver | ||
| from base.master.validator_coordination import ValidatorCoordinationService | ||
|
|
||
| NOW = datetime(2026, 6, 27, 12, 0, 0, tzinfo=UTC) | ||
|
|
||
| _BLACKWELL = "NVIDIA RTX PRO 6000 Blackwell Server Edition" | ||
|
|
||
|
|
||
| @dataclass | ||
| class FakeLiumClient: | ||
| """In-memory Lium surface — never hits the network.""" | ||
|
|
||
| offers: list[Offer] = field(default_factory=list) | ||
| provisioned: list[InstanceSpec] = field(default_factory=list) | ||
| terminated: list[str] = field(default_factory=list) | ||
| pods: list[dict[str, Any]] = field(default_factory=list) | ||
| _seq: int = 0 | ||
|
|
||
| async def list_offers( | ||
| self, *, max_price_per_hour: float | None = None | ||
| ) -> list[Offer]: | ||
| out: list[Offer] = [] | ||
| for offer in self.offers: | ||
| if ( | ||
| max_price_per_hour is not None | ||
| and offer.price_per_hour > max_price_per_hour | ||
| ): | ||
| continue | ||
| out.append(offer) | ||
| return out | ||
|
|
||
| async def list_pods(self) -> list[dict[str, Any]]: | ||
| return list(self.pods) | ||
|
|
||
| async def provision( | ||
| self, spec: InstanceSpec, *, offer: Offer | None = None | ||
| ) -> Instance: | ||
| self.provisioned.append(spec) | ||
| self._seq += 1 | ||
| pod_id = f"pod-fake-{self._seq}" | ||
| self.pods.append( | ||
| { | ||
| "id": pod_id, | ||
| "pod_name": spec.name, | ||
| "name": spec.name, | ||
| "status": "RUNNING", | ||
| "provider": "lium", | ||
| } | ||
| ) | ||
| if offer is not None: | ||
| self.offers = [o for o in self.offers if o.id != offer.id] | ||
| elif self.offers: | ||
| self.offers = self.offers[1:] | ||
| return Instance(id=pod_id, status="RUNNING", provider="lium") | ||
|
|
||
| async def terminate(self, instance_id: str) -> None: | ||
| self.terminated.append(instance_id) | ||
| self.pods = [p for p in self.pods if str(p.get("id")) != str(instance_id)] | ||
|
|
||
|
|
||
| def _blackwell_offer(*, offer_id: str = "offer-bw-1", price: float = 1.0) -> Offer: | ||
| return Offer( | ||
| id=offer_id, | ||
| gpu_type=_BLACKWELL, | ||
| gpu_count=1, | ||
| price_per_hour=price, | ||
| provider="lium", | ||
| raw={"machine_name": "rtx-pro-6000-blackwell-server"}, | ||
| ) | ||
|
|
||
|
|
||
| def test_try_build_scheduler_none_when_disabled() -> None: | ||
| """Given lium_training.enabled=False, When try_build, Then None (default off).""" | ||
| settings = Settings(lium_training=LiumTrainingSettings(enabled=False)) | ||
| assert try_build_lium_capacity_scheduler(settings) is None | ||
|
|
||
|
|
||
| def test_try_build_scheduler_fail_closed_missing_key_returns_none() -> None: | ||
| """Given enabled without key, When try_build, Then None (master still boots).""" | ||
| settings = Settings(lium_training=LiumTrainingSettings(enabled=True)) | ||
| assert try_build_lium_capacity_scheduler(settings) is None | ||
|
|
||
|
|
||
| def test_build_scheduler_fail_closed_raises_without_key() -> None: | ||
| """Given enabled without key, When hard build, Then ValueError names key fields.""" | ||
| settings = Settings(lium_training=LiumTrainingSettings(enabled=True)) | ||
| with pytest.raises(ValueError, match="api_key|api_key_file|lium_training"): | ||
| build_lium_capacity_scheduler(settings) | ||
|
|
||
|
|
||
| async def test_dispatch_enqueue_and_tick_provisions_mocked_pod() -> None: | ||
| """Given Prism pending work + fake Lium inventory, When bridge+tick, Then pod provisioned. | ||
|
|
||
| End-to-end master-owned dispatch without constation and without live API. | ||
| """ | ||
| client = FakeLiumClient(offers=[_blackwell_offer()]) | ||
| store = InMemoryLeaseStore() | ||
| scheduler = LiumCapacityScheduler( | ||
| lambda: client, | ||
| store=store, | ||
| ssh_public_keys=("ssh-ed25519 AAAA test-dispatch",), | ||
| ) | ||
|
|
||
| engine = create_engine("sqlite+aiosqlite:///:memory:") | ||
| try: | ||
| async with engine.begin() as connection: | ||
| await connection.run_sync(Base.metadata.create_all) | ||
| factory = create_session_factory(engine) | ||
| service = AssignmentService(factory, now_fn=lambda: NOW) | ||
| validators = ValidatorCoordinationService(factory, now_fn=lambda: NOW) | ||
|
|
||
| @dataclass | ||
| class _Src: | ||
| async def fetch_pending_work(self) -> list[ChallengePendingWork]: | ||
| return [ | ||
| ChallengePendingWork( | ||
| challenge_slug="prism", | ||
| submission_id="sub-t14-1", | ||
| submission_ref="miner-hk", | ||
| checkpoint_ref="hf://ckpt/1", | ||
| job_id="job-t14-1", | ||
| ) | ||
| ] | ||
|
|
||
| driver = MasterOrchestrationDriver( | ||
| assignment_service=service, | ||
| validator_service=validators, | ||
| work_source=_Src(), | ||
| lium_scheduler=scheduler, | ||
| ) | ||
|
|
||
| # bridge enqueues; run_once ticks Lium and provisions | ||
| result = await driver.run_once() | ||
| assert result.bridged["prism"] == ["sub-t14-1"] | ||
|
|
||
| assert len(client.provisioned) == 1 | ||
| assert client.provisioned[0].name == "prism-train-sub-t14-1" | ||
| active = store.get("sub-t14-1") | ||
| assert active is not None | ||
| assert active.state is LeaseState.ACTIVE | ||
| assert active.pod_id is not None | ||
|
|
||
| async with factory() as session: | ||
| rows = list((await session.execute(select(WorkAssignment))).scalars().all()) | ||
| assert len(rows) == 1 | ||
| assert rows[0].work_unit_id == "sub-t14-1" | ||
| assert rows[0].required_capability == "gpu" | ||
| finally: | ||
| await engine.dispose() | ||
|
|
||
|
|
||
| async def test_dispatch_queues_when_inventory_empty_no_terminal_fail() -> None: | ||
| """Given empty Lium inventory, When tick, Then lease stays queued (capacity_wait).""" | ||
| client = FakeLiumClient(offers=[]) | ||
| store = InMemoryLeaseStore() | ||
| scheduler = LiumCapacityScheduler( | ||
| lambda: client, | ||
| store=store, | ||
| ssh_public_keys=("ssh-ed25519 AAAA test-dispatch",), | ||
| ) | ||
| scheduler.enqueue(submission_id="sub-wait", job_id="job-wait") | ||
| await scheduler.tick() | ||
| lease = store.get("sub-wait") | ||
| assert lease is not None | ||
| assert lease.state is LeaseState.QUEUED | ||
| assert lease.reason == "capacity_wait" | ||
| assert client.provisioned == [] | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Fix the Ruff E501 failures.
Lines 121 and 182 exceed the configured 88-character limit, blocking CI.
Proposed fix
Also applies to: 181-182
🧰 Tools
🪛 GitHub Actions: CI / 8_ruff.txt
[error] 121-121: ruff check failed (E501): Line too long (94 > 88).
🪛 GitHub Actions: CI / ruff
[error] 121-121: ruff E501 Line too long (94 > 88).
🤖 Prompt for AI Agents
Source: Pipeline failures