Skip to content

[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
NVIDIA:mainfrom
dc3671:fix-mtp-overlap-kv-capacity
Draft

[https://nvbugs/6443620][fix] Reserve overlap-scheduler slack in gen KV capacity for one-model spec decode#16280
dc3671 wants to merge 2 commits into
NVIDIA:mainfrom
dc3671:fix-mtp-overlap-kv-capacity

Conversation

@dc3671

@dc3671 dc3671 commented Jul 12, 2026

Copy link
Copy Markdown
Collaborator

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_len per scheduled step, computed from host-side bookkeeping. However, under overlap the device-side KV position runs one verification round ahead of the host (up to draft_len accepted-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 to draft_len tokens.

This overrun is harmless while it stays inside an already-allocated block, but whenever it crosses a tokens_per_block boundary, applyMLARopeAndAssignQKVKernelGeneration looks up a block ordinal whose block-offset entry is still the empty sentinel (-1). A -1 offset computes an address below the pool base, producing:

torch.AcceleratorError: CUDA error: an illegal memory access was encountered

(reported asynchronously at the next sampler-event sync; the GPU coredump shows a Warp MMU Fault on the FP8 KV-append STG in 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 = 131071 while manager capacity was 131070 (the grow to 131074 landed 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_len instead of 1 + draft_len — one draft_len for the step's draft tokens, one as slack for the overlap scheduler's advance booking. revert_allocate_generation and extend_capacity_for_tokens are updated symmetrically.

Impact

  • Cost: at most draft_len extra 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.
  • No effect when speculative decoding is disabled (draft_len == 0) or for two-model spec decode (which reserves via dummy draft tokens in the scheduler).

Validation

  • Repro (DSV4 DEP8 + MTP3, bs1/rank, 128k ISL, disaggregated gen worker, overlap scheduler + CUDA graphs): faulted at decode iter 19–28 in five consecutive runs before the fix; with the fix, the same config runs the full benchmark to completion (775+ iters, zero IMA), verified both with and without a debug validator that checks every block-offset entry the MLA rope kernel will address.
  • One-axis ablations confirmed the trigger is the overlap+MTP capacity under-reservation: MTP0 clean, CUDA-graphs-off still faults, lm-head-TP-in-ADP-off still faults, CTX-server count and benchmark client irrelevant.

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_len slack was granted on every scheduled iteration, but update_resources only rewinds the rejected drafts (py_rewind_len = draft_len - num_accepted), which balances just the recurring 1 + draft_len part. Net capacity growth per iteration was 1 + accepted + draft_len while the sequence only grows 1 + accepted — the slack compounded, leaking draft_len tokens of KV capacity per generation iteration. After ~750 iterations (MTP3) the leak reached ~2.3k tokens (~9 blocks at tokens_per_block=256), overran the max_blocks_per_seq-sized host_kv_cache_block_offsets rows, and crashed the scheduler in kv_cache.resize():

ValueError: User-provided base page indices is too short

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-manager prepare_resources path, and the CUDA-graph padding top-up in extend_capacity_for_tokens) records the granted slack in _pending_overlap_slack, revert_allocate_generation deducts it, and update_resources reclaims it alongside the rewind. Capacity at forward prep still always includes the fresh 1 + 2 * draft_len growth, so the IMA protection from the first commit is preserved, while steady-state net growth returns to 1 + accepted per 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

…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>
@dc3671 dc3671 changed the title [None][fix] Reserve overlap-scheduler slack in gen KV capacity for one-model spec decode [https://nvbugs/6443620][fix] Reserve overlap-scheduler slack in gen KV capacity for one-model spec decode Jul 12, 2026
…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>
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