[https://nvbugs/6443620][fix] Reserve overlap-scheduler slack in gen KV capacity for one-model spec decode#16280
Draft
dc3671 wants to merge 2 commits into
Draft
[https://nvbugs/6443620][fix] Reserve overlap-scheduler slack in gen KV capacity for one-model spec decode#16280dc3671 wants to merge 2 commits into
dc3671 wants to merge 2 commits into
Conversation
…e-model spec decode Under the overlap scheduler with one-model speculative decoding (MTP), the device-side KV position runs one verification round ahead of host bookkeeping (up to draft_len accepted-but-uncommitted tokens), and the MTP draft layers then append draft-token KV beyond that position. The V2 scheduler grows generation KV capacity by only 1 + draft_len from the host view, so the device write reach can exceed the allocated capacity by up to draft_len tokens. Whenever that overrun crosses a tokens_per_block boundary, applyMLARopeAndAssignQKVKernelGeneration addresses a block ordinal whose block-offset entry is still the empty sentinel (-1), producing an illegal memory access. Observed with DSV4-Pro DEP8 + MTP3 at 128k ISL (disagg gen worker): with the prompt ending block-aligned, the first window-slide boundary crossing after prefill faults deterministically ~130 tokens into decode. TEP escaped only because its MTP acceptance rate happened to be near zero, keeping the device from outrunning the allocation. Grow generation capacity by 1 + 2 * draft_len instead (one draft_len for the step's draft tokens, one for the overlap advance slack), and mirror the doubled growth in revert_allocate_generation and extend_capacity_for_tokens. Costs at most draft_len extra reserved tokens per request (transiently one extra block near boundaries); max_blocks_per_seq headroom already covers it. Validated: DEP8+MTP3 128k gen_only repro previously faulting at decode iter 19-28 now runs the full benchmark clean (775+ iters, both with and without debug instrumentation). Signed-off-by: Zhenhuan Chen <chenzhh3671@gmail.com>
…anager_v2 3e7fa61d73 grew generation KV capacity by 1 + 2 * draft_len per scheduled iteration (the extra draft_len reserves overlap-scheduler slack for one-model spec decode), but update_resources only trims py_rewind_len = draft_len - num_accepted, which balances just the recurring 1 + draft_len part. Net capacity growth per iteration was therefore 1 + accepted + draft_len while the sequence only grows 1 + accepted: the slack compounded, leaking draft_len tokens of capacity per iteration. Under MTP3 at 128k ISL / 1k OSL the leak reaches ~2.3k tokens (~9 blocks at tokens_per_block=256) by the time requests near the end of decode, overrunning max_blocks_per_seq sizing of host_kv_cache_block_offsets and crashing the scheduler in kv_cache.resize() with 'User-provided base page indices is too short' (then the executor error-broadcast hangs the whole disagg instance). MTP0 (draft_len=0) is unaffected, matching observations. Track the slack granted per request in _pending_overlap_slack (allocation, draft-manager prepare_resources, and CUDA-graph padding top-up all record it; revert_allocate_generation deducts it) and reclaim it in update_resources alongside the rewind. The slack thus stays a constant offset over the request lifetime: capacity at forward prep still always includes the fresh 1 + 2 * draft_len growth, so the IMA protection from 3e7fa61d73 is preserved, while steady-state net growth returns to 1 + accepted per iteration. Signed-off-by: Zhenhuan Chen <chenzhh3671@gmail.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.
Background
With the overlap scheduler enabled and one-model speculative decoding (MTP), the KV-cache-manager V2 scheduler grows a generation request's KV capacity by
1 + draft_lenper scheduled step, computed from host-side bookkeeping. However, under overlap the device-side KV position runs one verification round ahead of the host (up todraft_lenaccepted-but-uncommitted tokens), and the MTP draft layers then append draft-token KV beyond that position. The device's write reach can therefore exceed the host-allocated capacity by up todraft_lentokens.This overrun is harmless while it stays inside an already-allocated block, but whenever it crosses a
tokens_per_blockboundary,applyMLARopeAndAssignQKVKernelGenerationlooks up a block ordinal whose block-offset entry is still the empty sentinel (-1). A-1offset computes an address below the pool base, producing:(reported asynchronously at the next sampler-event sync; the GPU coredump shows a Warp MMU Fault on the FP8 KV-append
STGin the MLA rope generation kernel).Observed with DeepSeek-V4 attention-DP (DEP8) + MTP3 at 128k ISL on a disaggregated gen worker: with the prompt length near a block boundary, the first sliding-window boundary crossing after prefill faults ~20–30 decode iterations in, deterministically. Debug instrumentation caught the exact inconsistency: a step ran with device
kv_len = 131071while manager capacity was131070(the grow to131074landed one scheduling round later), and the draft-KV append touched block ordinal 512 whose offsets entry was still-1.Notably, TP-sharded attention (TEP) only escaped this because its measured MTP acceptance was near zero in the same setup, so the device position never outran the allocation — the bug is acceptance-rate-dependent, not attention-DP-specific.
Summary
Grow generation KV capacity by
1 + 2 * draft_leninstead of1 + draft_len— onedraft_lenfor the step's draft tokens, one as slack for the overlap scheduler's advance booking.revert_allocate_generationandextend_capacity_for_tokensare updated symmetrically.Impact
draft_lenextra reserved KV tokens per in-flight generation request (transiently one extra block near block boundaries; freed at request completion). Measured kv_cache_util impact in the repro: +0.2% over a 1024-token generation.draft_len == 0) or for two-model spec decode (which reserves via dummy draft tokens in the scheduler).Validation
Follow-up (2nd commit): reclaim the slack each iteration
Long-decode validation under MTP3 (128k ISL / 1k OSL, disaggregated gen worker) exposed a defect in the first commit: the extra
draft_lenslack was granted on every scheduled iteration, butupdate_resourcesonly rewinds the rejected drafts (py_rewind_len = draft_len - num_accepted), which balances just the recurring1 + draft_lenpart. Net capacity growth per iteration was1 + accepted + draft_lenwhile the sequence only grows1 + accepted— the slack compounded, leakingdraft_lentokens of KV capacity per generation iteration. After ~750 iterations (MTP3) the leak reached ~2.3k tokens (~9 blocks attokens_per_block=256), overran themax_blocks_per_seq-sizedhost_kv_cache_block_offsetsrows, and crashed the scheduler inkv_cache.resize():right as requests approached the end of their 1024-token decode (reproduced 6/6 on DEP8/DEP16 × batch 1/2/4; MTP0 is unaffected since
draft_len == 0).The second commit keeps the slack a constant offset over the request lifetime: every grant site (
try_allocate_generation, the draft-managerprepare_resourcespath, and the CUDA-graph padding top-up inextend_capacity_for_tokens) records the granted slack in_pending_overlap_slack,revert_allocate_generationdeducts it, andupdate_resourcesreclaims it alongside the rewind. Capacity at forward prep still always includes the fresh1 + 2 * draft_lengrowth, so the IMA protection from the first commit is preserved, while steady-state net growth returns to1 + acceptedper iteration.Validation of the follow-up: the DSV4 DEP8 + MTP3 128k/1k gen-worker benchmark that previously crashed at iter ~757 in all six swept configs now runs to completion (1027+ iterations, all requests reaching the full 1024-token OSL, zero resize errors), on top of the original repro staying IMA-free.
🤖 Generated with Claude Code