Repository navigation
Conversation
… blocks The scheduler skips worker-side KV zeroing for every block an async KV load covers (the zeroing could race the transfer), but a Mooncake Store hit only fills the hit length -- a multiple of prefix_match_unit -- of its last logical block. With kernel-block splitting the remaining kernel pages of that block keep their previous owner's bytes. On hybrid models every cache group overlays one block pool, so a block that last held fp32 Mamba state is read back as bf16 attention latents, which decode to NaN/Inf with ~1/256 density. Attention decode kernels (e.g. trtllm-gen MLA) read the whole kernel page holding the current position, so the first decode step of such a request produces NaN in the first attention layer and the request degenerates; the prefill path never reads past the written tokens, which is why the damage only shows once decoding starts. Zero the never-written kernel pages of the last loaded logical block for every attention group once the load has landed (in get_finished, on the caller's compute stream). This can race neither the transfer nor the next forward, touches no Mamba group and costs one small zero_() per attention layer per completed load. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013hMcjaJa8vC9zEZeipxDfd Signed-off-by: Zhewen Li <zhewenli@inferact.ai>
…ten page Store hits are only prefix_match_unit aligned, and prefix_match_unit merely has to divide the logical block (cache.py documents 32 vs 1024), so the last loaded token can land in the middle of a kernel page. Zeroing whole never-written pages left the rest of that page stale, with the same NaN/Inf hazard. Zero the slots after the last loaded token along the token axis of the standardized [B, H, N, C] view (second to last, also for a raw [B, N, C] MLA latent cache), which keeps the operation layout agnostic. The unit test now uses a 200-token hit on 64-token pages and covers a 4-D and a 3-D view. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013hMcjaJa8vC9zEZeipxDfd Signed-off-by: Zhewen Li <zhewenli@inferact.ai>
Contributor
|
This pull request has merge conflicts that must be resolved before it can be |
6 tasks done
This reverts commits b60f566 and 3c74c05. Zeroing the tail inside the Mooncake worker misses NIXL and the other connectors, and its layout assert fails under DCP > 1. The scheduler + KVBlockZeroer fix that follows replaces it for every connector. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Zhewen Li <zhewenli@inferact.ai>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Zhewen Li <zhewenli@inferact.ai>
Admission exempts the blocks an async KV load writes from alloc-time zeroing, since zeroing could race the out-of-band write. The load fills only the leading tokens of the request's last block, so the rest of that block keeps its previous owner's bytes. In pools that need zeroing (hybrid Mamba/linear-attention + attention, mixed precision) those can be another group's state, e.g. fp32 recurrent state read back as fp8/bf16 KV, which decodes to NaN. Decode attention reads the whole kernel page holding the current position and multiplies the masked slots' V by a softmax weight of 0, and 0 * NaN = NaN, so the request's logits go NaN. This holds for every connector: a Store hit leaves the hit block's tail, and a NIXL transfer with equal P/D block sizes copies the producer's whole last block, stale tail included. When an async load completes, the scheduler now queues each zeroed attention group's partially filled last block, with its valid token count, in SchedulerOutput.kv_block_tails_to_zero (before cache_blocks can hash it; cached, shared and null blocks are never queued). KVBlockZeroer.zero_block_ids(block_ids, tails) zeroes new blocks whole and tails past their valid slots in the same launch of the existing kernel, in both model runners, before the forward. For every group with a layer in it, each segment (one kernel page of a logical block) records the page index, slots per page and slot stride, so groups sharing a KV cache tensor all reach it. A tail masks words by slot index (word // slot_stride % page_slots), which covers every KVCacheLayout and kernel-block splitting; indexer caches, whose pages pack values and scales separately, are skipped. Valid tokens are converted to rank-local slots for DCP-sharded groups. With overlapping batches, queueing a tail fences the request's blocks so an abort cannot hand them to a new async load before the zeroing step has run. Steps without tails take the unchanged path. This replaces the earlier MooncakeStoreWorker-only zeroing, which missed the other connectors and failed its layout assert under DCP > 1. Co-authored-by: Summer Yang <girasoleyang@gmail.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Zhewen Li <zhewenli@inferact.ai>
This branch has not been deployed
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.
Purpose
Fix NaN outputs for requests whose KV cache was restored by an async load (any KV connector: NIXL P/D, MooncakeStore, MultiConnector, offloading) on models that need KV zeroing (hybrid Mamba/linear-attention + attention pools, mixed-precision caches).
Problem
Admission exempts the blocks an async load will write from alloc-time zeroing (
skip_zeroing_group_ids, #59504), since zeroing could race the out-of-band write. A load fills only the leading tokens of the request's last block, so the rest of that block keeps its previous owner's bytes:P @ Vstill multiplies their V, and 0 × NaN = NaN. Every logit of the request goes NaN at its first decode step: degenerate output, or a crash once NaN logits reach the sampler.Fix
_update_waiting_for_remote_kv, the completion point shared by all connectors), queue each zeroed attention group's partially filled last block with its valid token count as aKVBlockTailinSchedulerOutput.kv_block_tails_to_zero. This runs beforecache_blocks, which can hash a partial block under a finerhash_block_size; cached, shared and null blocks are never queued. Gated byneeds_kv_cache_zeroing, no new flag.KVBlockZeroer.zero_block_ids(block_ids, tails)zeroes new blocks whole and tails past their valid slots in one launch of the existing Triton kernel, before the forward pass (the V1 runner keeps a separate tail call so runners that override_zero_block_idsstill zero new blocks). For every group with a layer in it, each zeroer segment (one kernel page of a logical block) records the page index, slots per page and slot stride, so groups that share a KV cache tensor all reach the shared segments. A tail entry skips fully valid pages and masks the partial one by slot index (word // slot_stride % page_slots), which is exact for everyKVCacheLayout(token-major and head-major pages, head-group-outer, block-major with interleaved layers) and for kernel-block splitting. Views whose slots don't tile int32 words, and indexer caches (their pages store all values, then all scales, unlike their[B, N, C]view), keep the old behavior with a one-time warning.defer_block_free), queueing a tail fences the request's blocks like a scheduled request's, so an abort cannot hand them to a new async load before the zeroing step has run.block_size × dcptokens dealt to ranks in runs ofcp_kv_cache_interleave_size; the zeroer converts the valid token count to this rank's valid slots (replicated groups use the count as is).HAS_TAILSis a constexpr), and warmup compiles the tail variant as a no-op.This replaces the first version of this PR, which zeroed the tail inside
MooncakeStoreWorkeronly. That version did not cover NIXL or other connectors, and its register-time layout assert fails under DCP > 1 (the worker's group block size is DCP-scaled).Co-authored with @GirasoleY.
Not a duplicate. #59504 decides which blocks are exempt from zeroing and still (correctly) exempts the partial last block. #49612 and #59746 zero the part of a NIXL transfer that mismatched P/D block sizes leave uncovered. #39283 extends zeroing to full-attention-only models. None zeroes the never-written tail of a completed load. Open-PR searches for the area keywords returned nothing else.
Test Plan
On a GB200 node:
pytest tests/v1/worker/test_kv_block_zeroer.py \ tests/v1/core/test_scheduler.py \ tests/v1/core/test_prefix_replay.py \ tests/v1/worker/test_utils.py \ tests/v1/kv_connector/unit/test_remote_prefill_lifecycle.py \ tests/v1/kv_connector/unit/test_remote_decode_lifecycle.py \ tests/v1/kv_connector/unit/test_kv_load_failure_recovery.py \ tests/v1/kv_connector/unit/test_invalid_blocks_correctness.py \ tests/v1/kv_connector/unit/test_error_propagation.py \ tests/v1/worker/test_gpu_warmup_blocks.py pre-commit run ruff-check / ruff-format (v0.14.0) on the touched files mypy on the touched source files (compared against main)New tests:
test_async_load_queues_last_block_tail_once(scheduler): the tail is queued exactly once, in the step that first schedules the request, with the loaded token count; nothing for a block-aligned load; a 12,288-token (DCP8-scaled) block.test_received_last_block_tail_queued_once(remote prefill): the same for a NIXL-style load, including a hybrid (Mamba) config with a coarser cache block than the hash block, where the count is taken before the full-hit N−1 adjustment.test_tail_local_valid_slots_follow_token_ownership: every prefix of a block against explicit per-rank token ownership (DCP 1/2/8 × interleave 1/4/16, plus a replicated group).test_zero_tails_keep_exactly_the_valid_slots: byte-exact against a reference for everyKVCacheLayout, with and without kernel-block splitting, tails ending inside the first and the second kernel page.test_zero_tails_dcp8_mla: DCP8, 1,536-slot rank-local blocks of 24 64-slot fp8 MLA pages; a 275,089-token load ends in block 22 with 595 valid slots on rank 0 and 594 elsewhere. Exactly the rest of that block is zeroed on every rank.test_zero_tails_of_groups_sharing_a_tensor: two groups sharing one tensor; each group's tail reaches it.test_async_load_tail_fences_block_free: a promoted request that is not scheduled in the queueing step and is then aborted keeps its blocks out of the pool until that step is processed.test_zero_new_blocks_and_tails_in_one_launch,test_tails_skip_layouts_whose_slots_are_not_word_aligned,test_tails_skip_indexer_caches.Test Result
mainon the touched files (none new).Known gaps: indexer caches are skipped (see above); the CPU model runner's own
_zero_block_idsdoes not zero tails.AI assistance (Claude Code) was used for the port, the tests and this description.
🤖 Generated with Claude Code