Repository navigation
[ROCm] EAGLE: stop draft_probs being recycled out from under the verify sampler - #58
Merged
Merged
Conversation
draft_probs is the one draft-graph output that the target forward does not
consume: it is read afterwards, by eagle_sample. The tensor the replay
returns points into the graph's private memory pool, and that block is
recycled before the read. What lands there is the DSA top-k mask, so
eagle_sample sees -inf in draft_probs -- scattered runs whose position
tracks the sequence at roughly the accept length, each ending on a
64-aligned column, always at the same draft step because the pool's
allocation order is fixed at capture.
Downstream that turns into corrupt output: the residual p - q goes
infinite, norm_sum with it, the cumulative search never matches, and the
sampler commits its degenerate fallback, an untrained embedding row. The
model degenerates from there.
Copy out at the graph boundary. Only rejection sampling produces
draft_probs, so this is a no-op everywhere else.
GLM-5.2-MXFP4, MI355X, TP4/EP4, EAGLE steps=5 topk=1 draft=6,
rejection sampling on, temperature=1.0 top_p=0.95:
before after
non-finite entries in draft_probs 5 to 59 0
stray tokens per 1200 generated 25 to 81 0
The same three arms with CUDA graphs disabled show zero in both columns,
which is what identified the pool rather than the producer.
GSM8K 200q 5-shot at temperature=0: 0.940 / 0.000 invalid / accept len
3.035 / 512.8 tok/s, against 0.935 / 0.000 / 3.031 / 520.3 without the
copy. The 1.4% throughput gap is inside this harness's noise: three
repeats on one unchanged server span 582 to 646 tok/s.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Defence in depth behind the previous commit. The residual guard tested q for NaN with `q_val == q_val`, which infinities satisfy, so a malformed draft distribution reaches `p - q` and drives norm_sum infinite. The cumulative search then never matches and the kernel commits its fallback, VOCAB_SIZE - 1 -- an id past the tokenizer's vocabulary whenever the embedding is padded, as it is for GLM-5.2 (154880 against 154856). Test q against the probability range instead. A comparison with NaN is false, so the range test rejects NaN along with the infinities and with negatives. A sampling kernel should not be able to emit an invalid token id because an input was malformed, whatever wrote that input. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
Two commits against one bug: the first removes its cause, the second stops the sampler turning a malformed input into an invalid token id. Supersedes #57, which carried the second commit alone.
Problem
With EAGLE and rejection sampling, a request at
temperature > 0comes back with stray!and repeated</think>/<tool_call>fragments — a few percent of generated tokens on GLM-5.2.temperature = 0is clean, because a greedy batch takes the argmax path and never reaches the sampler.The chain:
draft_probsis the one draft-graph output the target forward does not consume — it is read afterwards, byeagle_sample. The tensor the replay returns points into the graph's private memory pool.-infinto positions past the current sequence.q_val == q_valrejects NaN but not-inf, sop - qgoes to+infandnorm_sumwith it.total_cumsum > coin * norm_sumis then never true, and the kernel commits its fallback,VOCAB_SIZE - 1.VOCAB_SIZEis the embedding width, 154880 for GLM-5.2, against a tokenizer vocabulary of 154856. The committed id is an untrained row. It decodes to an empty string, so nothing is visible in the output, but it enters the context and the model degenerates from there.Three properties of the corruption identified step 2: each run of
-infends on a 64-aligned column (page_size = 64), the runs advance with the decode step at roughly the accept length (the mask's valid region tracks the sequence), and they land at the same draft step every time (the pool's allocation order is fixed at capture).--disable-cuda-graphremoves them entirely, which is what ruled out the producer.Fix
Copy
draft_probsout at the graph boundary. Only rejection sampling produces it, so this is a no-op everywhere else.Test
qagainst the probability range rather than for NaN, in the two residual passes:Reject rather than accept when
qis not a probability, in the accept test. That site is where a corrupt row does the most damage:coin * q < pis-inf < pfor an-infq and0 < pfor a zero one, so the step accepts unconditionally and commits the draft head's token instead of the target's. Zero also passes the range test above, so widening that guard does not cover this site.Xwas sampled fromq, soq(X)has to be strictly positive:The failure is invisible from the outside — acceptance climbing toward 1.0 reads as a good draft model, not as a broken test.
The second is worth keeping on its own: a sampling kernel should not be able to emit an invalid token id because an input was malformed, whatever wrote that input. It also covers the
VOCAB_SIZE - 1fallback, which stays wrong in principle for a padded vocabulary even though this makes it unreachable in the case observed.Usage
No flag. The path is reached with EAGLE and rejection sampling, which on ROCm means:
Send a request with
temperature > 0and count!in the reply.Results
GLM-5.2-MXFP4, MI355X, TP4/EP4, EAGLE
steps=5 topk=1 draft=6, rejection sampling on,temperature=1.0 top_p=0.95:draft_probsCost,
bench_servingrandom 1024/512, concurrency 1, both arms on this commit's base so the copy is the only difference:Median ITL is stable to the hundredth of a millisecond in both arms and moves by one of them, 0.4%, which is the display resolution of the metric. Throughput varies by more than that within each arm, so it carries no signal either way. The copy is one
(bs, steps, vocab)fp32 tensor per decode step, about 3 MB at these shapes.GSM8K 200q 5-shot at
temperature=0: 0.940 accuracy, 0.000 invalid, accept len 3.035. Unchanged, as expected for a path this cannot reach — three repeats on one unchanged server span 0.920 to 0.945.🤖 Generated with Claude Code
CI States
Latest PR Test (Base): ❌ Run #34646795717
Latest PR Test (Extra): ❌ Run #34646795214
Latest PR Test (AMD ROCm 10): ❌ Run #34646795657