Never serve a masked decode from a staged speculation fire#420
Merged
Conversation
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
… fire
The pass-level speculation chain extender pre-fires the next decode step
as soon as the previous one completes, before the inferlet has set up the
next step's constraints — so every staged fire is built with an EMPTY
constraint surface: no logit mask (`build_next_request` emits
`logit_masks = []`, since the grammar mask isn't known until the matcher
advances on the just-sampled token) and no custom attention mask
(`masks = []`, `has_user_mask = false`). The hit check
(`entry_matches_request`) compared anchor token/position and the spec
arrays but neither mask, so a request that carried one was served the
unconstrained, fully-causal staged token verbatim — the constraint was
silently dropped at the unconstrained→constrained transition.
Two reachable variants, both default-on (`speculation_depth = 1`):
- Logit mask: a json_object / json_schema / forced-tool grammar decode
whose anchor matched a staged entry ignored the grammar. A real model
hides it (asked for JSON it naturally emits a leading `{`); the dummy
driver (uniform-random within the mask) exposes it as fully
unconstrained garbage that never begins with `{`.
- Attention mask: a single-token decode supplying a custom attention
mask (windowed-attention, attention-sink, hierarchical-attention
inferlets, which decode one token at a time and call `attention_mask()`
once the sequence exceeds the window) was served a token computed with
full causal attention — the window mask silently dropped, so it
attended to positions the inferlet meant to exclude.
Reject a staged hit whenever the incoming request carries either mask,
forcing it onto the cold path where the mask is actually applied (the
`try_hit` miss branch clears the now-stale chain). This mirrors the
producer-side skip set in `evaluate_request_shape`, which already refuses
to stage from either kind of masked request; the two sites must stay
symmetric. Adds regression tests for both variants proving a masked
request misses an otherwise-matching staged entry.
Also teach the dummy driver to ack Copy and Adapter requests as no-ops:
a no-compute driver has no KV pages to copy and no adapter weights to
load, and token generation is independent of cache contents, so the
faithful behavior is a vacuous success. Previously these hit the
catch-all arm and returned a driver error (-1), which broke the
context-fork path the runtime uses for prefix sharing.
shsym
force-pushed
the
scratch/655-spec-mask-fix
branch
from
June 16, 2026 14:53
6f21edf to
2fcece7
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Pass-level speculation pre-fires each context's next decode step before the inferlet has set up that step's constraints, so every staged fire is built with an empty constraint surface: no logit mask (
build_next_requestemitslogit_masks = []) and no custom attention mask (masks = [],has_user_mask = false). The staged-hit checkentry_matches_requestcompared anchor token/position and the spec arrays but neither mask — so a request carrying one was served the unconstrained, fully-causal staged token verbatim, silently dropping the constraint at the unconstrained→constrained transition.Two reachable variants, both default-on (
speculation_depth = 1):json_object/json_schema/ forced tool-call grammar decode ignored the grammar. A real model hides this (asked for JSON it naturally emits a leading{); the dummy driver (uniform-random within the mask) exposes it as fully unconstrained output that never begins with{.windowed-attention/attention-sink/hierarchical-attentiondecode (these decode one token at a time and callattention_mask()once the sequence exceeds the window) was served a token computed with full causal attention, so it attended to positions the inferlet meant to exclude.Fix
Reject a staged hit whenever the incoming request carries either mask, forcing it onto the cold path where the mask is applied (the
try_hitmiss branch clears the now-stale chain). This mirrors the producer-side skip set inevaluate_request_shape, which already refuses to stage from either kind of masked request — the two sites must stay symmetric or the drop reappears.A regression test per variant proves a masked request misses an otherwise-matching staged entry (both mutation-proven: each fails if its mask clause is reverted).
Also teaches the dummy driver to ack
Copy/Adapteras no-ops (status 0) instead of the catch-all-1error: a no-compute driver has no KV pages to copy and no adapter weights to load, and token generation is independent of cache contents, so a vacuous success is faithful. The previous-1broke the context-fork path used for prefix sharing.Provenance
Integration bug surfaced by the reconcile: this branch's
runtime/src/inference/speculator.rs(pass-level speculation) and the pie-bridgelogit_masks/masksschema were developed on separate upstream branches and were not wired together until the reconcile merged them onto one tree. Neither source branch exercised both halves, so the staged-fire-vs-masked-request interaction was never tested until now.Verification
inference::speculator::tests(10)entry_match_rejects_logit_masked_request+entry_match_rejects_user_masked_request, both mutation-provenjson_object/json_schemaoutput begins with{json_schema→ 200 with{"name": ...}; forced tool → 200 withtool_calls; S3 coherence green