feat: checkpoint batching and release-gate stories

This commit is contained in:
Dobromir Popov
2026-07-16 17:24:36 +03:00
parent 737bade989
commit 02b3709311
18 changed files with 4580 additions and 1 deletions

View File

@@ -0,0 +1,472 @@
"""Continuous batching and bounded admission (DGR-012).
These tests drive the node-local continuous-batching scheduler with the *same*
pure-numpy KV-cached dense-Llama reference the Hot KV State manager uses
(DGR-007), imported from ``test_hot_kv_state``. That keeps the whole gate
deterministic, download-free, GPU-free, and API-credit-free while exercising the
real KV isolation path (``KvBoundaryAdapter`` + ``HotKvStateManager``) rather than
a mock.
Coverage maps to the story's acceptance criteria:
* bounded admission against weight/KV/scratch/queue budgets,
* compatible decode steps batched with per-session positions/outputs preserved,
* prefill never starving in-flight decode (explicit decode-first policy),
* backpressure when the bounded queue is full,
* capability telemetry reporting every required signal,
* a deterministic 1/2/4/8 concurrency sweep showing saturation and no
cross-session corruption.
"""
from __future__ import annotations
import numpy as np
import pytest
from meshnet_node.hot_kv_state import (
HotKvStateConfig,
HotKvStateManager,
KvBoundaryAdapter,
kv_recipe_for,
)
from meshnet_node.batch_scheduler import (
AdmissionReason,
ContinuousBatchScheduler,
GenerationRequest,
KvBatchEngine,
NodeBudget,
Phase,
run_concurrency_sweep,
)
# Reuse the certified numpy dense-Llama reference and shard from the DGR-007 gate.
from test_hot_kv_state import _KvDenseLlama, _KvReferenceShard
# --------------------------------------------------------------------------- #
# Helpers.
# --------------------------------------------------------------------------- #
class _FakeClock:
def __init__(self) -> None:
self.now = 0.0
def __call__(self) -> float:
return self.now
def advance(self, delta: float) -> None:
self.now += delta
def _make_engine(
model: _KvDenseLlama | None = None,
*,
config: HotKvStateConfig | None = None,
) -> KvBatchEngine:
"""A full-shard KV batch engine over the deterministic numpy dense-Llama."""
model = model or _KvDenseLlama()
shard = _KvReferenceShard(model, 0, model.n_layers - 1)
manager = HotKvStateManager(kv_recipe_for(shard), config=config)
adapter = KvBoundaryAdapter(shard, manager)
return KvBatchEngine(adapter)
def _reference_tokens(model: _KvDenseLlama, prompt, n_new: int) -> list[int]:
return model.stateless_greedy(list(prompt), n_new)
def _generation(session_id: str, prompt, n_new: int, epoch: int = 0) -> GenerationRequest:
return GenerationRequest(
session_id=session_id,
route_epoch=epoch,
prompt_token_ids=tuple(prompt),
max_new_tokens=n_new,
)
# --------------------------------------------------------------------------- #
# Bounded admission (weight / KV / scratch / queue budgets).
# --------------------------------------------------------------------------- #
def test_admission_respects_active_scratch_and_queue_budgets():
"Admission fills active slots, queues the overflow, then rejects a full queue.\n\nTags: node, scheduler, admission"
engine = _make_engine()
budget = NodeBudget(
max_active_sessions=2,
scratch_bytes_per_session=1,
scratch_budget_bytes=2, # scratch also caps at 2 concurrent
max_queue_depth=1,
max_batch_size=2,
)
scheduler = ContinuousBatchScheduler(engine, budget)
a = scheduler.submit(_generation("a", [1, 2, 3], 4))
b = scheduler.submit(_generation("b", [4, 5, 6], 4))
assert a.reason is AdmissionReason.ADMITTED
assert b.reason is AdmissionReason.ADMITTED
# Two active slots full -> the next goes to the bounded queue.
c = scheduler.submit(_generation("c", [7, 8, 9], 4))
assert c.reason is AdmissionReason.QUEUED
# Queue depth 1 is now full -> backpressure rejection.
d = scheduler.submit(_generation("d", [1, 1, 1], 4))
assert d.reason is AdmissionReason.REJECTED_QUEUE_FULL
assert d.rejected
telem = scheduler.telemetry()
assert telem.active_sessions == 2
assert telem.queue_depth == 1
assert telem.rejected_admissions_total == 1
assert telem.rejected_by_reason[AdmissionReason.REJECTED_QUEUE_FULL.value] == 1
def test_admission_rejects_a_session_that_cannot_fit_the_kv_budget():
"A generation whose whole KV cannot fit the node budget is rejected up front.\n\nTags: node, scheduler, admission"
engine = _make_engine()
per_token = engine._manager.recipe.bytes_per_token()
# Budget holds only 3 positions; a prompt(4)+7 new = 10 final positions cannot fit.
budget = NodeBudget(kv_budget_bytes=per_token * 3)
scheduler = ContinuousBatchScheduler(engine, budget)
decision = scheduler.submit(_generation("big", [1, 2, 3, 4], 7))
assert decision.reason is AdmissionReason.REJECTED_KV_BUDGET
assert scheduler.telemetry().rejected_admissions_total == 1
def test_admission_rejects_when_per_session_scratch_exceeds_budget():
"A per-session scratch larger than the whole scratch envelope is rejected.\n\nTags: node, scheduler, admission"
engine = _make_engine()
budget = NodeBudget(scratch_bytes_per_session=1024, scratch_budget_bytes=512)
scheduler = ContinuousBatchScheduler(engine, budget)
decision = scheduler.submit(_generation("s", [1, 2], 2))
assert decision.reason is AdmissionReason.REJECTED_SCRATCH_BUDGET
def test_duplicate_submission_is_rejected():
"Submitting a session id that is already scheduled is rejected as a duplicate.\n\nTags: node, scheduler, admission"
engine = _make_engine()
scheduler = ContinuousBatchScheduler(engine, NodeBudget(max_active_sessions=4))
assert scheduler.submit(_generation("dup", [1, 2], 3)).reason is AdmissionReason.ADMITTED
assert scheduler.submit(_generation("dup", [3, 4], 3)).reason is AdmissionReason.REJECTED_DUPLICATE
def test_weight_budget_is_reported_in_telemetry():
"The resident weight footprint is surfaced as a capability signal.\n\nTags: node, scheduler, telemetry"
engine = _make_engine()
budget = NodeBudget(weight_bytes=123_456)
scheduler = ContinuousBatchScheduler(engine, budget)
assert scheduler.telemetry().weight_bytes == 123_456
# --------------------------------------------------------------------------- #
# Continuous batching preserves per-session positions and outputs.
# --------------------------------------------------------------------------- #
def test_batched_decode_preserves_per_session_positions_and_outputs():
"Four sessions batched together each reproduce their own stateless tokens.\n\nTags: node, scheduler, batching"
model = _KvDenseLlama()
engine = _make_engine(model)
budget = NodeBudget(max_active_sessions=4, max_batch_size=4, max_queue_depth=4)
scheduler = ContinuousBatchScheduler(engine, budget)
prompts = {
"alpha": [1, 2, 3, 4],
"bravo": [40, 39, 2, 15],
"charlie": [7, 7, 7, 7],
"delta": [31, 5, 18, 22],
}
n_new = 10
references = {sid: _reference_tokens(model, p, n_new) for sid, p in prompts.items()}
# The four references must diverge, else "no cross-talk" would be vacuous.
assert len({tuple(v) for v in references.values()}) == 4
for sid, prompt in prompts.items():
assert scheduler.submit(_generation(sid, prompt, n_new)).running
outputs = scheduler.run_to_completion()
for sid in prompts:
assert outputs[sid] == references[sid], sid
telem = scheduler.telemetry()
# A genuine batch formed: at least one decode tick carried all four sessions.
assert telem.batch_occupancy_max == 4
assert telem.completed_sessions == 4
assert telem.active_sessions == 0
def test_positions_are_isolated_across_different_prompt_lengths():
"Sessions with different prompt lengths keep independent positions when batched.\n\nTags: node, scheduler, batching"
model = _KvDenseLlama()
engine = _make_engine(model)
scheduler = ContinuousBatchScheduler(
engine, NodeBudget(max_active_sessions=3, max_batch_size=3, max_queue_depth=3)
)
jobs = {
"short": ([5], 6),
"medium": ([2, 9, 14], 6),
"long": ([1, 2, 3, 4, 5, 6, 7], 6),
}
refs = {sid: _reference_tokens(model, p, n) for sid, (p, n) in jobs.items()}
for sid, (prompt, n) in jobs.items():
scheduler.submit(_generation(sid, prompt, n))
outputs = scheduler.run_to_completion()
for sid in jobs:
assert outputs[sid] == refs[sid], sid
# --------------------------------------------------------------------------- #
# Prefill does not starve decode.
# --------------------------------------------------------------------------- #
def test_prefill_does_not_starve_in_flight_decode():
"A burst of new prefills never stalls an already-decoding session.\n\nTags: node, scheduler, fairness"
model = _KvDenseLlama()
engine = _make_engine(model)
# One prefill per tick (budget == a single prompt) so prefill is throttled and
# we can observe that decode still advances every tick.
budget = NodeBudget(
max_active_sessions=8,
max_batch_size=8,
max_queue_depth=8,
scratch_bytes_per_session=1,
scratch_budget_bytes=8,
max_prefill_tokens_per_tick=4,
)
scheduler = ContinuousBatchScheduler(engine, budget)
# Session A starts and prefills on tick 1.
scheduler.submit(_generation("A", [3, 14, 1, 5], 12))
scheduler.run_tick()
a_state = scheduler.session_result("A")
assert a_state.phase is Phase.DECODING
a_len = len(a_state.generated)
assert a_len == 1
# Burst of new work arrives while A is decoding.
for sid in ("B", "C", "D", "E"):
scheduler.submit(_generation(sid, [2, 27, 18, 4], 12))
# Over the next few ticks A must decode on *every* tick (never starved),
# while at most one new session prefills per tick (prefill is bounded).
prefill_counts = []
for _ in range(4):
report = scheduler.run_tick()
new_a_len = len(scheduler.session_result("A").generated)
assert new_a_len == a_len + 1, "decode of A stalled while prefills were pending"
a_len = new_a_len
assert "A" in report.decoded
prefill_counts.append(len(report.prefilled))
assert max(prefill_counts) <= 1, "prefill was not bounded per tick"
def test_decode_first_policy_is_explicit_in_a_single_tick():
"In one tick decode of active sessions precedes prefill of new ones.\n\nTags: node, scheduler, fairness"
model = _KvDenseLlama()
engine = _make_engine(model)
scheduler = ContinuousBatchScheduler(
engine,
NodeBudget(max_active_sessions=4, max_batch_size=4, max_queue_depth=4,
scratch_bytes_per_session=1, scratch_budget_bytes=4),
)
scheduler.submit(_generation("live", [1, 2, 3], 8))
scheduler.run_tick() # 'live' prefills, now decoding
scheduler.submit(_generation("fresh", [9, 8, 7], 8))
report = scheduler.run_tick()
assert "live" in report.decoded
assert "fresh" in report.prefilled
# --------------------------------------------------------------------------- #
# Backpressure and bounded memory.
# --------------------------------------------------------------------------- #
def test_backpressure_signals_when_queue_full_then_recovers():
"A full queue rejects new work; a completed session frees a slot for the queue.\n\nTags: node, scheduler, backpressure"
engine = _make_engine()
budget = NodeBudget(
max_active_sessions=1,
max_batch_size=1,
max_queue_depth=1,
scratch_bytes_per_session=1,
scratch_budget_bytes=1,
)
scheduler = ContinuousBatchScheduler(engine, budget)
assert scheduler.submit(_generation("first", [1, 2], 2)).running
assert scheduler.submit(_generation("second", [3, 4], 2)).reason is AdmissionReason.QUEUED
# Both a slot and the queue are full now.
assert scheduler.submit(_generation("third", [5, 6], 2)).reason is AdmissionReason.REJECTED_QUEUE_FULL
# Drain 'first'; the queued 'second' must be pulled into the freed slot.
scheduler.run_to_completion()
outputs = scheduler.outputs()
assert set(outputs) == {"first", "second"}
def test_completed_sessions_release_kv_so_growth_is_bounded():
"Finished sessions release their KV, so total KV returns to zero.\n\nTags: node, scheduler, backpressure"
engine = _make_engine()
scheduler = ContinuousBatchScheduler(
engine, NodeBudget(max_active_sessions=2, max_batch_size=2, max_queue_depth=8)
)
for sid in ("a", "b", "c", "d"):
scheduler.submit(_generation(sid, [1, 2, 3], 4))
scheduler.run_to_completion()
telem = scheduler.telemetry()
assert telem.kv_total_bytes == 0, "KV not released after completion"
assert telem.active_sessions == 0
assert telem.completed_sessions == 4
# --------------------------------------------------------------------------- #
# Telemetry.
# --------------------------------------------------------------------------- #
def test_telemetry_reports_every_required_signal():
"The capability snapshot reports sessions, queue, batch, KV, rates, rejections.\n\nTags: node, scheduler, telemetry"
model = _KvDenseLlama()
engine = _make_engine(model)
clock = _FakeClock()
budget = NodeBudget(max_active_sessions=2, max_batch_size=2, max_queue_depth=1)
scheduler = ContinuousBatchScheduler(engine, budget, clock=clock)
scheduler.submit(_generation("x", [1, 2, 3], 4))
scheduler.submit(_generation("y", [4, 5, 6], 4))
scheduler.submit(_generation("z", [7, 8, 9], 4)) # queued
rejected = scheduler.submit(_generation("w", [1, 1, 1], 4)) # queue full
assert rejected.rejected
clock.advance(1.0)
scheduler.run_tick() # both prefill
clock.advance(1.0)
scheduler.run_tick() # both decode as a batch of 2
clock.advance(2.0)
telem = scheduler.telemetry()
snap = telem.to_dict()
for key in (
"active_sessions", "queue_depth", "batch_occupancy_last",
"batch_occupancy_avg", "batch_occupancy_max", "weight_bytes",
"kv_total_bytes", "kv_budget_bytes", "kv_pressure",
"scratch_used_bytes", "scratch_budget_bytes", "scratch_pressure",
"prefill_tokens_total", "decode_tokens_total",
"prefill_tokens_per_sec", "decode_tokens_per_sec",
"rejected_admissions_total", "rejected_by_reason",
"completed_sessions", "ticks",
):
assert key in snap, key
assert telem.batch_occupancy_max == 2
assert telem.prefill_tokens_total == 6 # two prompts of length 3
assert telem.decode_tokens_total == 2 # one batched decode step, two sessions
assert telem.rejected_admissions_total == 1
# Rates are deterministic under the injected clock: 4 seconds elapsed.
assert telem.decode_tokens_per_sec == pytest.approx(2 / 4.0)
assert telem.prefill_tokens_per_sec == pytest.approx(6 / 4.0)
assert 0.0 < telem.kv_pressure <= 1.0
# --------------------------------------------------------------------------- #
# Concurrency 1/2/4/8 sweep: saturation and no corruption.
# --------------------------------------------------------------------------- #
def test_concurrency_sweep_identifies_saturation_without_corruption():
"A 1/2/4/8 sweep raises batch occupancy, cuts ticks, and never corrupts output.\n\nTags: node, scheduler, benchmark"
model = _KvDenseLlama()
prompts = {
"s0": [1, 2, 3, 4], "s1": [5, 6, 7, 8], "s2": [9, 10, 11, 12],
"s3": [13, 14, 15, 16], "s4": [17, 18, 19, 20], "s5": [21, 22, 23, 24],
"s6": [25, 26, 27, 28], "s7": [29, 30, 31, 32],
}
n_new = 8
requests = [_generation(sid, p, n_new) for sid, p in prompts.items()]
sweep = run_concurrency_sweep(
lambda: _make_engine(model),
requests,
concurrency_levels=(1, 2, 4, 8),
)
assert sweep.corruption_free
assert [r.concurrency for r in sweep.results] == [1, 2, 4, 8]
# No session hit a cache miss (budgets are sized to never evict here).
assert all(r.cache_misses == 0 for r in sweep.results)
assert all(r.rejected_admissions == 0 for r in sweep.results)
# Each per-session stream matches the serialized (concurrency-1) reference.
for sid, prompt in prompts.items():
assert list(sweep.reference_outputs[sid]) == _reference_tokens(model, prompt, n_new)
occupancies = [r.avg_batch_occupancy for r in sweep.results]
ticks = [r.ticks for r in sweep.results]
tokens_per_tick = [r.tokens_per_tick for r in sweep.results]
# Batching packs more sessions per decode step as concurrency rises, so
# average occupancy strictly increases and total ticks strictly decrease.
assert occupancies == sorted(occupancies) and len(set(occupancies)) == 4
assert ticks == sorted(ticks, reverse=True) and len(set(ticks)) == 4
# Aggregate work per tick rises with concurrency (the throughput win).
assert tokens_per_tick == sorted(tokens_per_tick)
# For eight equal-length jobs the node keeps saturating up to the top level.
assert sweep.saturation_concurrency == 8
# The report is JSON-safe for durable evidence.
import json
json.dumps(sweep.to_dict())
def test_concurrency_sweep_saturates_below_max_when_load_is_small():
"With fewer concurrent jobs than slots, saturation is found below the top level.\n\nTags: node, scheduler, benchmark"
model = _KvDenseLlama()
# Only three jobs: at concurrency 4 and 8 the batch can never exceed 3, so
# occupancy stops rising past the load and saturation is detected early.
requests = [
_generation("j0", [1, 2, 3], 6),
_generation("j1", [4, 5, 6], 6),
_generation("j2", [7, 8, 9], 6),
]
sweep = run_concurrency_sweep(
lambda: _make_engine(model), requests, concurrency_levels=(1, 2, 4, 8)
)
assert sweep.corruption_free
assert sweep.saturation_concurrency <= 4
# Levels at or above the load size share the same occupancy/tick profile.
top = [r for r in sweep.results if r.concurrency >= 4]
assert len({r.ticks for r in top}) == 1
# --------------------------------------------------------------------------- #
# Engine contract guards.
# --------------------------------------------------------------------------- #
def test_kv_batch_engine_requires_a_full_shard():
"The batch engine rejects a partial (non head+tail) shard.\n\nTags: node, scheduler"
model = _KvDenseLlama()
head = _KvReferenceShard(model, 0, 2) # head only, not tail
manager = HotKvStateManager(kv_recipe_for(head))
adapter = KvBoundaryAdapter(head, manager)
with pytest.raises(Exception):
KvBatchEngine(adapter)
def test_run_to_completion_is_bounded_against_misconfiguration():
"run_to_completion raises rather than looping forever when work cannot drain.\n\nTags: node, scheduler"
engine = _make_engine()
scheduler = ContinuousBatchScheduler(
engine, NodeBudget(max_active_sessions=1, max_batch_size=1, max_queue_depth=4)
)
scheduler.submit(_generation("only", [1, 2], 3))
# A tiny explicit tick ceiling is exceeded deterministically.
with pytest.raises(Exception):
scheduler.run_to_completion(max_ticks=1)

