Skip to content

fix(specprefill): preserve the full static system/tool prefix - #3756

Open
tc3oliver wants to merge 1 commit into
jundot:mainfrom
tc3oliver:fix/specprefill-static-prefix-boundary
Open

tc3oliver wants to merge 1 commit into
jundot:mainfrom
tc3oliver:fix/specprefill-static-prefix-boundary

Conversation

@tc3oliver

@tc3oliver tc3oliver commented Sep 19, 2026 •

Copy link
Copy Markdown
Contributor

specprefill_system_end is the full prompt length minus a render without system/developer messages, and the scheduler uses it as a prefix length. On Qwen3.5-family templates that second render gets a default reasoning-effort system block, so the result comes out short.

The two engines are off in different ways. BatchedEngine passes tools to both renders, so the tool block cancels out. VLMBatchedEngine renders without tools or chat_template_kwargs, so it counts the tool block but still loses the default block. Whatever falls outside the boundary can be sparsified.

Measured on main 2165e75 with the Qwen3.8-27B tokenizer only, one short system message and one user turn. "User content starts at" is where the user's text begins in the prompt.

tools user content starts at main batched main VLM branch (both)
0 53 8 8 53
1 317 8 272 317
4 518 8 473 518
16 1334 8 1289 1334

Fix

#4994 added a two-probe render for the shared system prompt, so I reused it instead of adding a second one. The render moves out of _system_prefix_ids into _render_system_prefix_ids, and _system_prefix_ids behaves as before. _specprefill_static_prefix_end then keeps the part of that render the real prompt also starts with. It can't run past the first turn, because the two probes diverge there.

With no leading system message neither main nor this branch sets a boundary. I left that alone.

A longer protected prefix leaves fewer tokens to score, so a request close to the SpecPrefill threshold may now prefill normally. That comes from reading policy.py, I didn't measure it.

This costs one more render per request while SpecPrefill is on. Time spent in _inject_specprefill_system_end on the VLM engine, about 2.7k prompt tokens with 16 tools: 0.08 ms on main, 2.5 ms here. With 64 tools and about 6k tokens it's 5 ms.

Tests: the two updated test_chat_injects_specprefill_system_end tests fail on main on the boundary itself (assert 8 < 8 and a KeyError because it isn't set). The new tests/test_specprefill_boundary.py runs both engines against a small fake template that adds a default system block, but on main it only fails on the changed signature. I ran the tests matching specprefill, boundary, preflight, vlm or batched: 2208 passed, 118 skipped. I didn't run this on a loaded model.

@tc3oliver

Copy link
Copy Markdown
Contributor Author

CI failures match the current upstream main baseline after 4c8f634f. The same three tests fail on main and on this PR, on Python 3.11, 3.12 and 3.13:

  • tests/test_mlx_vlm_qwen4_exp_compat.py::test_qwen4_small_hyper_connection_fusion_fails_closed[False]
  • tests/test_mlx_vlm_qwen4_exp_compat.py::test_qwen4_small_hyper_connection_fusion_fails_closed[True]
  • tests/test_server_main.py::test_module_entry_api_key_setup_end_to_end

This PR introduces no additional test failures.

@tc3oliver
tc3oliver force-pushed the fix/specprefill-static-prefix-boundary branch from 859218d to 872a240 Compare October 13, 2026 10:07
specprefill_system_end was the full prompt length minus a render without
system messages. Qwen3.5-family templates add a default system block to
that render, so the result came out short. BatchedEngine also passed tools
to both renders, which cancels the tool block and leaves the boundary at
the system text length. VLMBatchedEngine rendered without tools or
chat_template_kwargs.

Measure it instead, with the two-probe render jundot#4994 added for the shared
system prompt, and keep what the real prompt also starts with. Both engines
now share that in BaseEngine.
@tc3oliver
tc3oliver force-pushed the fix/specprefill-static-prefix-boundary branch from 872a240 to 936cad9 Compare November 7, 2026 07:34
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