From 535f28af4a7603b2fde5e7860f935e2cd574b868 Mon Sep 17 00:00:00 2001 From: Ruslan Rakhimov Date: Mon, 24 Aug 2026 17:51:52 +0300 Subject: [PATCH 1/2] test(e2e): let the cache-rebuild gate run on reasoning checkpoints _generate() asks for 32 tokens and asserts a non-empty message["content"]. enable_thinking is a Qwen chat-template knob, so on a checkpoint that reasons by its own protocol the flag is a no-op: the budget goes to the reasoning trace, the text lands in reasoning_content, and content comes back empty with finish_reason "length". The gate then fails on the baseline generation, before any rebuild is attempted, so the path it exists to protect is never exercised. Against openai/gpt-oss-20b this reproduced on three consecutive runs. Raise the budget to 256 tokens and accept either channel, which matches the helper's own docstring: the point is that the engine still runs, not what it says. enable_thinking is left in place, still correct for the Qwen-family checkpoints the gate was written against. --- tests/e2e/test_cache_rebuild.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/tests/e2e/test_cache_rebuild.py b/tests/e2e/test_cache_rebuild.py index ed17e240..dcb4805d 100644 --- a/tests/e2e/test_cache_rebuild.py +++ b/tests/e2e/test_cache_rebuild.py @@ -91,20 +91,27 @@ def _wait_until_serving(base: str, proc: subprocess.Popen, deadline: float) -> N def _generate(base: str) -> str: - """A short non-thinking completion — the point is that the engine still runs, not what it says.""" + """A short completion — the point is that the engine still runs, not what it says. + + ``enable_thinking`` is a Qwen chat-template knob and does nothing on a + checkpoint that reasons by its own protocol (gpt-oss and friends), so the + budget has to cover a reasoning trace and either channel counts as alive -- + otherwise the gate fails on a healthy engine before it has torn anything down. + """ code, body = _post( base, "/v1/chat/completions", { "model": "rebuild-test", "messages": [{"role": "user", "content": "Reply with the single word: ready"}], - "max_tokens": 32, + "max_tokens": 256, "temperature": 0.0, "chat_template_kwargs": {"enable_thinking": False}, }, ) assert code == 200, body - return str(body["choices"][0]["message"]["content"]) + message = body["choices"][0]["message"] + return str(message.get("content") or message.get("reasoning_content") or "") @pytest.mark.skipif(not torch.cuda.is_available(), reason="cache rebuild e2e needs CUDA") From 2a070e39ce0f31d3c6b692cc695b3a3202f2770b Mon Sep 17 00:00:00 2001 From: Ruslan Rakhimov Date: Mon, 24 Aug 2026 20:58:23 +0300 Subject: [PATCH 2/2] test(e2e): count decoded tokens instead of guessing the reply's channel Reworks the previous commit after review. Asserting on content-or-reasoning_content still ties the gate to which reasoning parser --reasoning-parser auto happened to pick and to how that parser splits channels, none of which is the thing under test. usage.completion_tokens is accumulated from the scheduler's own acks, upstream of every parser, so it says decode ran without depending on any of it -- and tests/README asks for expectations checked against something independent. That also makes the max_tokens bump unnecessary: the observed failure was content being empty, not the budget being short, and 32 tokens is enough to show decode ran. Reverting it keeps the gate cheap, which matters because _generate is called four times and the offload/cpu backends this project exists for decode at 1-20 tok/s, where 256 tokens per call can cross _post's 180 s timeout and cascade into the rebuild's if_idle refusal. Corrects the docstring too: enable_thinking is read by the qwen/glm/gemma/ dsv4 templates, not Qwen alone, and gpt-oss is inert to it because its Harmony channels are not gated by that flag. --- tests/e2e/test_cache_rebuild.py | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/tests/e2e/test_cache_rebuild.py b/tests/e2e/test_cache_rebuild.py index dcb4805d..a6494f33 100644 --- a/tests/e2e/test_cache_rebuild.py +++ b/tests/e2e/test_cache_rebuild.py @@ -90,13 +90,15 @@ 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 completion — the point is that the engine still runs, not what it says. - - ``enable_thinking`` is a Qwen chat-template knob and does nothing on a - checkpoint that reasons by its own protocol (gpt-oss and friends), so the - budget has to cover a reasoning trace and either channel counts as alive -- - otherwise the gate fails on a healthy engine before it has torn anything down. +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, @@ -104,14 +106,13 @@ def _generate(base: str) -> str: { "model": "rebuild-test", "messages": [{"role": "user", "content": "Reply with the single word: ready"}], - "max_tokens": 256, + "max_tokens": 32, "temperature": 0.0, "chat_template_kwargs": {"enable_thinking": False}, }, ) assert code == 200, body - message = body["choices"][0]["message"] - return str(message.get("content") or message.get("reasoning_content") or "") + return int(body["usage"]["completion_tokens"]) @pytest.mark.skipif(not torch.cuda.is_available(), reason="cache rebuild e2e needs CUDA") @@ -149,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}) @@ -160,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. @@ -173,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. @@ -184,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: