Skip to content

test(e2e): let the cache-rebuild gate run on reasoning checkpoints - #145

Open
rakhimovv wants to merge 2 commits into
FlashML-org:mainfrom
rakhimovv:test/cache-rebuild-reasoning-checkpoints
Open

test(e2e): let the cache-rebuild gate run on reasoning checkpoints#145
rakhimovv wants to merge 2 commits into
FlashML-org:mainfrom
rakhimovv:test/cache-rebuild-reasoning-checkpoints

Conversation

@rakhimovv

@rakhimovv rakhimovv commented Aug 24, 2026

Copy link
Copy Markdown

Small test-only fix found while running the pre-PR gates from tests/README.md on a fresh box. Thanks for documenting them — they were easy to find and run.

Revised after review. The first version asserted on content or reasoning_content and raised max_tokens to 256. Both are replaced by one better assertion; see "The fix". An inaccurate claim about enable_thinking is corrected too.

What breaks

tests/README.md calls tests/e2e/test_cache_rebuild.py the primary gate for the rebuild
path and asks contributors to run it before opening a PR, pointing
FREETOKEN_REBUILD_TEST_MODEL at "a SMALL local model dir". On openai/gpt-oss-20b — the
smallest checkpoint on the known-good list in docs/models.md — the gate cannot get past its
first assertion.

_generate() posts to /v1/chat/completions and returns message["content"]; line 145 then
asserts that string is non-empty:

assert _generate(base)  # a baseline generation, before anything is torn down

gpt-oss's Harmony channels are not gated by enable_thinking, so it emits an analysis trace
regardless, that text lands in reasoning_content, and content can come back empty on a
perfectly healthy engine.

Observed

With FREETOKEN_REBUILD_TEST_MODEL pointed at openai/gpt-oss-20b, three consecutive runs
failed identically:

E           AssertionError: assert ''
E            +  where '' = _generate('http://127.0.0.1:45979')
tests/e2e/test_cache_rebuild.py:145: AssertionError

The failure is on the baseline generation — the one taken before anything is torn down —
so no rebuild is ever attempted. The path the gate exists to protect is not exercised at all;
the gate just reports red.

It is a boundary case rather than a structural impossibility: the same request against a
separately booted server returned content: 'ready' with completion_tokens: 31 of the 32
allowed plus 83 characters of reasoning_content. One token of headroom, and which side of
the line you land on depends on the prompt and the engine's configuration.

The gate was not masking an engine bug

Worth confirming, since a red gate that starts passing after a test edit deserves suspicion.
Against a live ft serve on gpt-oss-20b the rebuild path itself is healthy:

$ ft ctl cache --moe 512 --wait 300
status=ok

The pool table then showed moe 512 slots (lru, 66.7%) 6.3 GiB, down from
768 slots ... 9.5 GiB, with last rebuild ok, and
ft ctl generate "2+2=" --max-tokens 12 still produced text afterwards.

The fix

Return usage.completion_tokens instead of the reply text, and assert it is non-zero.

Which field the text lands in depends on the checkpoint's reasoning protocol and on which
parser --reasoning-parser auto picked, neither of which is what this gate is about. Reading
either channel keeps that coupling; counting tokens drops it. 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 it — which is the kind of independent expectation
tests/README.md asks for.

max_tokens stays at 32. The observed failure was content being empty, not the budget
being short, and 32 tokens is plenty to show decode ran. That 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 an aborted request keeps
decoding server-side, which the rebuild's if_idle mode refuses rather than waits for. The
gate is advertised as cheap; this keeps it that way.

enable_thinking: False is left in place. It is read by the qwen/glm/gemma/dsv4 templates
and is simply inert for gpt-oss; Jinja ignores undeclared variables, so it cannot harm a
checkpoint that does not read it.

Before / after

Before, three runs in a row, all AssertionError: assert '' at
tests/e2e/test_cache_rebuild.py:145. After, same command and same checkpoint:

1 passed in 37.52s

Command:

FREETOKEN_REBUILD_TEST_MODEL=/path/to/gpt-oss-20b CUDA_VISIBLE_DEVICES=1 \
  python -m pytest tests/e2e/test_cache_rebuild.py -v

The test fails before this change and passes after it, on the same checkpoint and the same
command.

Tested on

  • GPU: NVIDIA H100 80GB HBM3 (sm_90), one GPU of four in the box
  • NVIDIA driver: 580.126.16
  • CPU: Intel Xeon Platinum 8462Y+, 56 threads; 2015 GiB system RAM
  • OS: Linux 5.15.0-157-generic
  • CUDA toolkit: nvcc release 13.1, V13.1.115
  • torch 2.11.0+cu130, Python 3.12.13
  • FreeToken 0.1.2, at commit bd372b6
  • Checkpoint: openai/gpt-oss-20b, revision 6cee5e81ee83917806bbde320786a8fb61efebee
  • Install: uv pip install -e ".[accel,dev]"

What was wrong in the first version

  • "enable_thinking is a Qwen chat-template knob." It is read by the qwen/glm/gemma/dsv4
    templates — tokenizer/effort.py broadcasts it for all four. gpt-oss is the family it is
    inert for, which is the opposite of what I wrote.
  • "the whole 32-token budget goes to the reasoning trace ... content comes back empty"
    contradicted my own datum three paragraphs later (content: 'ready' at 31/32 tokens).
    Restated above as the boundary case it is.
  • The max_tokens bump was unevidenced — with the assertion fixed, 32 tokens passes.
  • "the Qwen-family checkpoints the gate was originally exercised with" — dropped; the file
    has a single commit in its history and names no checkpoint family.

_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.
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant