Skip to content

[None][fix] Derive DSA indexer K-cache block stride at use time#16223

Open
lucifer1004 wants to merge 2 commits into
NVIDIA:mainfrom
lucifer1004:fix/dsa-indexer-kcache-stride
Open

[None][fix] Derive DSA indexer K-cache block stride at use time#16223
lucifer1004 wants to merge 2 commits into
NVIDIA:mainfrom
lucifer1004:fix/dsa-indexer-kcache-stride

Conversation

@lucifer1004

@lucifer1004 lucifer1004 commented Jul 10, 2026

Copy link
Copy Markdown
Collaborator

DeepseekV4TrtllmAttentionMetadata.__post_init__ calls super().__post_init__() before installing the model's real compress_ratios, so the base class derives _tokens_per_block from 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 of tokens_per_block // compress_ratio tokens), 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, indexerKCacheGatherUnifiedKernel reads 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_block from the live cache-manager geometry at Indexer.build_indexer_params instead 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 behind TRTLLM_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

  • Bug Fixes
    • Improved sparse attention cache indexing to use the current KV-cache configuration, preventing incorrect compressed-token mapping.
    • Added optional debug validation to detect out-of-bounds cache access during index gathering.

lucifer1004 and others added 2 commits July 10, 2026 00:12
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>
@lucifer1004 lucifer1004 requested a review from a team as a code owner July 10, 2026 07:12
@lucifer1004 lucifer1004 requested a review from PerkzZheng July 10, 2026 07:12
@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The indexer now derives compressed-token strides from the live KV-cache configuration and adds an environment-gated bounds assertion before prefill K-cache gathers.

Changes

Indexer cache addressing

Layer / File(s) Summary
Use live KV-cache stride
tensorrt_llm/_torch/attention_backend/sparse/dsa.py
IndexerParams.tokens_per_block is computed from the KV-cache manager’s live tokens_per_block and the effective compression ratio.
Validate prefill gather bounds
tensorrt_llm/_torch/attention_backend/sparse/dsa.py
When TRTLLM_DEBUG_INDEXER_GATHER_BOUNDS=1, prefill slot-mapping byte offsets are checked against the indexer K-cache tensor before the gather operation.

Estimated code review effort: 3 (Moderate) | ~20 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title follows the required [None][fix] format and clearly summarizes the main change to derive the DSA indexer stride at use time.
Description check ✅ Passed It clearly explains the bug, fix, and validation, though it doesn't use the template's section headings.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
tensorrt_llm/_torch/attention_backend/sparse/dsa.py (1)

1949-1950: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Guard against non-divisible tokens_per_block // compress_ratio.

Floor division silently truncates when tokens_per_block is not an exact multiple of compress_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 local kv_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

📥 Commits

Reviewing files that changed from the base of the PR and between e2ce7a6 and 3bf4490.

📒 Files selected for processing (1)
  • tensorrt_llm/_torch/attention_backend/sparse/dsa.py

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