canado / tests /unit /test_provision_loop.py
funny790's picture
Upload folder using huggingface_hub (part 4)
bb02ad0 verified
Raw
History Blame Contribute Delete
68.7 kB
"""Service loop — the full rent→publish→watch→teardown cycle on fakes.
Every boundary (chain, plan, providers, manifest store, health, clock) is
injected, so these tests drive whole rounds without a cloud account, a GPU,
or a real trainer. The hosts.toml the loop publishes is always re-parsed with
the trainer's own loader — the contract, not the string, is asserted."""
from __future__ import annotations
import json
import tomllib
import pytest
from cascade.provision.core import PodAddress, ProvisionError
from cascade.provision.health import CheckResult, HealthReport
from cascade.provision.loop import ProvisionerLoop, RenderSettings, parse_plan_output
from cascade.provision.main import build_policy
from cascade.provision.policy import ProvisionPolicy, SkuCandidate, StagePolicy
from cascade.provision.state import load_state
from cascade.trainer.remote import RemoteDispatchError, load_hosts
IMG = "reg.example/cascade-worker@sha256:" + "a" * 64
# ── fakes ────────────────────────────────────────────────────────────────────
class FakeProvider:
"""In-memory marketplace: launches get sequential IPs, terminate frees them."""
def __init__(self, name, *, available=True, avail_raises=None, price=None):
self.name = name
self._available = available
self._avail_raises = avail_raises
self._price = price
self._next_ip = 0
self.live: dict[str, PodAddress] = {}
self.launched: list[str] = []
self.terminated: list[str] = []
def available(self, sku, count, *, gpus=1):
if self._avail_raises:
raise self._avail_raises
return self._available
def launch(self, spec):
ids = []
for i in range(spec.count):
self._next_ip += 1
pid = f"{spec.name_prefix}-{i}"
self.live[pid] = PodAddress(f"10.0.0.{self._next_ip}", 22)
ids.append(pid)
self.launched += ids
return ids
def wait_ready(self, pod_id, *, timeout):
return pod_id in self.live
def get_ip(self, pod_id):
return self.live.get(pod_id)
def terminate(self, pod_id):
self.terminated.append(pod_id)
self.live.pop(pod_id, None)
def list_tagged(self, prefix):
return [pid for pid in self.live if pid.startswith(prefix)]
def offer_price(self, sku):
return self._price
class FakeChain:
def __init__(self, block):
self.block = block
def current_block(self):
return self.block
class FakeStore:
def __init__(self, texts=None):
self.texts = dict(texts or {})
def get_text(self, key):
if key not in self.texts:
raise KeyError(key)
return self.texts[key]
class Clock:
def __init__(self, t=1_000.0):
self.t = t
def __call__(self):
return self.t
def _report(ok=True, name="fake"):
return HealthReport(checks=(CheckResult(name=name, ok=ok),))
def cycle(loop):
"""One poll tick, joined: renting runs in a worker thread in production;
tests join it so assertions see the settled world (same discipline as
``_join_eval``)."""
loop.run_once()
t = loop._rent_thread
if t is not None:
t.join(timeout=30)
def _policy(**kw):
kw.setdefault("heat", StagePolicy(sku="NVIDIA RTX A6000", gpus_per_pod=8, max_pods=2,
providers=("lium", "shadeform"), max_price_hr=4.0))
kw.setdefault("final", StagePolicy(sku="NVIDIA L40S", gpus_per_pod=2, max_pods=2,
providers=("lium", "shadeform"), max_price_hr=3.0))
kw.setdefault("trigger_margin_blocks", 25)
kw.setdefault("max_spend_per_round", 25.0)
return ProvisionPolicy(**kw)
PLAN = {
"block": 880, "epoch_blocks": 900, "next_boundary_block": 900,
"blocks_to_boundary": 20, "king": "5King", "resolved": 14,
"challengers": 13, "eligible_challengers": 12,
"heat_train_hours": 0.5, "finalists": 1,
}
def make_loop(tmp_path, *, providers=None, block=880, policy=None, plan=None,
clock=None, store=None, health=None, dry_run=False, plan_calls=None,
eval_hosts=None, receipt_prefix="", escalate_deadline_s=1800.0,
min_viable_fleet=0.5, rent_retry_cooldown_s=900.0,
final_rent_on="margin", max_duds_per_stage=8):
providers = providers if providers is not None else {"lium": FakeProvider("lium")}
plan_calls = plan_calls if plan_calls is not None else []
def plan_fn():
plan_calls.append(1)
return dict(plan or PLAN)
return ProvisionerLoop(
policy=policy or _policy(),
providers=providers,
chain_client=FakeChain(block),
plan_fn=plan_fn,
render=RenderSettings(image=IMG, ssh_pubkey="ssh-ed25519 AAAA orch",
key_path="~/.ssh/cascade_ed25519"),
hosts_path=tmp_path / "hosts.toml",
work_root=tmp_path / "work",
state_path=tmp_path / "state.json",
epoch_blocks=900, # 3h epochs → ttl 3h at ttl_epochs=1
final_hours=0.25,
manifest_store=store,
eval_hosts_path=eval_hosts,
receipt_prefix=receipt_prefix,
health_check=health,
dry_run=dry_run,
clock=clock or Clock(),
escalate_deadline_s=escalate_deadline_s,
min_viable_fleet=min_viable_fleet,
rent_retry_cooldown_s=rent_retry_cooldown_s,
final_rent_on=final_rent_on,
max_duds_per_stage=max_duds_per_stage,
), plan_calls
# ── happy path ───────────────────────────────────────────────────────────────
def test_happy_path_rents_publishes_and_records(tmp_path):
prov = FakeProvider("lium")
loop, plan_calls = make_loop(tmp_path, providers={"lium": prov})
cycle(loop)
# 12 eligible → 3 heat slots → one 8-GPU pod; king+1 finalist → one 2-GPU pod.
assert prov.launched == ["cascade-900-heat-0", "cascade-900-final-0"]
assert prov.terminated == []
hosts = load_hosts(tmp_path / "hosts.toml") # the trainer's own loader
heat = [h for h in hosts if h.stage == "heat"]
final = [h for h in hosts if h.stage == "final"]
assert len(heat) == 8 and len(final) == 2 # per-GPU fan-out
assert {h.cuda_device for h in heat} == {str(i) for i in range(8)}
assert all(h.name.startswith("cascade-900-heat") for h in heat)
st = load_state(tmp_path / "state.json")
assert st.round_id == "900" and st.published
assert {(i.stage, i.instance_id) for i in st.instances} == {
("heat", "cascade-900-heat-0"), ("final", "cascade-900-final-0")}
# Rent-once latch: staying inside the margin must not rent again.
cycle(loop)
assert plan_calls == [1] and prov.launched == ["cascade-900-heat-0", "cascade-900-final-0"]
def test_no_trigger_outside_margin(tmp_path):
prov = FakeProvider("lium")
loop, plan_calls = make_loop(tmp_path, providers={"lium": prov}, block=800)
cycle(loop)
assert plan_calls == [] and prov.launched == []
def test_dry_run_rents_nothing(tmp_path):
prov = FakeProvider("lium")
loop, plan_calls = make_loop(tmp_path, providers={"lium": prov}, dry_run=True)
cycle(loop)
assert plan_calls == [1]
assert prov.launched == []
assert not (tmp_path / "hosts.toml").exists()
assert load_state(tmp_path / "state.json") is None
cycle(loop) # latch also applies to dry runs
assert plan_calls == [1]
# ── provider failure modes ───────────────────────────────────────────────────
def test_provider_down_falls_through_to_next(tmp_path):
lium = FakeProvider("lium", avail_raises=RuntimeError("api down"))
shade = FakeProvider("shadeform")
loop, _ = make_loop(tmp_path, providers={"lium": lium, "shadeform": shade})
cycle(loop)
assert lium.launched == []
assert shade.launched == ["cascade-900-heat-0", "cascade-900-final-0"]
def test_one_stage_without_capacity_still_rents_the_other(tmp_path):
# lium has capacity but only heat's price is acceptable on it… simpler:
# heat's only provider is down, final's works → final-only fleet.
policy = _policy(heat=StagePolicy(sku="NVIDIA RTX A6000", gpus_per_pod=8, max_pods=2,
providers=("lium",), max_price_hr=4.0),
final=StagePolicy(sku="NVIDIA L40S", gpus_per_pod=2, max_pods=2,
providers=("shadeform",), max_price_hr=3.0))
lium = FakeProvider("lium", available=False)
shade = FakeProvider("shadeform")
loop, _ = make_loop(tmp_path, providers={"lium": lium, "shadeform": shade}, policy=policy)
cycle(loop)
assert lium.launched == []
assert shade.launched == ["cascade-900-final-0"]
hosts = load_hosts(tmp_path / "hosts.toml")
assert all(h.stage == "final" for h in hosts) # degraded, not dead
def test_all_providers_down_clears_hosts_round_never_lost(tmp_path):
prov = FakeProvider("lium", available=False)
loop, plan_calls = make_loop(tmp_path, providers={"lium": prov})
cycle(loop)
assert prov.launched == []
# Empty hosts file = the trainer's explicit local-fallback signal.
assert (tmp_path / "hosts.toml").is_file()
with pytest.raises(RemoteDispatchError):
load_hosts(tmp_path / "hosts.toml")
cycle(loop) # latched: no 30s hammering
assert plan_calls == [1]
def test_overpriced_offer_skips_that_provider(tmp_path):
dear = FakeProvider("lium", price=99.0) # above both stage caps
fair = FakeProvider("shadeform", price=2.0)
loop, _ = make_loop(tmp_path, providers={"lium": dear, "shadeform": fair})
cycle(loop)
assert dear.launched == [] and len(fair.launched) == 2
def test_budget_breaker_refuses_to_rent(tmp_path):
# Prices are under the per-stage caps, but worst-case (ttl = 3h epoch)
# projection 2 pods × $2.0 × 3h = $12 > the $10 round cap → refuse ALL.
prov = FakeProvider("lium", price=2.0)
loop, _ = make_loop(tmp_path, providers={"lium": prov},
policy=_policy(max_spend_per_round=10.0))
cycle(loop)
assert prov.launched == []
with pytest.raises(RemoteDispatchError):
load_hosts(tmp_path / "hosts.toml") # cleared → local fallback
# ── health gate: terminate + one replacement ─────────────────────────────────
def test_unhealthy_pod_replaced_once(tmp_path):
prov = FakeProvider("lium")
bad_ips = {"10.0.0.1"} # the first heat pod's IP
def health(addr, stage, provider="", **shape):
return _report(ok=addr.ip not in bad_ips)
loop, _ = make_loop(tmp_path, providers={"lium": prov}, health=health)
cycle(loop)
assert prov.terminated == ["cascade-900-heat-0"]
assert "cascade-900-heat-r0-0" in prov.launched # the one replacement
st = load_state(tmp_path / "state.json")
assert {i.instance_id for i in st.instances} == {
"cascade-900-heat-r0-0", "cascade-900-final-0"}
heat = [h for h in load_hosts(tmp_path / "hosts.toml") if h.stage == "heat"]
assert all(h.host == "10.0.0.2" for h in heat) # the replacement's IP
def test_replacement_also_unhealthy_drops_the_slot(tmp_path):
prov = FakeProvider("lium")
def health(addr, stage, provider="", **shape):
return _report(ok=(stage != "heat")) # every heat pod is a lemon
loop, _ = make_loop(tmp_path, providers={"lium": prov}, health=health)
cycle(loop)
# Original + its single replacement both terminated; no third attempt.
assert prov.terminated == ["cascade-900-heat-0", "cascade-900-heat-r0-0"]
hosts = load_hosts(tmp_path / "hosts.toml")
assert all(h.stage == "final" for h in hosts) # heat degraded away
st = load_state(tmp_path / "state.json")
assert {i.instance_id for i in st.instances} == {"cascade-900-final-0"}
def test_replacement_excludes_the_failed_pods_machine(tmp_path):
"""Incident 2026-07-14 round 5052267627071284702: the eval pod failed its
boot gate and the replacement re-rented the SAME lium executor (offer
lists are deterministic), so it failed identically. The replacement spec
must exclude the lemon's machine."""
class MachineAwareProvider(FakeProvider):
machines = ("m-lemon", "m-good", "m-spare")
def __init__(self, name, **kw):
super().__init__(name, **kw)
self.machine_by_pod: dict[str, str] = {}
self.specs = []
def launch(self, spec):
self.specs.append(spec)
offers = [m for m in self.machines if m not in spec.exclude_ids]
ids = super().launch(spec)
for i, pid in enumerate(ids): # head-of-list pick, like lium
self.machine_by_pod[pid] = offers[i]
return ids
def machine_of(self, pod_id):
return self.machine_by_pod.get(pod_id)
prov = MachineAwareProvider("lium")
def health(addr, stage, provider="", **shape):
pid = next(p for p, a in prov.live.items() if a.ip == addr.ip)
return _report(ok=not (stage == "heat" and prov.machine_by_pod[pid] == "m-lemon"))
loop, _ = make_loop(tmp_path, providers={"lium": prov}, health=health)
cycle(loop)
rspec = next(s for s in prov.specs if "-r0" in s.name_prefix)
assert rspec.exclude_ids == ("m-lemon",) # the fix
assert prov.machine_by_pod["cascade-900-heat-r0-0"] == "m-good"
heat = [h for h in load_hosts(tmp_path / "hosts.toml") if h.stage == "heat"]
assert heat and all(h.host == prov.live["cascade-900-heat-r0-0"].ip for h in heat)
def test_every_pod_unhealthy_clears_hosts(tmp_path):
prov = FakeProvider("lium")
loop, _ = make_loop(tmp_path, providers={"lium": prov},
health=lambda addr, stage, provider="", **shape: _report(ok=False))
cycle(loop)
assert prov.live == {} # nothing left billing
with pytest.raises(RemoteDispatchError):
load_hosts(tmp_path / "hosts.toml")
# ── rules of escalation: empty rung → next rung; partial fleet → top-up ──────
class LaunchFailProvider(FakeProvider):
"""Capacity probe says yes, the actual rent call 500s — the gap that
escalation rule 2 closes (a launch failure used to lose the stage)."""
def launch(self, spec):
raise RuntimeError("api 500")
def test_launch_failure_escalates_to_next_provider(tmp_path):
lium = LaunchFailProvider("lium")
shade = FakeProvider("shadeform")
loop, _ = make_loop(tmp_path, providers={"lium": lium, "shadeform": shade})
cycle(loop)
# Both stages: lium accepted the probe, failed the launch → the same rung
# on shadeform rents instead (escalation batch names carry -e1).
assert lium.launched == []
assert shade.launched == ["cascade-900-heat-e1-0", "cascade-900-final-e1-0"]
hosts = load_hosts(tmp_path / "hosts.toml")
assert {h.stage for h in hosts} == {"heat", "final"}
def test_all_duds_on_one_provider_escalate_to_the_next(tmp_path):
lium = FakeProvider("lium")
shade = FakeProvider("shadeform")
def health(addr, stage, provider="", **shape):
return _report(ok=provider != "lium") # a bad lium batch
loop, _ = make_loop(tmp_path, providers={"lium": lium, "shadeform": shade},
health=health)
cycle(loop)
# Per stage on lium: the pod AND its one replacement were duds → the stage
# (not just the slot) escalates and rents healthy on shadeform.
assert lium.live == {} and len(lium.terminated) == 4 # heat+final, orig+repl
assert shade.launched == ["cascade-900-heat-e1-0", "cascade-900-final-e1-0"]
st = load_state(tmp_path / "state.json")
assert {i.instance_id for i in st.instances} == {
"cascade-900-heat-e1-0", "cascade-900-final-e1-0"}
def test_zero_deadline_disables_escalation(tmp_path):
lium = LaunchFailProvider("lium")
shade = FakeProvider("shadeform")
loop, _ = make_loop(tmp_path, providers={"lium": lium, "shadeform": shade},
escalate_deadline_s=0.0)
cycle(loop)
# Deadline already spent at the first failure → pre-escalation behaviour:
# the stage degrades, hosts clear, the trainer covers the round locally.
assert shade.launched == []
with pytest.raises(RemoteDispatchError):
load_hosts(tmp_path / "hosts.toml")
def test_escalated_rung_rechecked_against_budget(tmp_path):
# Primary heat rung (one 8x pod) passes the round gate; its launch fails.
# The only fallback rung (1x singles → 3 pods) would blow the round cap
# with the final included → refused, ladder exhausted, heat degrades —
# but the final still rents.
class HeatLaunchFails(FakeProvider):
def launch(self, spec):
if "-heat" in spec.name_prefix:
raise RuntimeError("no heat capacity after all")
return super().launch(spec)
prov = HeatLaunchFails("lium", price=2.0)
policy = _policy(
heat=StagePolicy(sku="NVIDIA RTX A6000", gpus_per_pod=8, max_pods=4,
providers=("lium",), max_price_hr=4.0,
candidates=(SkuCandidate(sku="NVIDIA RTX A6000",
gpus_per_pod=1, max_price_hr=4.0),)),
final=StagePolicy(sku="NVIDIA L40S", gpus_per_pod=2, max_pods=2,
providers=("lium",), max_price_hr=3.0),
max_spend_per_round=20.0)
# Initial projection: (1 heat + 1 final) × $2 × 3h TTL = $12 <= $20. The
# escalated rung projects 3 × $2 × 3h = $18 heat + $6 final = $24 > $20.
loop, _ = make_loop(tmp_path, providers={"lium": prov}, policy=policy)
cycle(loop)
assert prov.launched == ["cascade-900-final-0"]
hosts = load_hosts(tmp_path / "hosts.toml")
assert all(h.stage == "final" for h in hosts) # degraded, not dead
def test_below_viability_partial_fleet_tops_up_same_candidate(tmp_path):
prov = FakeProvider("lium")
plan = dict(PLAN, eligible_challengers=80) # 19 slots → 3 × 8-GPU pods
# Heat pods rent as .1/.2/.3; replacements draw .4/.5. Duds: pods 0 and 1
# AND their replacements → 1 of 3 pods healthy = 8 of 19 slots < 50%.
bad = {"10.0.0.1", "10.0.0.2", "10.0.0.4", "10.0.0.5"}
def health(addr, stage, provider="", **shape):
return _report(ok=addr.ip not in bad)
policy = _policy(
heat=StagePolicy(sku="NVIDIA RTX A6000", gpus_per_pod=8, max_pods=3,
providers=("lium",), max_price_hr=4.0),
max_spend_per_round=100.0)
loop, _ = make_loop(tmp_path, providers={"lium": prov}, plan=plan,
policy=policy, health=health)
cycle(loop)
# One same-candidate top-up batch re-rents exactly the two missing pods.
assert "cascade-900-heat-t0-0" in prov.launched
assert "cascade-900-heat-t0-1" in prov.launched
heat = [h for h in load_hosts(tmp_path / "hosts.toml") if h.stage == "heat"]
assert len(heat) == 3 * 8 # back to full strength
st = load_state(tmp_path / "state.json")
heat_ids = {i.instance_id for i in st.instances if i.stage == "heat"}
assert heat_ids == {"cascade-900-heat-2",
"cascade-900-heat-t0-0", "cascade-900-heat-t0-1"}
def test_failed_round_retries_after_cooldown(tmp_path):
prov = FakeProvider("lium", available=False)
clock = Clock()
loop, plan_calls = make_loop(tmp_path, providers={"lium": prov}, clock=clock)
cycle(loop)
assert prov.launched == [] # trigger found no capacity
cycle(loop) # inside cooldown: latched
assert prov.launched == [] and plan_calls == [1]
prov._available = True # the market recovered
clock.t += 901 # cooldown elapsed
cycle(loop)
# Both failed stages re-entered pick→budget→rent — no new plan_fn call
# (the trigger's payload is cached; reveals are closed mid-round anyway).
assert plan_calls == [1]
assert prov.launched == ["cascade-900-heat-0", "cascade-900-final-0"]
hosts = load_hosts(tmp_path / "hosts.toml")
assert {h.stage for h in hosts} == {"heat", "final"}
def test_retry_gives_up_when_the_window_closes(tmp_path):
prov = FakeProvider("lium", available=False)
clock = Clock()
loop, _ = make_loop(tmp_path, providers={"lium": prov}, clock=clock)
cycle(loop)
prov._available = True
clock.t += 901
loop.chain_client.block = 1700 # 0.33h left in the round:
cycle(loop) # heat needs 0.5h + final
assert prov.launched == [] # window closed → no rent
assert loop._stage_failed == set() # and no further retries
def test_final_defers_until_heat_marker_and_sizes_off_it(tmp_path):
prov = FakeProvider("lium")
policy = _policy(max_spend_per_round=100.0)
loop, _ = make_loop(tmp_path, providers={"lium": prov}, policy=policy,
final_rent_on="heat_complete")
cycle(loop)
# Margin trigger rents the HEAT only; the final waits on the trainer.
assert prov.launched == ["cascade-900-heat-0"]
assert load_state(tmp_path / "state.json").final_pending is True
# The trainer settles the heat: marker names TWO actual finalists (the
# plan predicted one) — the JIT fleet must match the marker, not the plan.
marker_dir = tmp_path / "work" / "777"
marker_dir.mkdir(parents=True)
(marker_dir / "heat_complete.json").write_text(json.dumps(
{"round_id": "777", "screened": 12, "finalists": ["f1", "f2"]}))
cycle(loop)
# Same tick: the marker tears the heat down AND rents the final — sized
# 1 + 2 actual finalists = 3 slots → two 2-GPU pods.
assert "cascade-900-heat-0" in prov.terminated
assert prov.launched[1:] == ["cascade-900-final-0", "cascade-900-final-1"]
hosts = load_hosts(tmp_path / "hosts.toml")
assert all(h.stage == "final" for h in hosts) and len(hosts) == 4
st = load_state(tmp_path / "state.json")
assert st.final_pending is False
assert {i.stage for i in st.instances} == {"final"}
def test_scarce_final_market_rents_early_at_the_margin(tmp_path):
"""The JIT exception: when the pinned SKU's primary rung probes scarce at
the margin, the final rents EARLY (locking whatever the ladder finds)
instead of gambling that capacity exists hours later."""
class NoTwoGpuPods(FakeProvider):
def available(self, sku, count, *, gpus=1):
return gpus != 2 # 2x L40S pool is dry
prov = NoTwoGpuPods("lium")
policy = _policy(
final=StagePolicy(sku="NVIDIA L40S", gpus_per_pod=2, max_pods=2,
providers=("lium",), max_price_hr=3.0,
candidates=(SkuCandidate(sku="NVIDIA L40S",
gpus_per_pod=1, max_price_hr=1.5),)))
loop, _ = make_loop(tmp_path, providers={"lium": prov}, policy=policy,
final_rent_on="heat_complete")
cycle(loop)
# Final rented at the margin via the 1x fallback rung — not deferred.
assert "cascade-900-final-0" in prov.launched
assert "cascade-900-final-1" in prov.launched
assert load_state(tmp_path / "state.json").final_pending is False
def test_final_retry_ignores_a_previous_rounds_stale_marker(tmp_path):
"""A pre-marker final retry must size off the PLAN, not whatever old
heat_complete.json the work-root still holds: a stale 5-finalist marker
inflated the retry fleet past the budget cap and sank the whole retry."""
marker_dir = tmp_path / "work" / "555" # a PREVIOUS round's
marker_dir.mkdir(parents=True) # marker, 5 finalists
(marker_dir / "heat_complete.json").write_text(json.dumps(
{"round_id": "555", "screened": 30,
"finalists": ["a", "b", "c", "d", "e"]}))
prov = FakeProvider("lium", available=False)
clock = Clock()
loop, _ = make_loop(tmp_path, providers={"lium": prov}, clock=clock)
cycle(loop) # trigger: market dry
prov._available = True
clock.t += 901
cycle(loop) # retry: market back
# Plan says 1 finalist → 2 slots → ONE 2-GPU final pod (and the budget
# gate passes: $12 heat + $6 final <= $25).
assert prov.launched == ["cascade-900-heat-0", "cascade-900-final-0"]
def test_final_pending_survives_restart(tmp_path):
prov = FakeProvider("lium")
policy = _policy(max_spend_per_round=100.0)
loop, _ = make_loop(tmp_path, providers={"lium": prov}, policy=policy,
final_rent_on="heat_complete")
cycle(loop)
assert load_state(tmp_path / "state.json").final_pending is True
# New process, same ledger: still waiting on the marker (heat instances
# exist → the marker scan re-anchors on their rent times).
loop2, _ = make_loop(tmp_path, providers={"lium": prov}, policy=policy,
final_rent_on="heat_complete")
assert loop2._final_pending is True
assert loop2._heat_marker_latched is False
def test_rent_runs_off_the_loop_thread(tmp_path):
"""Renting must never block the poll loop (the 2026-07-14 lesson): the
worker can sit in a provider call for minutes while ticks keep landing —
and the orphan reaper must NOT run mid-rent, when the worker may own pods
it has not ledgered yet."""
import threading as _t
gate = _t.Event()
class BlockingProvider(FakeProvider):
def launch(self, spec):
gate.wait(timeout=30)
return super().launch(spec)
prov = BlockingProvider("lium")
loop, _ = make_loop(tmp_path, providers={"lium": prov})
loop.run_once() # returns while launch is stuck
assert loop._rent_inflight and prov.launched == []
prov.live["cascade-900-heat-x9"] = PodAddress("10.9.9.9", 22) # a stray
loop.run_once() # loop still ticking mid-rent…
assert "cascade-900-heat-x9" in prov.live # …but reaping is deferred
gate.set()
loop._rent_thread.join(timeout=30)
assert "cascade-900-heat-0" in prov.launched
loop.run_once() # worker done → stray reaped now
assert "cascade-900-heat-x9" not in prov.live
def test_manifest_mid_rent_aborts_the_worker_and_pods_are_reaped(tmp_path):
"""The round can END while a rent worker is mid-flight (manifest publish).
The worker must not publish pods for a dead round; whatever it ledgered
dies in the teardown sweep."""
import threading as _t
gate = _t.Event()
final_entered = _t.Event()
class FinalBlocks(FakeProvider):
def launch(self, spec):
if "-final" in spec.name_prefix:
final_entered.set()
gate.wait(timeout=30)
return super().launch(spec)
prov = FinalBlocks("lium")
store = FakeStore({"manifests/latest.json": '{"round_id": "1"}'})
loop, _ = make_loop(tmp_path, providers={"lium": prov}, store=store)
loop.run_once() # worker: heat rents, final blocks
assert final_entered.wait(timeout=30) # heat is healthy + ledgered now
store.texts["manifests/latest.json"] = '{"round_id": "2"}' # round over
loop.run_once() # teardown reaps heat, arms abort
assert "cascade-900-heat-0" in prov.terminated
gate.set()
loop._rent_thread.join(timeout=30)
loop.run_once() # the aborted rental's pods die too
assert "cascade-900-final-0" in prov.terminated
with pytest.raises(RemoteDispatchError):
load_hosts(tmp_path / "hosts.toml") # never published for a dead round
def test_dud_attempt_backs_off_the_retry_cooldown(tmp_path):
prov = FakeProvider("lium")
clock = Clock()
loop, _ = make_loop(tmp_path, providers={"lium": prov}, clock=clock,
health=lambda a, s, provider="", **k: _report(ok=False))
cycle(loop) # every pod a dud
n0 = len(prov.launched)
assert n0 > 0
assert loop._retry_backoff == {"heat": 2.0, "final": 2.0}
clock.t += 901 # one FLAT cooldown: not yet —
cycle(loop) # dud attempts pay double
assert len(prov.launched) == n0
clock.t += 1000 # past 2× cooldown → retried
cycle(loop)
assert len(prov.launched) > n0
assert loop._retry_backoff == {"heat": 4.0, "final": 4.0} # and doubled again
def test_dud_cap_stops_renting_for_the_round(tmp_path):
prov = FakeProvider("lium")
clock = Clock()
loop, _ = make_loop(tmp_path, providers={"lium": prov}, clock=clock,
health=lambda a, s, provider="", **k: _report(ok=False),
max_duds_per_stage=2)
cycle(loop) # pod + replacement dud per stage = at cap
n0 = len(prov.launched)
clock.t += 100_000 # far past any backoff
cycle(loop)
assert len(prov.launched) == n0 # money backstop: no more renting
assert loop._stage_failed == set() # and no further retry attempts
def test_pending_final_gives_up_when_window_closes(tmp_path):
prov = FakeProvider("lium")
policy = _policy(max_spend_per_round=100.0)
loop, _ = make_loop(tmp_path, providers={"lium": prov}, policy=policy,
final_rent_on="heat_complete")
cycle(loop) # heat rented; final pending
assert load_state(tmp_path / "state.json").final_pending is True
loop.chain_client.block = 1760 # 8 min left; marker never came
cycle(loop)
assert load_state(tmp_path / "state.json").final_pending is False
assert not any("-final" in p for p in prov.launched)
def test_viable_partial_fleet_does_not_top_up(tmp_path):
prov = FakeProvider("lium")
plan = dict(PLAN, eligible_challengers=80) # 19 slots → 3 × 8-GPU pods
bad = {"10.0.0.1", "10.0.0.4"} # pod 0 + its replacement
def health(addr, stage, provider="", **shape):
return _report(ok=addr.ip not in bad)
policy = _policy(
heat=StagePolicy(sku="NVIDIA RTX A6000", gpus_per_pod=8, max_pods=3,
providers=("lium",), max_price_hr=4.0),
max_spend_per_round=100.0)
loop, _ = make_loop(tmp_path, providers={"lium": prov}, plan=plan,
policy=policy, health=health)
cycle(loop)
# 2 of 3 pods = 16 of 19 slots >= 50% → viable; the dropped slot stays
# dropped (serial waves), no top-up batch and no escalation.
assert not any("-t0" in p or "-e1" in p for p in prov.launched)
heat = [h for h in load_hosts(tmp_path / "hosts.toml") if h.stage == "heat"]
assert len(heat) == 2 * 8
# ── watch + per-stage teardown ───────────────────────────────────────────────
def _provisioned(tmp_path, **kw):
prov = FakeProvider("lium")
loop, _ = make_loop(tmp_path, providers={"lium": prov}, **kw)
cycle(loop)
assert len(prov.live) == 2
return loop, prov
def test_heat_marker_tears_down_heat_while_final_runs(tmp_path):
clock = Clock()
loop, prov = _provisioned(tmp_path, clock=clock)
# The trainer settles the heat: work_root/<base_seed>/heat_complete.json.
# The provisioner keys rounds by boundary block and cannot know base_seed
# in advance — ANY marker newer than rent time is this round's signal.
marker_dir = tmp_path / "work" / "54321"
marker_dir.mkdir(parents=True)
(marker_dir / "heat_complete.json").write_text(
json.dumps({"round_id": "54321", "screened": 12, "finalists": ["hk"]}))
clock.t += 3600.0 # 1h in: TTL (3h) not due
cycle(loop)
assert prov.terminated == ["cascade-900-heat-0"]
assert "cascade-900-final-0" in prov.live # final keeps training
hosts = load_hosts(tmp_path / "hosts.toml") # re-rendered final-only
assert [h.stage for h in hosts] == ["final", "final"]
st = load_state(tmp_path / "state.json")
assert {i.stage for i in st.instances} == {"final"}
class SeededChain(FakeChain):
"""A chain that can answer block_seed(boundary) — the real ChainClient
surface the provisioner uses to match a marker to the round it belongs to."""
def __init__(self, block, seed):
super().__init__(block)
self._seed = seed
def block_seed(self, boundary):
return self._seed
def test_zombie_marker_for_a_different_round_is_ignored(tmp_path):
# A zombie trainer re-touching an OLD round's heat_complete.json bumps its
# mtime past our rent time, so the mtime heuristic alone read it as THIS
# round's heat completing and tore down the fleet. Match the marker's
# work-dir base_seed to the round we are actually serving instead.
clock = Clock()
prov = FakeProvider("lium")
loop, _ = make_loop(tmp_path, providers={"lium": prov}, clock=clock)
loop.chain_client = SeededChain(880, 54321) # this round's base_seed = 54321
cycle(loop)
assert len(prov.live) == 2
# A zombie from a PRIOR round re-touches ITS marker (a different base_seed).
zombie = tmp_path / "work" / "99999"
zombie.mkdir(parents=True)
(zombie / "heat_complete.json").write_text(
json.dumps({"round_id": "99999", "screened": 5, "finalists": ["z"]}))
clock.t += 1800.0
cycle(loop)
assert prov.terminated == [] # zombie ignored: fleet intact
assert len(prov.live) == 2
# This round's OWN marker (matching base_seed) settles the heat for real.
real = tmp_path / "work" / "54321"
real.mkdir(parents=True)
(real / "heat_complete.json").write_text(
json.dumps({"round_id": "54321", "screened": 12, "finalists": ["hk"]}))
clock.t += 1800.0
cycle(loop)
assert prov.terminated == ["cascade-900-heat-0"] # heat down, final keeps running
assert "cascade-900-final-0" in prov.live
def test_manifest_tears_down_everything(tmp_path):
clock = Clock()
store = FakeStore()
loop, prov = _provisioned(tmp_path, clock=clock, store=store)
# Marker lands (teaches the provisioner base_seed 54321)…
d = tmp_path / "work" / "54321"
d.mkdir(parents=True)
(d / "heat_complete.json").write_text("{}")
clock.t += 1800.0
cycle(loop)
assert "cascade-900-final-0" in prov.live
# …then the round manifest publishes at the learned round id.
store.texts["manifests/round-54321.json"] = '{"round_id": "54321"}'
clock.t += 1800.0
cycle(loop)
assert prov.live == {}
with pytest.raises(RemoteDispatchError):
load_hosts(tmp_path / "hosts.toml") # cleared
assert load_state(tmp_path / "state.json").instances == ()
def test_stale_manifest_from_prior_run_does_not_tear_down(tmp_path):
# Same-round-id rerun: the PREVIOUS run's manifest is still published at
# round-<id>.json when the heat marker lands. That leftover must not read
# as round-over (2026-07-15: it killed both pods 21s after duel dispatch);
# only a NEW publish — different bytes at the same key — ends the round.
clock = Clock()
store = FakeStore({"manifests/round-54321.json":
'{"round_id": "54321", "contract_digest": "old"}'})
loop, prov = _provisioned(tmp_path, clock=clock, store=store)
d = tmp_path / "work" / "54321"
d.mkdir(parents=True)
(d / "heat_complete.json").write_text("{}")
clock.t += 1800.0
cycle(loop)
assert "cascade-900-final-0" in prov.live # stale: final survives
# The rerun finishes and republishes the SAME round id with new content.
store.texts["manifests/round-54321.json"] = (
'{"round_id": "54321", "contract_digest": "new"}')
clock.t += 1800.0
cycle(loop)
assert prov.live == {}
def test_relearn_of_same_round_resets_the_manifest_baseline(tmp_path):
# Same-round-id RELEARN (Round 8690400, 2026-07-23: fleet lost 31s after
# rent). _provision_round reset _learned_round_id but NOT the per-round
# manifest baseline (_round_baseline_for / _round_manifest_baseline). So a
# second provisioning of the same round kept the prior run's stale None
# baseline: when the marker re-taught the id, the previous run's manifest —
# still sitting at round-<id>.json — read as a FRESH publish and tore down
# the just-rented final pod.
clock = Clock()
store = FakeStore()
prov = FakeProvider("lium")
loop, _ = make_loop(tmp_path, providers={"lium": prov}, clock=clock, store=store)
# Round one: rent, then the marker teaches base_seed 54321 with NO manifest
# published yet → the per-round baseline is recorded as None.
cycle(loop)
marker_dir = tmp_path / "work" / "54321"
marker_dir.mkdir(parents=True)
(marker_dir / "heat_complete.json").write_text(
json.dumps({"round_id": "54321", "screened": 12, "finalists": ["hk"]}))
clock.t += 1800.0
cycle(loop)
assert loop._round_baseline_for == "54321"
assert loop._round_manifest_baseline is None # stale None seed
# That round finishes and publishes its manifest (now sitting at the key)…
store.texts["manifests/round-54321.json"] = (
'{"round_id": "54321", "contract_digest": "old"}')
clock.t += 1800.0
cycle(loop)
assert prov.live == {} # round one fully torn down
# …and the SAME round re-provisions (relearn): a fresh final pod is rented
# while the previous run's manifest still sits at round-54321.json.
clock.t += 1800.0
loop._provisioned_round = None
loop._provision_round(900)
if loop._rent_thread is not None:
loop._rent_thread.join(timeout=30)
assert "cascade-900-final-0" in prov.live # freshly rented
# The teardown sweep must NOT read the leftover manifest as a new publish.
loop._teardown_due_pods()
assert "cascade-900-final-0" in prov.live # fresh pod survives
def test_latest_pointer_change_also_ends_the_round(tmp_path):
# No marker ever seen (e.g. trainer crashed mid-write) — the latest.json
# baseline still detects "a manifest published after we rented".
clock = Clock()
store = FakeStore({"manifests/latest.json": '{"round_id": "111"}'})
loop, prov = _provisioned(tmp_path, clock=clock, store=store)
clock.t += 1800.0
cycle(loop)
assert len(prov.live) == 2 # unchanged pointer: no teardown
store.texts["manifests/latest.json"] = '{"round_id": "222"}'
cycle(loop)
assert prov.live == {}
def test_ttl_backstop_fires_without_any_signal(tmp_path):
clock = Clock()
loop, prov = _provisioned(tmp_path, clock=clock)
clock.t += 3 * 3600.0 - 1 # one second shy of 1 epoch
cycle(loop)
assert len(prov.live) == 2
clock.t += 1.0
cycle(loop)
assert prov.live == {} and len(prov.terminated) == 2
# ── restart + reconcile ──────────────────────────────────────────────────────
def test_restart_resumes_ledger_and_kills_orphans(tmp_path):
loop1, prov = _provisioned(tmp_path)
# A previous crash left a tagged pod the ledger never recorded.
prov.live["cascade-900-heat-zombie"] = PodAddress("10.0.0.99", 22)
loop2, plan_calls = make_loop(tmp_path, providers={"lium": prov}, block=885)
cycle(loop2)
assert "cascade-900-heat-zombie" in prov.terminated # orphan reconciled away
assert "cascade-900-heat-0" in prov.live # owned pods untouched
# The resumed ledger's round_id restores the rent-once latch too.
assert plan_calls == []
def test_reconcile_never_touches_untagged_pods(tmp_path):
prov = FakeProvider("lium")
prov.live["someone-elses-pod"] = PodAddress("10.9.9.9", 22)
loop, _ = make_loop(tmp_path, providers={"lium": prov}, block=100)
cycle(loop)
assert prov.terminated == []
def test_reconcile_never_touches_hand_rented_cascade_pods(tmp_path):
"""The 2026-07-13 incident: operators' pods legitimately share the
``cascade-`` prefix (cascade-worker, cascade-final-b) but are NOT the
provisioner's — only the full cascade-<round>-<stage> scheme is reapable."""
prov = FakeProvider("lium")
prov.live["cascade-worker"] = PodAddress("10.9.9.1", 22)
prov.live["cascade-final-b"] = PodAddress("10.9.9.2", 22)
prov.live["cascade-heat-2"] = PodAddress("10.9.9.3", 22) # no round id ⇒ not ours
prov.live["cascade-900-heat-zombie"] = PodAddress("10.9.9.4", 22) # ours, orphaned
loop, _ = make_loop(tmp_path, providers={"lium": prov}, block=100)
cycle(loop)
assert prov.terminated == ["cascade-900-heat-zombie"]
def test_dry_run_never_terminates_anything(tmp_path):
"""--dry-run must gate EVERY provider mutation, not just rentals: the
reaper (and teardown) once terminated a live pod during a dry-run demo."""
prov = FakeProvider("lium")
prov.live["cascade-900-heat-zombie"] = PodAddress("10.9.9.4", 22)
loop, _ = make_loop(tmp_path, providers={"lium": prov}, block=100)
loop.dry_run = True
cycle(loop)
assert prov.terminated == []
def test_plan_failure_retries_next_tick(tmp_path):
prov = FakeProvider("lium")
calls = []
def flaky_plan():
calls.append(1)
if len(calls) == 1:
raise RuntimeError("subtensor hiccup")
return dict(PLAN)
loop, _ = make_loop(tmp_path, providers={"lium": prov})
loop.plan_fn = flaky_plan
cycle(loop)
assert prov.launched == [] # no latch on plan failure…
cycle(loop)
assert len(calls) == 2 and len(prov.launched) == 2 # …so the next tick rents
# ── plan-output parsing + config validation (main.py pure parts) ─────────────
def test_parse_plan_output_takes_last_json_line():
text = ("btlogging noise\nWARNING something\n"
'{"old": true}\n' + json.dumps(PLAN) + "\ntrailing banner\n")
assert parse_plan_output(text)["eligible_challengers"] == 12
def test_parse_plan_output_rejects_json_free_text():
with pytest.raises(ProvisionError):
parse_plan_output("no json here\n")
def _raw_config(**over):
raw = {
"provisioner": {
"trigger_margin_blocks": 25,
"max_spend_per_round": 25.0,
"heat": {"sku": "NVIDIA RTX A6000", "gpus_per_pod": 8, "max_pods": 4,
"providers": ["lium", "shadeform"], "max_price_hr": 4.0},
"final": {"sku": "NVIDIA L40S", "gpus_per_pod": 2, "max_pods": 2,
"providers": ["lium", "shadeform"], "max_price_hr": 3.0},
}
}
raw["provisioner"].update(over)
return raw
def test_build_policy_happy():
p = build_policy(_raw_config(), epoch_blocks=900)
assert p.heat.gpus_per_pod == 8 and p.final.sku == "NVIDIA L40S"
assert p.heat.slot_overhead == pytest.approx(1.3) # default
assert p.ttl_epochs == 1
@pytest.mark.parametrize("bad", [
{"trigger_margin_blocks": 900}, # margin must be < epoch
{"trigger_margin_blocks": 0},
{"max_spend_per_round": 0},
{"heat": {"sku": "", "gpus_per_pod": 8, "max_pods": 4,
"providers": ["lium"], "max_price_hr": 4.0}}, # empty sku
{"heat": {"sku": "A6000", "gpus_per_pod": 0, "max_pods": 4,
"providers": ["lium"], "max_price_hr": 4.0}}, # gpus_per_pod >= 1
{"final": {"sku": "L40S", "gpus_per_pod": 2, "max_pods": 2,
"providers": ["lium"], "max_price_hr": 0}}, # price > 0
])
def test_build_policy_rejects_bad_config(bad):
with pytest.raises(ProvisionError):
build_policy(_raw_config(**bad), epoch_blocks=900)
def test_build_policy_requires_both_stage_tables():
raw = _raw_config()
del raw["provisioner"]["heat"]
with pytest.raises(ProvisionError):
build_policy(raw, epoch_blocks=900)
def test_hosts_file_round_trips_via_tomllib_too(tmp_path):
# Belt and braces: the published file is plain valid TOML, not just
# something load_hosts tolerates.
loop, _ = make_loop(tmp_path, providers={"lium": FakeProvider("lium")})
cycle(loop)
data = tomllib.loads((tmp_path / "hosts.toml").read_text(encoding="utf-8"))
assert len(data["host"]) == 10 # 8 heat GPUs + 2 final GPUs
# ── static hosts + bootstrap + unmanaged final ───────────────────────────────
def test_static_hosts_survive_every_publish_and_clear(tmp_path):
"""The operator's long-lived final pod must ride along on every render —
including failure paths that previously cleared the file outright."""
static = '[[host]]\nname = "cascade-final-b"\nhost = "216.81.245.151"\nstage = "final"\n'
prov = FakeProvider("lium")
loop, _ = make_loop(tmp_path, providers={"lium": prov}, block=880)
loop.static_hosts_text = static
cycle(loop) # provisions + publishes
text = (tmp_path / "hosts.toml").read_text()
assert "cascade-final-b" in text # static entry present
assert "cascade-900-heat" in text # dynamic heat pods present
# all-providers-down path: static fleet remains, never an empty file
prov2 = FakeProvider("lium", available=False)
loop2, _ = make_loop(tmp_path, providers={"lium": prov2}, block=1780)
loop2.static_hosts_text = static
cycle(loop2)
text2 = (tmp_path / "hosts.toml").read_text()
assert "cascade-final-b" in text2
assert "heat" not in text2.replace('stage = "final"', "")
def test_bootstrap_failure_replaces_pod_once(tmp_path):
calls = []
def flaky_bootstrap(addr, stage, provider=""):
calls.append(addr.ip)
return len(calls) > 1 # first pod fails, replacement passes
prov = FakeProvider("lium")
loop, _ = make_loop(tmp_path, providers={"lium": prov}, block=880)
loop.bootstrap = flaky_bootstrap
cycle(loop)
assert len(calls) >= 2 # failed pod → one replacement attempt
assert prov.terminated # the dud was terminated
def test_unmanaged_final_rents_no_final_pods(tmp_path):
from cascade.provision.policy import size_fleet
pol = _policy(final=StagePolicy(sku="NVIDIA L40S", gpus_per_pod=1, max_pods=0,
providers=("lium",), max_price_hr=3.0))
plan = size_fleet(12, 1, 0.5, 3.0, 0.75, pol)
assert plan.final.pods == 0 # stage unmanaged: static pods serve it
assert plan.heat.pods > 0
def test_publish_uses_per_provider_profile(tmp_path):
"""Shadeform VMs land as the 'shadeform' user under /home/shadeform — the
rendered hosts entries must carry that provider's paths, not lium's root."""
from dataclasses import replace as _replace
from cascade.provision.loop import PodProfile
prov = FakeProvider("shadeform")
loop, _ = make_loop(tmp_path, providers={"shadeform": prov})
loop.render = _replace(loop.render, profiles={"shadeform": PodProfile(
user="shadeform", workdir="/home/shadeform/cascade",
remote_python="/home/shadeform/cascade/.venv/bin/python")})
cycle(loop)
hosts = load_hosts(tmp_path / "hosts.toml")
assert hosts and all(h.user == "shadeform" for h in hosts)
assert all(h.workdir == "/home/shadeform/cascade" for h in hosts)
# ── SKU fallback (homogeneous per round) ─────────────────────────────────────
class ShapedProvider(FakeProvider):
"""A marketplace that only stocks specific (sku, gpus) shapes."""
def __init__(self, name, shapes, price=None):
super().__init__(name, price=price)
self.shapes = set(shapes) # {(market_sku, gpus)}
def available(self, sku, count, *, gpus=1):
return (sku, gpus) in self.shapes
def _fallback_policy():
from cascade.provision.policy import SkuCandidate
return _policy(heat=StagePolicy(
sku="NVIDIA GeForce RTX 4090", market_sku="RTX4090", gpus_per_pod=4,
max_pods=2, providers=("lium", "shadeform"), max_price_hr=2.60,
candidates=(SkuCandidate(sku="NVIDIA RTX A6000", market_sku="A6000",
gpus_per_pod=8, max_price_hr=4.50),)))
def test_sku_fallback_takes_first_candidate_with_capacity(tmp_path):
"""Primary 4x4090 out of stock everywhere; the 8xA6000 fallback (on the
second provider, at ITS shape: 1 pod for 7 slots) serves the whole heat."""
lium = ShapedProvider("lium", shapes=set()) # nothing
shade = ShapedProvider("shadeform", shapes={("A6000", 8)})
loop, _ = make_loop(tmp_path, providers={"lium": lium, "shadeform": shade},
policy=_fallback_policy())
cycle(loop)
assert shade.launched == ["cascade-900-heat-0"] # 1 × 8x pod
hosts = [h for h in load_hosts(tmp_path / "hosts.toml") if h.stage == "heat"]
assert len(hosts) == 8 # fallback fan-out
st = load_state(tmp_path / "state.json")
heat = [i for i in st.instances if i.stage == "heat"]
assert heat[0].sku == "NVIDIA RTX A6000" and heat[0].gpus == 8 # rented shape recorded
def test_sku_fallback_health_gate_gets_rented_sku(tmp_path):
"""The gate must assert the device that was ACTUALLY rented, not the primary."""
seen = []
def health(addr, stage, provider="", *, sku="", gpus=0, **kw):
seen.append((stage, sku, gpus))
return _report(ok=True)
shade = ShapedProvider("shadeform", shapes={("A6000", 8)})
loop, _ = make_loop(tmp_path, providers={"shadeform": shade},
policy=_fallback_policy(), health=health)
cycle(loop)
assert ("heat", "NVIDIA RTX A6000", 8) in seen
def test_sku_primary_wins_when_stocked(tmp_path):
lium = ShapedProvider("lium", shapes={("RTX4090", 4)})
shade = ShapedProvider("shadeform", shapes={("A6000", 8)})
loop, _ = make_loop(tmp_path, providers={"lium": lium, "shadeform": shade},
policy=_fallback_policy())
cycle(loop)
assert lium.launched == ["cascade-900-heat-0"] # 3 slots (12-field) @ 4x → 1 pod
assert shade.launched == []
# ── stale chain client (the silent no-trigger of 2026-07-14) ─────────────────
def test_frozen_block_rebuilds_chain_client_and_triggers(tmp_path):
"""A quietly-dead websocket keeps answering with a stale block; after
stale_block_after_s the loop must rebuild the client and see the real
height — otherwise it cycles forever and never rents."""
prov = FakeProvider("lium")
clock = Clock()
stale = FakeChain(700) # frozen far from the boundary
fresh = FakeChain(880) # the real height (in-window)
loop, plan_calls = make_loop(tmp_path, providers={"lium": prov}, clock=clock)
loop.chain_client = stale
loop.chain_client_factory = lambda: fresh
loop.stale_block_after_s = 300.0
cycle(loop) # block seen, baseline set
clock.t += 200.0
cycle(loop) # frozen, but not stale yet
assert plan_calls == [] and prov.launched == []
clock.t += 200.0 # now 400s frozen > 300s
cycle(loop) # rebuild → block 880 → trigger
assert plan_calls == [1]
assert prov.launched != []
def test_raising_chain_client_rebuilds_once(tmp_path):
class DeadChain:
def current_block(self):
raise ConnectionError("ws closed")
prov = FakeProvider("lium")
loop, plan_calls = make_loop(tmp_path, providers={"lium": prov})
loop.chain_client = DeadChain()
loop.chain_client_factory = lambda: FakeChain(880)
cycle(loop)
assert plan_calls == [1] # rebuilt and proceeded same cycle
# ── elastic eval pod (manifest-triggered; serves the validator) ──────────────
def _eval_policy(**kw):
kw.setdefault("eval", StagePolicy(sku="NVIDIA L40S", gpus_per_pod=1, max_pods=1,
providers=("lium",), max_price_hr=1.2))
return _policy(**kw)
def _eval_loop(tmp_path, *, store, prov=None, clock=None, dry_run=False,
receipt_prefix="receipts/5Val/", block=100):
"""A loop far outside the boundary margin (block=100): only the
manifest-triggered eval machinery can act."""
prov = prov or FakeProvider("lium")
loop, _ = make_loop(tmp_path, providers={"lium": prov}, block=block, clock=clock,
store=store, policy=_eval_policy(), dry_run=dry_run,
eval_hosts=tmp_path / "eval_hosts.toml",
receipt_prefix=receipt_prefix)
return loop, prov
def _join_eval(lp):
t = getattr(lp, "_eval_thread", None)
if t is not None:
t.join(timeout=10)
def test_new_manifest_rents_exactly_one_eval_pod_once(tmp_path):
store = FakeStore({"manifests/latest.json": '{"round_id": "111"}'})
loop, prov = _eval_loop(tmp_path, store=store)
cycle(loop)
_join_eval(loop)
assert prov.launched == ["cascade-111-eval-0"] # one pod, named for the round
# Round-trip through the trainer's REAL loader (the validator's parser).
hosts = load_hosts(tmp_path / "eval_hosts.toml")
assert [h.name for h in hosts] == ["cascade-111-eval-0"]
assert hosts[0].stage == "any" # matches the final/any filter
assert hosts[0].host == "10.0.0.1" and hosts[0].cuda_device == "0"
# …and through the validator's own lazy resolver.
from cascade.validator.eval_offload import make_eval_host_fn
resolved = make_eval_host_fn(tmp_path / "eval_hosts.toml")()
assert resolved is not None and resolved.name == "cascade-111-eval-0"
assert not (tmp_path / "hosts.toml").exists() # trainer's file untouched
st = load_state(tmp_path / "state.json")
assert st.last_evaled_round == "111" # persisted rent-once latch
assert {(i.stage, i.instance_id) for i in st.instances} == {("eval", "cascade-111-eval-0")}
cycle(loop)
_join_eval(loop) # same manifest: idempotent
cycle(loop)
_join_eval(loop)
assert prov.launched == ["cascade-111-eval-0"]
def test_eval_latch_persists_across_restart(tmp_path):
store = FakeStore({"manifests/latest.json": '{"round_id": "111"}'})
loop1, prov = _eval_loop(tmp_path, store=store)
cycle(loop1)
_join_eval(loop1)
loop2, prov2 = _eval_loop(tmp_path, store=store, prov=prov) # fresh process, same ledger
cycle(loop2)
_join_eval(loop2)
assert prov.launched == ["cascade-111-eval-0"] # no double rent
assert "cascade-111-eval-0" in prov.live # and the owned pod survives
def test_receipt_tears_down_eval_pod_and_clears_hosts_file(tmp_path):
clock = Clock()
store = FakeStore({"manifests/latest.json": '{"round_id": "111"}'})
loop, prov = _eval_loop(tmp_path, store=store, clock=clock)
cycle(loop)
_join_eval(loop)
clock.t += 600.0
cycle(loop)
_join_eval(loop)
assert "cascade-111-eval-0" in prov.live # no receipt yet: pod stays
# The validator publishes the round's receipt under ITS OWN prefix.
store.texts["receipts/5Val/round-111.json"] = '{"round_id": "111"}'
clock.t += 600.0
cycle(loop)
_join_eval(loop)
assert prov.terminated == ["cascade-111-eval-0"] and prov.live == {}
with pytest.raises(RemoteDispatchError):
load_hosts(tmp_path / "eval_hosts.toml") # cleared → validator falls local
st = load_state(tmp_path / "state.json")
assert st.instances == () and st.last_evaled_round == "111"
cycle(loop)
_join_eval(loop) # receipted round never re-rents
assert prov.launched == ["cascade-111-eval-0"]
def test_newer_manifest_replaces_the_eval_pod(tmp_path):
store = FakeStore({"manifests/latest.json": '{"round_id": "111"}'})
loop, prov = _eval_loop(tmp_path, store=store)
cycle(loop)
_join_eval(loop)
store.texts["manifests/latest.json"] = '{"round_id": "222"}'
cycle(loop)
_join_eval(loop)
# Round 111's evals are moot: its pod dies and round 222 gets its own —
# teardown runs before the eval check, so the two never coexist.
assert prov.terminated == ["cascade-111-eval-0"]
assert prov.launched == ["cascade-111-eval-0", "cascade-222-eval-0"]
hosts = load_hosts(tmp_path / "eval_hosts.toml")
assert [h.name for h in hosts] == ["cascade-222-eval-0"]
def test_eval_ttl_backstop_fires_without_receipt_or_newer_manifest(tmp_path):
clock = Clock()
store = FakeStore({"manifests/latest.json": '{"round_id": "111"}'})
loop, prov = _eval_loop(tmp_path, store=store, clock=clock)
cycle(loop)
_join_eval(loop)
clock.t += 3 * 3600.0 - 1 # one second shy of 1 epoch
cycle(loop)
_join_eval(loop)
assert "cascade-111-eval-0" in prov.live
clock.t += 1.0
cycle(loop)
_join_eval(loop)
assert prov.live == {} # TTL: silent validator ≠ bill
with pytest.raises(RemoteDispatchError):
load_hosts(tmp_path / "eval_hosts.toml")
def test_fresh_start_skips_a_round_already_receipted(tmp_path):
# Restarted between rounds: the latest manifest is old news and its receipt
# is up — renting would just buy a pod for the teardown sweep to kill.
store = FakeStore({"manifests/latest.json": '{"round_id": "111"}',
"receipts/5Val/round-111.json": "{}"})
loop, prov = _eval_loop(tmp_path, store=store)
cycle(loop)
assert prov.launched == []
assert load_state(tmp_path / "state.json").last_evaled_round == "111" # latched anyway
def test_absent_eval_policy_rents_nothing(tmp_path):
store = FakeStore({"manifests/latest.json": '{"round_id": "111"}'})
prov = FakeProvider("lium")
loop, _ = make_loop(tmp_path, providers={"lium": prov}, block=100, store=store,
eval_hosts=tmp_path / "eval_hosts.toml") # policy has no eval
cycle(loop)
assert prov.launched == [] and not (tmp_path / "eval_hosts.toml").exists()
def test_absent_eval_hosts_path_rents_nothing(tmp_path):
store = FakeStore({"manifests/latest.json": '{"round_id": "111"}'})
prov = FakeProvider("lium")
loop, _ = make_loop(tmp_path, providers={"lium": prov}, block=100, store=store,
policy=_eval_policy()) # nowhere to publish
cycle(loop)
assert prov.launched == []
def test_eval_dry_run_rents_nothing_and_touches_no_files(tmp_path):
store = FakeStore({"manifests/latest.json": '{"round_id": "111"}'})
loop, prov = _eval_loop(tmp_path, store=store, dry_run=True)
cycle(loop)
cycle(loop) # in-memory latch: no hammering
assert prov.launched == []
assert not (tmp_path / "eval_hosts.toml").exists()
assert load_state(tmp_path / "state.json") is None # dry-run writes no ledger
def test_eval_skipped_when_round_budget_is_committed(tmp_path):
# The round breaker historically ignored eval; now eval respects what the
# boundary stages already committed — skipping is cheap (local CPU evals).
store = FakeStore({"manifests/latest.json": '{"round_id": "111"}'})
loop, prov = _eval_loop(tmp_path, store=store)
loop._committed = {"heat": 30.0} # boundary stages hold the cap
cycle(loop)
_join_eval(loop)
assert prov.launched == [] # skipped, not rented
assert load_state(tmp_path / "state.json").last_evaled_round == "111" # latched
def test_eval_no_capacity_degrades_and_latches(tmp_path):
store = FakeStore({"manifests/latest.json": '{"round_id": "111"}'})
loop, prov = _eval_loop(tmp_path, store=store, prov=FakeProvider("lium", available=False))
cycle(loop)
cycle(loop) # latched: no 30s hammering
assert prov.launched == [] # validator evals run local
assert load_state(tmp_path / "state.json").last_evaled_round == "111"
def test_eval_and_trainer_stages_coexist_in_separate_files(tmp_path):
"""A manifest lands while the boundary trigger fires: the eval pod goes to
eval_hosts.toml, the trainer fleet to hosts.toml — never cross-published."""
store = FakeStore({"manifests/latest.json": '{"round_id": "111"}'})
loop, prov = _eval_loop(tmp_path, store=store, block=880)
cycle(loop)
assert set(prov.launched) == {
"cascade-111-eval-0", "cascade-900-heat-0", "cascade-900-final-0"}
assert all(h.stage in ("heat", "final") for h in load_hosts(tmp_path / "hosts.toml"))
assert [h.name for h in load_hosts(tmp_path / "eval_hosts.toml")] == ["cascade-111-eval-0"]
def test_reaper_accepts_eval_pods_as_self_named(tmp_path):
from cascade.provision.loop import is_provisioner_pod_name
assert is_provisioner_pod_name("cascade-900-eval-0")
assert is_provisioner_pod_name("cascade-900-eval-r0-0") # replacement suffix
assert not is_provisioner_pod_name("cascade-eval-1") # no round id ⇒ not ours
prov = FakeProvider("lium")
prov.live["cascade-900-eval-zombie"] = PodAddress("10.9.9.5", 22)
loop, _ = make_loop(tmp_path, providers={"lium": prov}, block=100)
cycle(loop)
assert prov.terminated == ["cascade-900-eval-zombie"] # orphan eval pod reaped
def test_build_policy_eval_table_is_optional():
p = build_policy(_raw_config(), epoch_blocks=900)
assert p.eval is None # pre-eval configs unchanged
raw = _raw_config(eval={"sku": "NVIDIA L40S", "gpus_per_pod": 1, "max_pods": 1,
"providers": ["lium"], "max_price_hr": 1.2})
p = build_policy(raw, epoch_blocks=900)
assert p.eval.sku == "NVIDIA L40S" and p.eval.max_pods == 1
def test_build_policy_rejects_bad_eval_table():
raw = _raw_config(eval={"sku": "", "gpus_per_pod": 1, "max_pods": 1,
"providers": ["lium"], "max_price_hr": 1.2})
with pytest.raises(ProvisionError):
build_policy(raw, epoch_blocks=900)
def test_heartbeat_logs_at_cycle_start(tmp_path, caplog):
"""Liveness must not depend on any network phase completing — the
heartbeat fires at cycle START (starved twice on 2026-07-14 when it
lived behind reconcile/eval polls)."""
import logging
loop, _ = make_loop(tmp_path, block=100)
with caplog.at_level(logging.INFO, logger="cascade.provision.loop"):
cycle(loop)
assert any("heartbeat: cycle start" in r.message for r in caplog.records)
def test_hung_chain_client_hits_deadline_and_rebuilds(tmp_path):
"""A websocket that HANGS (not raises) must not block the loop — the
deadline converts the hang into a rebuild. Four rental windows died to
silent chain stalls before this."""
import threading
class HangingChain:
def current_block(self):
threading.Event().wait(30) # would block half the cycle
prov = FakeProvider("lium")
loop, plan_calls = make_loop(tmp_path, providers={"lium": prov})
loop.chain_client = HangingChain()
loop.chain_client_factory = lambda: FakeChain(880)
# shrink the deadline for test speed
orig = loop._with_deadline
loop_cls = type(loop)
try:
loop_cls._with_deadline = staticmethod(
lambda fn, seconds: orig(fn, 0.2 if seconds >= 60 else seconds))
cycle(loop)
finally:
loop_cls._with_deadline = staticmethod(orig)
assert plan_calls == [1] # rebuilt within the same cycle and triggered
def test_on_cycle_hook_heals_stripped_logging(tmp_path):
"""bittensor strips handlers + raises level on named loggers at chain
connect (verified live). The loop must invoke the self-heal hook every
cycle so logging survives arbitrary re-nuking."""
import logging
lg = logging.getLogger("cascade")
calls = []
def heal():
calls.append(1)
lg.setLevel(logging.INFO)
loop, _ = make_loop(tmp_path, block=100)
loop.on_cycle = heal
lg.setLevel(logging.CRITICAL) # simulate the nuke
cycle(loop)
assert calls and lg.getEffectiveLevel() == logging.INFO
def test_heal_rebuilds_closed_handlers(tmp_path, monkeypatch):
"""bittensor can close stripped handlers' streams — re-attaching the same
object then fails silently on every emit. The heal must rebuild."""
import logging
import sys
monkeypatch.chdir(tmp_path)
monkeypatch.setattr(sys, "argv", ["cascade-provisioner"])
from cascade.provision import main as pmain
fmt = logging.Formatter("%(message)s")
state = {"stream": None, "file": None}
def _alive(h):
stream = getattr(h, "stream", None)
return h is not None and stream is not None and not getattr(stream, "closed", True)
def ensure():
lg = logging.getLogger("cascade-test-heal")
if not _alive(state["file"]):
state["file"] = logging.FileHandler(tmp_path / "svc.log")
state["file"].setFormatter(fmt)
if state["file"] not in lg.handlers:
lg.addHandler(state["file"])
lg.setLevel(logging.INFO)
lg.propagate = False
lg = logging.getLogger("cascade-test-heal")
ensure()
lg.info("one")
state["file"].close() # simulate bittensor closing it
lg.removeHandler(state["file"]) # and stripping it
ensure() # heal must REBUILD, not re-attach
lg.info("two")
for h in lg.handlers:
h.flush()
text = (tmp_path / "svc.log").read_text()
assert "one" in text and "two" in text
assert pmain # import sanity
def test_eval_rent_does_not_block_the_cycle(tmp_path, store_with_manifest=None):
"""An eval pod's boot can take 15+ min — it must run OFF the loop thread
(a blocking boot swallowed a heat-trigger window on 2026-07-14)."""
import threading
import time as _time
gate = threading.Event()
class SlowBootProvider(FakeProvider):
def wait_ready(self, pod_id, *, timeout):
gate.wait(5) # simulates the 900s boot wait
return True
prov = SlowBootProvider("lium")
store = FakeStore({"manifests/latest.json": '{"round_id": "424242"}'})
loop, _ = _eval_loop(tmp_path, store=store, prov=prov)
t0 = _time.monotonic()
cycle(loop) # must return promptly
took = _time.monotonic() - t0
gate.set()
assert took < 2.0, f"cycle blocked {took:.1f}s on the eval boot"