Skip to content

glm5next: keep rejected DFlash draft keys out of the C4 tail ring - #622

Draft
shieldstar wants to merge 1 commit into
local-inference-lab:dev/jovian-judgementfrom
shieldstar:contrib/dflash-tail-ring
Draft

shieldstar wants to merge 1 commit into
local-inference-lab:dev/jovian-judgementfrom
shieldstar:contrib/dflash-tail-ring

Conversation

@shieldstar

@shieldstar shieldstar commented Sep 3, 2026

Copy link
Copy Markdown

What

glm_kpool.py:_decode_update_kernel wrote tail[slot][pos % 4] = key for every row of a speculative request, including DFlash draft rows the verifier then rejected. The 4-slot C4 tail ring therefore carried keys of never-committed tokens into the next completed pool of the FP8 index. Glm5NextModelState already stages selector_num_accepted_tokens, but pooled_indexer.py never consumed it.

This PR adds per-slot DecodeTailRing bookkeeping: each decode step snapshots the ring, and the next step restores that snapshot and replays only the accepted prefix of the previously saved rows before writing its own.

Details

  • Accepted length is position-derived: this step's first position minus the previous step's first position. selector_num_accepted_tokens is consumed as a cross-check; on disagreement positions win, so resume and replay re-base the request correctly instead of trusting a stale count.
  • _prefill_tail_kernel zeroes the slot's saved count, so a fresh, resumed or short-tail prefill chunk never restores a stale ring.
  • update_decode_pools takes tail_ring= and num_accepted_tokens= (ones as fallback); the pooled indexer allocates the ring sized 1 + num_speculative_tokens and passes main_metadata.selector_num_accepted_tokens[:num_reqs].
  • Memory: about 100 KB at max_num_seqs=16, K=7.
  • Only the DFlash path of GLM-5.3 is affected. The MTP snapshot/restore path is untouched.

Found while auditing the mixed prefill/decode path for the corruption described in #616; it is a separate defect (that corruption reproduces with speculative decoding off), reported here on its own.

Testing

Kernel-level (GB10, Triton 3.7.1, the dev/jovian-judgement container): _decode_update_kernel, _prefill_pool_kernel and _prefill_tail_kernel JIT-compile and run. A two-step scenario (max_num_seqs=16, K=7, two 8-row speculative requests at positions 100 and 200 plus a 300-row prefill; step 2 restarts them at 103 and 205 with num_accepted_tokens=[3, 5]) produces tails, ring state and completed pools bit-identical to a from-scratch run that only ever saw the accepted rows; the prefill slot's saved count is 0. The check script is small and can be attached if wanted.

An earlier revision of this patch failed to compile at engine warm-up (Loop carried variable saved changed type, a tl.static_range index reused as a runtime loop variable); that is fixed and is why the kernels are now exercised on the GPU rather than only py_compiled.

Serving-level, four-node DGX Spark TP4, image built from dev/jovian-judgement@2e67b303 plus this branch and #618, GLM-5.3-Flash-NVFP4-Spark with the DFlash2 draft (k=7), fp8 KV, --max-num-batched-tokens 1024:

vllm serve local-inference-lab/GLM-5.3-Flash-NVFP4-Spark -tp 4 --decode-context-parallel-size 1 \
  --block-size 256 --attention-backend B12X --moe-backend b12x --linear-backend b12x \
  --mamba-cache-mode align --enable-prefix-caching --enable-chunked-prefill --kv-cache-dtype fp8 \
  -O2 --compilation-config '{"cudagraph_mode": "FULL"}' --max-cudagraph-capture-size 128 \
  --speculative-config '{"method": "dflash", "model": "local-inference-lab/GLM-5.3-Flash-DFlash2", "num_speculative_tokens": 7, "kv_cache_dtype": "auto", "attention_backend": "FLASH_ATTN"}' \
  --max-num-batched-tokens 1024 --max-model-len 1000000 --max-num-seqs 16 --gpu-memory-utilization 0.82

Not measured: a direct before/after on index-pool fidelity for generated tokens, which is what this fix changes; the semantics check above is the evidence for that part.

Duplicate check

gh pr list --state open --search "tail ring OR glm_kpool OR C4 tail OR rejected draft" returns nothing related; #505 (parallel C4 prefill pool writes) touches the prefill pool path, not the decode tail ring, and does not consume selector_num_accepted_tokens either.

Conventions

ruff check and ruff format --check (0.14.0, repo pyproject) pass on both files; every touched function has a Google-style docstring; the commit is DCO signed.

AI assistance

The audit that found the defect, the kernel change and the compile/semantics check were produced with AI assistance (Claude); the submitter reviewed every changed line and ran the tests listed above on our cluster.

Assumptions to check in review

  • selector_num_accepted_tokens equals bonus + accepted rows of the previous verify step; it is only used as a cross-check, so a mismatch degrades to the position-derived value rather than corrupting state.
  • DFlash rows for a request are position-ordered and the next step starts at prev_first + accepted.

glm_kpool.py:_decode_update_kernel writes tail[slot][pos % 4] = key for
every row of a speculative request, including draft rows the verifier
later rejects. The 4-slot ring therefore ends each step holding keys of
tokens that were never committed, and the next completed pool of the
FP8 C4 index is built from them. selector_num_accepted_tokens is staged
by Glm5NextModelState but pooled_indexer.py never consumed it.

Track a per-slot DecodeTailRing: snapshot the ring at the start of each
decode step, and on the next step restore that snapshot and replay only
the accepted prefix of the saved rows before writing new ones. The
accepted length is position-derived (this step's first position minus
the previous step's); selector_num_accepted_tokens is consumed as a
cross-check and positions win on disagreement, so resume and replay
re-base the request correctly. The prefill tail kernel zeroes the slot's
saved count so a fresh or resumed prefill never restores a stale ring.
Cost: ~100 KB at max_num_seqs=16, K=7.

Only speculative decoding of GLM-5.3 (DFlash) is affected; the MTP
snapshot/restore path is unchanged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: shieldstar <53370897+shieldstar@users.noreply.github.com>
@coderabbitai

coderabbitai Bot commented Sep 3, 2026

Copy link
Copy Markdown

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

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