View File

@@ -0,0 +1,611 @@
"""Bounded failure, cancellation, and restart semantics (DGR-013).
These tests drive the hardened per-session decode stream with the *same*
pure-numpy KV-cached dense-Llama reference the Hot KV State manager (DGR-007) and
the continuous-batch scheduler (DGR-012) use, imported from ``test_hot_kv_state``.
The whole matrix stays deterministic, download-free, GPU-free, and API-credit-free
while exercising the real KV isolation path (``KvBoundaryAdapter`` +
``HotKvStateManager``) rather than a mock.
Coverage maps to the story's acceptance criteria:
* deadlines and heartbeat/health loss terminate blocked stream operations,
* cancellation propagates across every Shard and releases KV + queued buffers,
* duplicate steps are idempotent; uncertain mutations are never replayed silently,
* alpha failover restarts from token zero rather than importing unverified KV,
* worker death / stream reset / malformed bundle / stale epoch / cache miss,
* billing/work records distinguish completed, cancelled, failed, and unverified.
"""
from __future__ import annotations
import json
import numpy as np
import pytest
from meshnet_node.batch_scheduler import (
ContinuousBatchScheduler,
DoneReason,
GenerationRequest,
KvBatchEngine,
NodeBudget,
)
from meshnet_node.boundary_adapter import BoundaryBundle, BoundaryContractError
from meshnet_node.hot_kv_state import (
CacheMiss,
CacheMissReason,
HotKvStateConfig,
HotKvStateManager,
KvBoundaryAdapter,
StaleRouteEpochError,
kv_recipe_for,
)
from meshnet_node.failure_semantics import (
CancellationToken,
DeadlineGuard,
FailureKind,
HardenedSessionRunner,
IdempotencyLedger,
OperationCancelled,
RestartController,
ShardCancellationGroup,
StepKey,
StreamTerminated,
UncertainMutationError,
WorkLedger,
WorkRecord,
WorkStatus,
classify_exception,
work_status_for,
)
# Reuse the certified numpy dense-Llama reference and shard from the DGR-007 gate.
from test_hot_kv_state import _KvDenseLlama, _KvReferenceShard
# --------------------------------------------------------------------------- #
# Helpers.
# --------------------------------------------------------------------------- #
class _FakeClock:
def __init__(self) -> None:
self.now = 0.0
def __call__(self) -> float:
return self.now
def advance(self, delta: float) -> None:
self.now += delta
class _FaultyShard(_KvReferenceShard):
"""A full-shard reference that raises on the Nth ``run_layers_cached`` call.
``run_layers_cached`` is invoked once per stream step, so ``fail_at_call=k``
simulates a worker dying at step ``k-1`` (calls are 1-indexed). The call
counter persists across attempts, so a restart on a fresh epoch keeps counting
and does not re-trip the same fault.
"""
def __init__(self, model, start, end, *, fail_at_call=None, error=None):
super().__init__(model, start, end)
self._fail_at_call = fail_at_call
self._error = error or RuntimeError("worker died mid-step")
self.calls = 0
def run_layers_cached(self, hidden, *, positions, past_kv):
self.calls += 1
if self._fail_at_call is not None and self.calls == self._fail_at_call:
raise self._error
return super().run_layers_cached(hidden, positions=positions, past_kv=past_kv)
def _make_adapter(model=None, *, config=None, shard=None):
"""A full-shard KV boundary adapter over the deterministic numpy dense-Llama."""
model = model or _KvDenseLlama()
shard = shard or _KvReferenceShard(model, 0, model.n_layers - 1)
manager = HotKvStateManager(kv_recipe_for(shard), config=config)
adapter = KvBoundaryAdapter(shard, manager)
return adapter
def _generation(session_id, prompt, n_new, epoch=0):
return GenerationRequest(
session_id=session_id,
route_epoch=epoch,
prompt_token_ids=tuple(prompt),
max_new_tokens=n_new,
)
# --------------------------------------------------------------------------- #
# Happy path (the baseline the failure paths deviate from).
# --------------------------------------------------------------------------- #
def test_clean_run_matches_stateless_reference_and_is_billable():
"A clean stream reproduces the stateless tokens and records completed work.\n\nTags: node, failure, billing"
model = _KvDenseLlama()
adapter = _make_adapter(model)
runner = HardenedSessionRunner(adapter)
prompt = [1, 2, 3, 4]
n_new = 8
outcome = runner.run(_generation("clean", prompt, n_new))
assert outcome.status is WorkStatus.COMPLETED
assert list(outcome.tokens) == model.stateless_greedy(prompt, n_new)
record = runner.work_ledger.records_for("clean")[0]
assert record.billable
assert record.tokens == n_new
assert runner.work_ledger.billable_tokens() == n_new
# --------------------------------------------------------------------------- #
# Deadlines and heartbeat/health loss terminate blocked operations.
# --------------------------------------------------------------------------- #
def test_deadline_terminates_a_blocked_stream_and_releases_kv():
"A deadline reached mid-stream terminates the run and frees its KV.\n\nTags: node, failure, deadline"
clock = _FakeClock()
adapter = _make_adapter()
manager = adapter.manager
runner = HardenedSessionRunner(adapter, clock=clock)
# Each step advances the clock by 1.0; the deadline fires at t=3.
def before_step(_step):
clock.advance(1.0)
outcome = runner.run(
_generation("slow", [5, 6, 7], 20),
deadline=3.0,
before_step=before_step,
)
assert outcome.status is WorkStatus.FAILED
assert outcome.failure_kind is FailureKind.DEADLINE_EXCEEDED
# The stream did not hang and did not finish: only the steps before the
# deadline committed, and the session's KV was released.
assert outcome.token_count < 20
assert isinstance(manager.resolve("slow", 0), CacheMiss)
def test_heartbeat_loss_terminates_a_blocked_stream():
"Losing the peer heartbeat past the timeout terminates the stream.\n\nTags: node, failure, heartbeat"
clock = _FakeClock()
adapter = _make_adapter()
runner = HardenedSessionRunner(adapter, clock=clock)
def before_step(_step):
clock.advance(1.0)
# Heartbeats stop arriving after step 2; with a timeout of 1.5 the gap grows
# past the bound and the stream is terminated (health loss).
def heartbeat(step):
return step < 2
outcome = runner.run(
_generation("hb", [9, 8, 7], 20),
heartbeat_timeout=1.5,
heartbeat=heartbeat,
before_step=before_step,
)
assert outcome.status is WorkStatus.FAILED
assert outcome.failure_kind is FailureKind.HEARTBEAT_LOST
assert outcome.token_count < 20
def test_deadline_guard_reports_remaining_and_resets_on_heartbeat():
"The guard exposes remaining time and a heartbeat resets the health timer.\n\nTags: node, failure, deadline"
clock = _FakeClock()
guard = DeadlineGuard(deadline=10.0, heartbeat_timeout=2.0, clock=clock)
guard.start()
guard.check()
assert guard.remaining() == 10.0
clock.advance(1.5)
guard.heartbeat() # health refreshed at t=1.5
clock.advance(1.0) # gap since heartbeat is 1.0 < 2.0
guard.check()
clock.advance(2.5) # gap since heartbeat is now 3.5 > 2.0
with pytest.raises(StreamTerminated) as exc:
guard.check()
assert exc.value.kind is FailureKind.HEARTBEAT_LOST
# --------------------------------------------------------------------------- #
# Cancellation propagates across shards and releases KV + queued buffers.
# --------------------------------------------------------------------------- #
def test_cancellation_token_terminates_stream_and_releases_kv():
"A client cancel mid-stream stops the run and releases the session KV.\n\nTags: node, failure, cancel"
adapter = _make_adapter()
manager = adapter.manager
token = CancellationToken()
runner = HardenedSessionRunner(adapter)
# Cancel after two steps have run.
def before_step(step):
if step == 2:
token.cancel("client-hangup")
outcome = runner.run(
_generation("cancelme", [1, 2, 3], 20),
cancel_token=token,
before_step=before_step,
)
assert outcome.status is WorkStatus.CANCELLED
assert outcome.failure_kind is FailureKind.CANCELLED
assert outcome.token_count == 2 # steps 0 and 1 committed before the cancel
assert isinstance(manager.resolve("cancelme", 0), CacheMiss)
def test_shard_cancellation_group_releases_every_shard_and_queued_buffers():
"One cancel frees KV on every node-local shard and releases queued buffers.\n\nTags: node, failure, cancel"
model = _KvDenseLlama()
# Three node-local shards of the same route, each with its own KV manager.
managers = []
for start, end in ((0, 1), (2, 3), (4, 5)):
shard = _KvReferenceShard(model, start, end)
mgr = HotKvStateManager(kv_recipe_for(shard))
mgr.open("route", 0) # each holds live state for the session
managers.append(mgr)
released_buffers = []
group = ShardCancellationGroup("route", 0)
for mgr in managers:
group.add_shard(mgr)
group.add_queued_buffer(lambda: released_buffers.append("bundle-a"))
group.add_queued_buffer(lambda: released_buffers.append("bundle-b"))
outcome = group.cancel()
assert outcome.shards_released == 3
assert outcome.buffers_released == 2
assert released_buffers == ["bundle-a", "bundle-b"]
# Every shard's KV is gone: a lookup now yields an explicit released miss.
for mgr in managers:
miss = mgr.resolve("route", 0)
assert isinstance(miss, CacheMiss)
assert miss.reason is CacheMissReason.RELEASED
# Cancellation is idempotent.
again = group.cancel()
assert again.shards_released == 0
assert again.buffers_released == 0
def test_scheduler_cancel_drains_queue_and_releases_active_kv():
"The scheduler cancel drops queued work and frees an active session's KV.\n\nTags: node, scheduler, cancel"
model = _KvDenseLlama()
shard = _KvReferenceShard(model, 0, model.n_layers - 1)
manager = HotKvStateManager(kv_recipe_for(shard))
engine = KvBatchEngine(KvBoundaryAdapter(shard, manager))
scheduler = ContinuousBatchScheduler(
engine, NodeBudget(max_active_sessions=1, max_batch_size=1, max_queue_depth=4)
)
assert scheduler.submit(_generation("active", [1, 2, 3], 8)).running
assert scheduler.submit(_generation("waiting", [4, 5, 6], 8)).reason.value == "queued"
scheduler.run_tick() # 'active' prefills and starts decoding, holding KV
# Cancel the queued one: it leaves the queue without ever taking a slot.
assert scheduler.cancel("waiting") is True
# Cancel the active one: its KV is released and it is recorded as cancelled.
assert scheduler.cancel("active") is True
assert manager.total_bytes == 0
telem = scheduler.telemetry()
assert telem.cancelled_sessions == 2
assert telem.completed_sessions == 0
assert telem.active_sessions == 0
assert telem.queue_depth == 0
# Cancelling an unknown / already-finished session is a no-op.
assert scheduler.cancel("active") is False
assert scheduler.cancel("never-seen") is False
def test_scheduler_cancel_rejects_a_completed_reason():
"cancel() refuses a non-terminal reason so completed work is never faked.\n\nTags: node, scheduler, cancel"
model = _KvDenseLlama()
shard = _KvReferenceShard(model, 0, model.n_layers - 1)
manager = HotKvStateManager(kv_recipe_for(shard))
engine = KvBatchEngine(KvBoundaryAdapter(shard, manager))
scheduler = ContinuousBatchScheduler(engine)
scheduler.submit(_generation("x", [1, 2], 4))
with pytest.raises(Exception):
scheduler.cancel("x", reason=DoneReason.COMPLETED)
# --------------------------------------------------------------------------- #
# Idempotency: duplicate steps are no-ops; uncertain mutations never replay.
# --------------------------------------------------------------------------- #
def test_duplicate_step_delivery_is_idempotent_no_remutation():
"Replaying a committed step returns the recorded token without re-mutating KV.\n\nTags: node, failure, idempotency"
ledger = IdempotencyLedger()
key = StepKey("s", 0, 5)
disposition = ledger.begin(key)
assert disposition.fresh
ledger.commit(key, 42)
# A duplicate delivery of the same step returns the recorded token and is a
# no-op — the caller must not re-run the mutation.
replay = ledger.begin(key)
assert replay.duplicate
assert replay.token == 42
def test_idempotent_run_replays_tokens_without_advancing_kv():
"Re-running a completed stream on the same ledger/epoch re-mutates nothing.\n\nTags: node, failure, idempotency"
model = _KvDenseLlama()
adapter = _make_adapter(model)
ledger = IdempotencyLedger()
runner = HardenedSessionRunner(adapter, idempotency=ledger)
request = _generation("idem", [3, 1, 4], 6)
first = runner.run(request)
assert first.status is WorkStatus.COMPLETED
kv_len_after_first = adapter.manager.get("idem", 0).seq_len
# A duplicate delivery of the entire stream: every step is a committed
# duplicate, so the runner replays the identical tokens and the KV length is
# unchanged (no double-append).
second = runner.run(request)
assert second.status is WorkStatus.COMPLETED
assert list(second.tokens) == list(first.tokens)
assert adapter.manager.get("idem", 0).seq_len == kv_len_after_first
def test_uncertain_mutation_is_never_replayed_silently():
"A step marked uncertain refuses a silent replay; it must be verified/restarted.\n\nTags: node, failure, idempotency"
ledger = IdempotencyLedger()
key = StepKey("s", 0, 3)
ledger.begin(key)
ledger.mark_uncertain(key, "worker died before ack")
# Replaying an uncertain mutation is refused rather than silently re-applied.
with pytest.raises(UncertainMutationError):
ledger.begin(key)
assert ledger.has_uncertain()
def test_in_flight_duplicate_is_treated_as_uncertain():
"A second begin before commit is refused (concurrent duplicate is unverified).\n\nTags: node, failure, idempotency"
ledger = IdempotencyLedger()
key = StepKey("s", 0, 1)
ledger.begin(key) # in-flight, not yet committed
with pytest.raises(UncertainMutationError):
ledger.begin(key)
# --------------------------------------------------------------------------- #
# Worker death, stream reset, malformed bundle, stale epoch, cache miss.
# --------------------------------------------------------------------------- #
def test_worker_death_midstream_is_unverified_and_marks_step_uncertain():
"A worker dying mid-step yields unverified work and an unreplayable step.\n\nTags: node, failure, worker-death"
model = _KvDenseLlama()
# Fail on the 3rd step call (step index 2), after two tokens committed.
shard = _FaultyShard(model, 0, model.n_layers - 1, fail_at_call=3)
adapter = _make_adapter(model, shard=shard)
ledger = IdempotencyLedger()
runner = HardenedSessionRunner(adapter, idempotency=ledger)
outcome = runner.run(_generation("dead", [1, 2, 3], 8))
assert outcome.status is WorkStatus.UNVERIFIED
assert outcome.failure_kind is FailureKind.WORKER_DEATH
assert outcome.token_count == 2 # the two committed steps
assert not outcome.completed
# The failed step is uncertain and can never be silently replayed.
assert ledger.has_uncertain()
with pytest.raises(UncertainMutationError):
ledger.begin(StepKey("dead", 0, 2))
# KV was released on failure.
assert isinstance(adapter.manager.resolve("dead", 0), CacheMiss)
def test_stream_reset_is_restartable_failure():
"A stream reset injected mid-stream fails the run as a restartable transport loss.\n\nTags: node, failure, stream-reset"
adapter = _make_adapter()
runner = HardenedSessionRunner(adapter)
def before_step(step):
if step == 2:
raise StreamTerminated(FailureKind.STREAM_RESET, "peer reset the stream")
outcome = runner.run(_generation("reset", [1, 2, 3], 8), before_step=before_step)
assert outcome.status is WorkStatus.FAILED
assert outcome.failure_kind is FailureKind.STREAM_RESET
assert outcome.restartable
def test_malformed_bundle_is_classified_and_does_not_corrupt_kv():
"A malformed activation bundle is rejected and leaves the KV context empty.\n\nTags: node, failure, malformed-bundle"
model = _KvDenseLlama()
mid = _KvReferenceShard(model, 2, 3) # middle range: not head, not tail
manager = HotKvStateManager(kv_recipe_for(mid))
adapter = KvBoundaryAdapter(mid, manager)
assert not adapter.is_head and not adapter.is_tail
# A bundle that hands over at the wrong layer is malformed.
bad = BoundaryBundle(
architecture_adapter=adapter.architecture.adapter,
schema_version=adapter.architecture.boundary_schema_version,
tensor_name=adapter.architecture.boundary_tensor_name,
residual=np.zeros((1, 3, model.hidden), dtype=np.float32),
positions=np.arange(3, dtype=np.int64)[None, :],
next_layer=adapter.start_layer + 5, # wrong handover layer
normalized=False,
)
with pytest.raises(BoundaryContractError) as exc:
adapter.prefill("mal", 0, boundary=bad)
assert classify_exception(exc.value) is FailureKind.MALFORMED_BUNDLE
# The malformed step never appended KV: the context is empty, not corrupted.
assert manager.get("mal", 0).seq_len == 0
def test_stale_epoch_reference_is_rejected_and_classified():
"A reference to a superseded epoch is rejected as stale, never silently reused.\n\nTags: node, failure, stale-epoch"
model = _KvDenseLlama()
adapter = _make_adapter(model)
manager = adapter.manager
manager.open("sess", 5) # current epoch is now 5
with pytest.raises(StaleRouteEpochError) as exc:
manager.resolve("sess", 4) # epoch 4 is stale
assert classify_exception(exc.value) is FailureKind.STALE_EPOCH
# Driving the hardened runner on the stale epoch fails closed as STALE_EPOCH.
runner = HardenedSessionRunner(adapter)
outcome = runner.run(_generation("sess", [1, 2, 3], 4, epoch=3))
assert outcome.status is WorkStatus.FAILED
assert outcome.failure_kind is FailureKind.STALE_EPOCH
def test_cache_miss_midstream_is_restartable():
"A KV eviction mid-stream surfaces an explicit cache miss the head can restart.\n\nTags: node, failure, cache-miss"
adapter = _make_adapter()
manager = adapter.manager
runner = HardenedSessionRunner(adapter)
# Evict the session's KV just before step 3's decode.
def before_step(step):
if step == 3:
manager.release("evict", 0)
outcome = runner.run(_generation("evict", [1, 2, 3], 10), before_step=before_step)
assert outcome.failure_kind is FailureKind.CACHE_MISS
assert outcome.restartable
assert outcome.token_count == 3 # steps 0..2 committed before the eviction
# --------------------------------------------------------------------------- #
# Alpha failover: restart from token zero, never import unverified KV.
# --------------------------------------------------------------------------- #
def test_alpha_failover_restarts_from_token_zero_and_completes():
"A transient worker death fails over to a fresh epoch and reproduces the tokens.\n\nTags: node, failure, failover"
model = _KvDenseLlama()
# Die on the 3rd step of the first attempt; the persistent call counter means
# the restart (which keeps counting) does not re-trip the fault.
shard = _FaultyShard(model, 0, model.n_layers - 1, fail_at_call=3)
adapter = _make_adapter(model, shard=shard)
manager = adapter.manager
runner = HardenedSessionRunner(adapter)
controller = RestartController([manager])
prompt = [7, 3, 9, 1]
n_new = 6
result = runner.run_with_failover(
_generation("alpha", prompt, n_new, epoch=0), controller, max_restarts=2
)
assert result.completed
assert result.restarts == 1
# The restart began on a fresh epoch and reproduced the full stateless stream
# from token zero — no half-computed KV was imported.
assert result.outcome.route_epoch == 1
assert list(result.outcome.tokens) == model.stateless_greedy(prompt, n_new)
# The failed epoch's KV is gone and the epoch is now stale.
with pytest.raises(StaleRouteEpochError):
manager.resolve("alpha", 0)
# First attempt was unverified, the restart completed: only the restart bills.
statuses = [a.status for a in result.attempts]
assert statuses == [WorkStatus.UNVERIFIED, WorkStatus.COMPLETED]
assert runner.work_ledger.billable_tokens() == n_new
def test_failover_refuses_to_import_unverified_kv():
"assert_fresh_start fails closed if any shard still holds new-epoch KV.\n\nTags: node, failure, failover"
model = _KvDenseLlama()
adapter = _make_adapter(model)
manager = adapter.manager
controller = RestartController([manager])
new_epoch = controller.failover("s", 0)
assert new_epoch == 1
# A clean fresh start passes.
controller.assert_fresh_start("s", new_epoch)
# If unverified KV were present under the new epoch, the guard refuses it.
manager.open("s", new_epoch)
manager.append(
"s",
new_epoch,
{i: (np.zeros((1, model.n_heads, model.head_dim), dtype=np.float32),
np.zeros((1, model.n_heads, model.head_dim), dtype=np.float32))
for i in range(model.n_layers)},
)
with pytest.raises(Exception):
controller.assert_fresh_start("s", new_epoch)
def test_non_restartable_failure_is_not_retried():
"A deterministic failure (deadline) returns immediately without a restart.\n\nTags: node, failure, failover"
clock = _FakeClock()
adapter = _make_adapter()
runner = HardenedSessionRunner(adapter, clock=clock)
controller = RestartController([adapter.manager])
def before_step(_step):
clock.advance(1.0)
result = runner.run_with_failover(
_generation("bounded", [1, 2, 3], 20),
controller,
max_restarts=3,
deadline=2.0,
before_step=before_step,
)
assert not result.completed
assert result.restarts == 0
assert result.outcome.failure_kind is FailureKind.DEADLINE_EXCEEDED
# --------------------------------------------------------------------------- #
# Billing / work records distinguish completed, cancelled, failed, unverified.
# --------------------------------------------------------------------------- #
def test_work_ledger_distinguishes_all_four_statuses():
"The work ledger keeps completed/cancelled/failed/unverified distinct.\n\nTags: node, failure, billing"
ledger = WorkLedger()
ledger.record(WorkRecord("a", 0, WorkStatus.COMPLETED, tokens=8))
ledger.record(WorkRecord("b", 0, WorkStatus.CANCELLED, tokens=3,
failure_kind=FailureKind.CANCELLED))
ledger.record(WorkRecord("c", 0, WorkStatus.FAILED, tokens=1,
failure_kind=FailureKind.DEADLINE_EXCEEDED))
ledger.record(WorkRecord("d", 0, WorkStatus.UNVERIFIED, tokens=2,
failure_kind=FailureKind.WORKER_DEATH))
counts = ledger.counts_by_status()
assert counts == {
"completed": 1, "cancelled": 1, "failed": 1, "unverified": 1,
}
# Only completed work is billable — cancelled/failed/unverified tokens are
# recorded for observability but never charged.
assert ledger.billable_tokens() == 8
assert [r.session_id for r in ledger.billable_records()] == ["a"]
# JSON-safe for durable evidence.
payload = ledger.to_dict()
assert payload["billable_tokens"] == 8
assert payload["counts_by_status"]["unverified"] == 1
json.dumps(payload)
def test_work_status_and_classification_mapping():
"Failure kinds map to the right billing status and exception classes.\n\nTags: node, failure, billing"
assert work_status_for(FailureKind.CANCELLED) is WorkStatus.CANCELLED
assert work_status_for(FailureKind.WORKER_DEATH) is WorkStatus.UNVERIFIED
# A stream reset detected at a step boundary is a certain failure (nothing
# committed for that step) — only an unexpected mid-step error is unverified.
assert work_status_for(FailureKind.STREAM_RESET) is WorkStatus.FAILED
assert work_status_for(FailureKind.DEADLINE_EXCEEDED) is WorkStatus.FAILED
assert work_status_for(FailureKind.MALFORMED_BUNDLE) is WorkStatus.FAILED
assert work_status_for(FailureKind.STALE_EPOCH) is WorkStatus.FAILED
assert work_status_for(FailureKind.CACHE_MISS) is WorkStatus.FAILED
assert classify_exception(OperationCancelled()) is FailureKind.CANCELLED
assert classify_exception(StaleRouteEpochError("x")) is FailureKind.STALE_EPOCH
assert classify_exception(BoundaryContractError("x")) is FailureKind.MALFORMED_BUNDLE
assert classify_exception(RuntimeError("boom")) is FailureKind.WORKER_DEATH
assert (
classify_exception(StreamTerminated(FailureKind.HEARTBEAT_LOST))
is FailureKind.HEARTBEAT_LOST
)