Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 15 additions & 7 deletions tests/e2e/test_cache_rebuild.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,16 @@ def _wait_until_serving(base: str, proc: subprocess.Popen, deadline: float) -> N
raise TimeoutError("server never reached the serving state")


def _generate(base: str) -> str:
"""A short non-thinking completion — the point is that the engine still runs, not what it says."""
def _generate(base: str) -> int:
"""Tokens the engine decoded -- the point is that it still runs, not what it says.

Counts rather than reads the reply. Which field the text lands in depends on the
checkpoint: ``enable_thinking`` is read by the qwen/glm/gemma/dsv4 templates and is
inert for gpt-oss, whose Harmony channels are not gated by it, so its trace goes to
``reasoning_content`` and ``content`` can be empty on a perfectly healthy engine.
``usage.completion_tokens`` is accumulated from the scheduler's own acks, upstream of
every reasoning parser, so it says decode ran without depending on any of that.
"""
code, body = _post(
base,
"/v1/chat/completions",
Expand All @@ -104,7 +112,7 @@ def _generate(base: str) -> str:
},
)
assert code == 200, body
return str(body["choices"][0]["message"]["content"])
return int(body["usage"]["completion_tokens"])


@pytest.mark.skipif(not torch.cuda.is_available(), reason="cache rebuild e2e needs CUDA")
Expand Down Expand Up @@ -142,7 +150,7 @@ def test_cache_rebuild_resizes_a_live_engine_and_keeps_serving(tmp_path):

geometry = _get(base, "/v1/cache/status")["geometry"]
assert geometry["num_pages"] == BOOT_PAGES
assert _generate(base) # a baseline generation, before anything is torn down
assert _generate(base) > 0 # a baseline generation, before anything is torn down

# 1. Grow the KV pool. The reply carries the geometry the engine actually landed on.
code, reply = _post(base, "/v1/cache/rebuild", {"num_pages": GROWN_PAGES})
Expand All @@ -153,7 +161,7 @@ def test_cache_rebuild_resizes_a_live_engine_and_keeps_serving(tmp_path):
assert status["geometry"]["num_pages"] == GROWN_PAGES
assert status["last_rebuild"]["status"] == "ok"
# Graphs were re-captured against the new pool: decoding still works.
assert _generate(base)
assert _generate(base) > 0

# 2. The second axis, if this checkpoint has a GDN state pool. Resizing it moves a
# different pool through the same teardown/re-capture path.
Expand All @@ -166,7 +174,7 @@ def test_cache_rebuild_resizes_a_live_engine_and_keeps_serving(tmp_path):
assert geometry["num_mamba_slots"] == grown_slots
# A pool-only resize must not disturb the pool the previous rebuild grew.
assert geometry["num_pages"] == GROWN_PAGES
assert _generate(base)
assert _generate(base) > 0

# 3. An unfittable target is rejected BEFORE anything is freed, and the engine that was
# serving a moment ago keeps serving on its old cache.
Expand All @@ -177,7 +185,7 @@ def test_cache_rebuild_resizes_a_live_engine_and_keeps_serving(tmp_path):
status = _get(base, "/v1/cache/status")
assert status["state"] == "serving"
assert status["geometry"]["num_pages"] == GROWN_PAGES # untouched by the rejection
assert _generate(base)
assert _generate(base) > 0
finally:
proc.terminate()
try:
Expand Down