[None][fix] Derive DSA indexer K-cache block stride at use time#16223
[None][fix] Derive DSA indexer K-cache block stride at use time#16223lucifer1004 wants to merge 2 commits into
Conversation
The DeepSeek-V4 indexer K-cache (INDEXER_COMPRESS) is stored per-compressed token: each page holds compressed_block_size = tokens_per_block // compress_ratio compressed tokens. The full-KV indexer gather (taken under chunked prefill or KV cache reuse) built its flat byte offsets from metadata._tokens_per_block, which is computed in DSAtrtllmAttentionMetadata.__post_init__ (dsa.py). That base __post_init__ runs inside super().__post_init__() BEFORE DeepseekV4TrtllmAttentionMetadata installs its real compress_ratios, so the derivation sees the pre-override [1], _select_indexer_compress_ratio returns 1, and _tokens_per_block stays at the raw tokens_per_block (256) instead of the compressed 64. The raw stride over-strides the gather by the compress ratio (4x): flat = block_ids * (256 * bytes_per_token) walks 4x past each 64-compressed-token page and runs off the end of the indexer K-cache once block_ids climbs past ~upper_bound / ratio. Block reuse is what pushes block_ids that high (committed indexer blocks are retained, so the per-request page index climbs toward the pool capacity), which is why the random-8k1k bench crashed with an illegal memory access in indexerKCacheGather.cu while gsm8k (light reuse) passed. Measured boundary: flat crossed the pool size between block_ids 35427 (safe) and 36015 (crash), with stride 17408 = 4 * 4352 and page 4352 bytes. Fix: in Indexer.build_indexer_params derive the indexer tokens-per-block from the LIVE compress ratio (kv_cache_manager.tokens_per_block // ratio) so the slot mapping strides by the compressed page size on every path that reads IndexerParams (the full-KV gather and recompute_slot_mappings). DSA v3.2 has ratio-divisor 1, so its value is unchanged. Note: on DSv4 the on_update slot_mapping_fp8 scatter is dead - the shared DSA scatter is no-op'd in DeepseekV4Indexer._update_k_cache (the compressor already writes INDEXER_COMPRESS) and the indexer reads go paged via the block table - so IndexerParams.tokens_per_block is the live indexer stride consumer. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Zihua Wu <13583761+lucifer1004@users.noreply.github.com>
The C++ indexer_k_cache_gather_op has no internal bounds check, unlike its Python sibling _gather_k_cache_for_chunk (which asserts the gather byte indices stay within k_cache.numel()). An over-strided full-KV slot mapping therefore surfaced as an illegal memory access instead of a diagnosable error. Add a pre-launch bounds assert on the flat fp8/scale byte indices, gated behind TRTLLM_DEBUG_INDEXER_GATHER_BOUNDS (default off) because it costs a device sync on the prefill hot path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Zihua Wu <13583761+lucifer1004@users.noreply.github.com>
📝 WalkthroughWalkthroughThe indexer now derives compressed-token strides from the live KV-cache configuration and adds an environment-gated bounds assertion before prefill K-cache gathers. ChangesIndexer cache addressing
Estimated code review effort: 3 (Moderate) | ~20 minutes 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
tensorrt_llm/_torch/attention_backend/sparse/dsa.py (1)
1949-1950: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick winGuard against non-divisible
tokens_per_block // compress_ratio.Floor division silently truncates when
tokens_per_blockis not an exact multiple ofcompress_ratio. Since this value becomes the indexer stride feeding slot-mapping addressing, a truncated block size would mis-stride the compressed K-cache pages — the exact class of bug this PR is fixing, but in the opposite direction. An explicit check makes the invariant (raw block size divisible by the compression ratio) fail loudly instead of producing subtly wrong gathers. Also consider reusing the localkv_cache_manager(Line 1923) here for consistency.♻️ Proposed guard
+ raw_tokens_per_block = kv_cache_manager.tokens_per_block + assert raw_tokens_per_block % compress_ratio == 0, ( + f"tokens_per_block ({raw_tokens_per_block}) must be divisible by " + f"compress_ratio ({compress_ratio}) for exact indexer strides") indexer_tokens_per_block = ( - metadata.kv_cache_manager.tokens_per_block // compress_ratio) + raw_tokens_per_block // compress_ratio)🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tensorrt_llm/_torch/attention_backend/sparse/dsa.py` around lines 1949 - 1950, Validate that the local kv_cache_manager.tokens_per_block is exactly divisible by compress_ratio before computing indexer_tokens_per_block, and fail explicitly if the invariant is violated instead of relying on floor division. Then compute the stride from kv_cache_manager rather than metadata.kv_cache_manager for consistency; update the surrounding logic in the relevant attention setup method.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@tensorrt_llm/_torch/attention_backend/sparse/dsa.py`:
- Around line 1949-1950: Validate that the local
kv_cache_manager.tokens_per_block is exactly divisible by compress_ratio before
computing indexer_tokens_per_block, and fail explicitly if the invariant is
violated instead of relying on floor division. Then compute the stride from
kv_cache_manager rather than metadata.kv_cache_manager for consistency; update
the surrounding logic in the relevant attention setup method.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Enterprise
Run ID: 7292f50b-26c8-40ae-87ed-6355f3130de4
📒 Files selected for processing (1)
tensorrt_llm/_torch/attention_backend/sparse/dsa.py
DeepseekV4TrtllmAttentionMetadata.__post_init__callssuper().__post_init__()before installing the model's realcompress_ratios, so the base class derives_tokens_per_blockfrom the default[1]and freezes it at the raw tokens-per-block — even though the comment right above the derivation warns the indexer slot mapping must use the compressed stride. The indexer K-cache is compressed-native (pages oftokens_per_block // compress_ratiotokens), so the fullkv/chunked-prefill gather computes flat offsets with a stride compress_ratio× too large. Data movement stays self-consistent at low block ids, which hides the bug; once KV block reuse retention (or a sufficiently large pool watermark) pushes block ids past capacity/compress_ratio,indexerKCacheGatherUnifiedKernelreads out of bounds — measured on a DSv4 tp2 setup: block_ids 35427 in-bounds, 36015 →cudaErrorIllegalAddress, against a predicted threshold of numel/stride = 35459. A side effect of the inflated addressing is that the indexer pool's effective capacity is silently 1/compress_ratio of its allocation.Commit 1 derives
IndexerParams.tokens_per_blockfrom the live cache-manager geometry atIndexer.build_indexer_paramsinstead of the stale metadata attribute. The write side (the DSv4 indexer compressor) already uses the manager geometry directly and is untouched; DeepSeek-V3.2-family models (compress divisor 1) are behavior-unchanged. Commit 2 adds a pre-launch bounds assert for the C++ gather matching its Python sibling_gather_k_cache_for_chunk, gated behindTRTLLM_DEBUG_INDEXER_GATHER_BOUNDS=1(off by default; the check costs a sync on the hot path).Validated on DSv4 (4× RTX PRO 6000 Blackwell): with block reuse enabled, a 5-round 20-concurrent ~7k-token load that previously faulted within 40–63 requests completes 103/103 with zero crashes; a full closed-loop qualification with reuse enabled passes (gsm8k 0.9447, serving bench green through the concurrency ramp); prefix-resume semantics verified across a mid-block partial-reuse boundary (early-context and near-boundary facts both answered correctly, reuse hits confirmed by 1.89× TTFT); chunked-prefill long-context ladder green at 128k/512k/1M with prefix reuse up to 52.7× TTFT gain at 1M. Reuse-disabled smoke shows no regression.
May be related to the failure family discussed in #15295 (retention-pressure-dependent corruption under block reuse), though that report is on a compress-divisor-1 model where this specific stride mismatch does not apply.
🤖 Generated with Claude Code
Summary by CodeRabbit