Skip to content

[None][perf] DeepSeek-V4: cut host overhead in generation-phase metadata preparation - #17257

Open
hyukn wants to merge 5 commits into
NVIDIA:mainfrom
hyukn:feat/dsv4-gen-metadata-host-overhead
Open

[None][perf] DeepSeek-V4: cut host overhead in generation-phase metadata preparation#17257
hyukn wants to merge 5 commits into
NVIDIA:mainfrom
hyukn:feat/dsv4-gen-metadata-host-overhead

Conversation

@hyukn

@hyukn hyukn commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator

Summary

_prepare_inputs on the DeepSeek-V4 generation worker is pure host work that is rebuilt from scratch on every decode iteration. Profiling a disaggregated GB300 run showed 12.5 ms per iteration spent there, spread over ~1480 ATen dispatches of which only ~5% launch any GPU work — the rest is interpreter, dispatcher and allocator cost building index and block-table metadata.

This PR removes that work in three ways and brings _prepare_inputs to 8.96 ms (-24.2%).

What changed

1. Reuse the request→slot mapping within one prepare() step.
IndexMapper.get_copy_index was walked three times per step — by compute_sliding_block_tables, once per compression ratio by copy_batch_compress_block_tables, and by copy_batch_indexer_compress_block_tables — always with the same (request_ids, num_contexts, beam_width). The mapping is deterministic in those arguments and returns a view of one shared pinned buffer, so the later walks recomputed a result equal to the one already there. Each walk costs O(num_seqs) dispatches (select + as_strided + fill_ per entry). It is now computed once and memoized for the step, keyed on the request-id list contents rather than its identity, because callers reuse and mutate a single list object. The memo is reset at the top of every step.

2. Stop pre-filling block-table regions that are immediately overwritten.
copy_batch_sliding_block_tables and copy_batch_block_offsets filled their whole destination with BAD_PAGE_INDEX and then overwrote the live rows. The destinations are sized for the mapper capacity, not the batch, so the fill covered a [layers, sliding_types, capacity, max_blocks] tensor in order to keep only the padding. Only the regions the following copy does not touch are filled now. The padding still has to carry BAD_PAGE_INDEX — padded CUDA-graph token slots can index those rows through req_idx_per_token, so this is load-bearing, not dead.

3. Build the per-iteration index and compressed-KV metadata on the device.
Six new CUDA ops replace element-wise ATen chains on the decode critical path: sparse-attention indices, token positions with the cu_seq_lens/req_idx_per_token pair, per-compression-ratio KV lengths, the compressed-token mask, context and generation compressed position ids, and the shared compression block table. The block-table op also removes a host gather of a few hundred KB out of host_kv_cache_block_offsets plus its staging copy, per compression ratio; that buffer is now uploaded at most once per iteration and shared by all consumers instead of once per table. Together these cut the dispatch count in the region from ~1480 to ~1290.

Performance

Config: disaggregated serving, 1 generation server, tp16/ep16, attention DP, MTP-3, max batch 64, fp8 KV cache, GB300. torch profiler, 60 consecutive decode iterations, 4 generation ranks. Both arms use the same build with the kernels compiled in, differing only in whether the python call sites use them, so the comparison isolates this change.

region before after
_prepare_inputs 11.82 ms 8.96 ms -24.2%
compressed KV metadata 0.756 ms 0.112 ms -85%
sparse-attention indices 0.682 ms 0.112 ms -84%
block tables 1.508 ms 0.986 ms -35%
compressed-token mask 0.250 ms 0.049 ms -80%
indices conversion 0.155 ms 0.071 ms -55%
cu_seq_lens 0.086 ms 0.045 ms -48%

Regions this change does not touch move by -1.09% ± 4.01% over 15 samples, which sets the noise floor; every region above is 8–21σ outside it and reproduces on all 4 ranks. Per-rank spread of the region also collapses from 0.111 ms to 0.016 ms.

The two host-side items alone (1 and 2) were measured separately on a build without the kernels: 0.771 ms (-6.2%) — block tables 1.526 → 0.929 ms, indexer K-cache 0.696 → 0.417 ms. So they stand on their own if the kernels are ever disabled.

Two of the eight regions targeted showed no effect and are reported as such: cached_token_lens H2D (+1%) and the per-ratio host scalars (-0.3%, already only 0.016 ms).

On end-to-end numbers

End-to-end serving throughput and latency are unchanged, and I would not claim otherwise. The generation loop in this configuration is 92–95% GPU-busy, so the overlap scheduler absorbs a host saving of this size. A paired comparison over 6921 identical conversation turns (same prompt, same output length, matched within 2%/5%) put TTFT, inter-token latency and output speed all at ~50/50 — no detectable effect in either direction. An unpaired comparison appeared to show -2% throughput, but that was a load-mismatch artifact: the two servers held different request mixes at the same iteration index (GPU-busy differed by +1.6%, which a host-side python change cannot cause), and the deficit vanishes once turns are paired.

The value of this change is host headroom on the decode critical path, not a throughput number. It matters where the host is the constraint — lower GPU occupancy, larger batches, or after GPU-side work lands.

Testing

tests/unittest/_torch/custom_ops/test_deepseek_v4_metadata_ops.py — pins each new op against the python reference it replaces, with the references reproduced verbatim in the test file so the tests fix the semantics rather than the current implementation. Covers all compression-ratio subsets, batch sizes from 1 to 300, padded output buffers (asserting the padding tail is left untouched), per-ratio list-length mismatches, and empty batches.

tests/unittest/_torch/attention/sparse/test_deepseek_v4_block_table_host_path.py — covers the mapping memo (value equality with the uncached path, recomputation when any argument changes, in-place mutation of a reused request-id list, per-step invalidation) and asserts the partially-filled block-table buffers are bit-identical to the previous fill-everything behaviour, including the num_tables == capacity no-tail case and the beam/row padding of copy_batch_block_offsets. These are host-only and need no GPU.

The correctness of the kernels was also verified on GB200/GB300 SBSA hardware during development (221 parametrized cases passing).

Notes for reviewers

  • Draft: the kernels have been built and correctness-tested on SBSA, but this exact branch has not been rebuilt against current main. Requesting CI to confirm the build across architectures before de-drafting.
  • _compute_compressed_mask and _compute_gen_compressed_position_ids are no longer called from prepare() now that the ops replace them, but they should not be deleted: tests/unittest/_torch/attention/sparse/deepseek_v4/test_compressor_module.py calls metadata._compute_compressed_mask(...) directly (pre-existing test, signature unchanged by this PR). Correcting an earlier note in this description that suggested they were dead code.
  • The device path in copy_batch_compress_block_tables is gated on dst_tensor.is_cuda, so the host fallback remains for non-CUDA destinations.

🤖 Generated with Claude Code


Update: reuse boundary made explicit

Reviewing which of these kernels are actually DeepSeek-V4-specific, two of them are not, and the deepseek_v4_ prefix was hiding that. They now live in kernels/attentionMetadataKernels.{h,cu} with the prefix dropped:

op reusable? why
trtllm::compute_token_positions generic takes only (seq_lens, cached_tokens, batch_size, num_tokens). It is the device-side form of pad(cumsum(seq_lens)) + repeat_interleave(arange(batch), seq_lens) + searchsorted. The shared DSA metadata path builds exactly this by hand today, and that path also serves the separate dsa algorithm — so a second consumer already exists.
trtllm::compute_shared_block_table generic takes (pool_id, scale) as plain ints; computes where(base == BAD_PAGE_INDEX, BAD_PAGE_INDEX, base * scale) over block_offsets[pool_id, copy_idx, 0, :]. Any KV manager with a shared-page pool can use it.
per-ratio KV lengths, compressed-token mask, ctx/gen compressed position ids DSA-family signatures carry the per-compression-ratio concept, shared by DeepSeek-V4 and dsa but not beyond
sparse-attention indices DeepSeek-V4 signature carries window_size, max_comp_128, sparse_mla_topk

No behaviour change in that commit: same kernels, same launch configuration, same schemas apart from the names.

I have not converted the DSA path to call the shared op in this PR — that is a separate change with its own correctness surface, and it should be measured on a prefill-heavy workload rather than inherited from this one. Flagging it as the obvious follow-up.

Note the two python-level items are generic in pattern but not in benefit: the base KVCacheManagerV2 also calls get_copy_index, but only once per invocation and it passes copy_idx down rather than re-walking, so the 3x redundancy is specific to DeepSeek-V4 having three independent table builders. And the base copy_batch_block_offsets already delegates to a device op, so it never had the fill-everything-then-overwrite pattern.

Test coverage added in the same update

The op tests previously stopped at batch 300 and so never exercised the single-block scan's template ladder (<=512, <=2048, else the 4096 compile-time bound). Added cases at 511/512/513/2047/2048/2049/4096 for both scan-based ops, plus two asserting that a batch above the bound is rejected rather than silently overrunning shared memory (the bound is enforced by TORCH_CHECK in the op layer). Also added coverage for the shared block table, which had none despite being registered and called — including an assertion that rows past num_tables are left untouched.

Verification status

Run on GB300 against a build with these kernels compiled in:

  • op registration: 7/7
  • test_deepseek_v4_metadata_ops.py: 262 passed, 3 skipped
  • test_deepseek_v4_block_table_host_path.py: 8 passed

The post-rename suite was re-run and passes identically (the prebuilt image predates the rename, so the new names were aliased onto the old registrations for that run; the rename itself is covered by the schema/impl pairing and by CI's build). Still draft pending a CI build against current main.

Adjacent gap noticed, deliberately left alone

While checking that my own tests get collected I found that
tests/unittest/_torch/custom_ops/test_deepseek_v4_block_table.py (pre-existing, covers the two
upstream deepseek_v4_compute_sliding_block_tables* ops) is not referenced by any test list
and therefore never runs in CI — I confirmed it is absent from tests/integration/test_lists/
entirely and absent from the testReport of my earlier pipeline. My PR only adds a license-header
line to the file those ops live in, so registering that test is out of scope here; flagging it
rather than silently expanding this PR.


CI status

Latest pipeline on d63bc157fa. Both builds pass on both architectures, and the tests this PR
adds are now actually collected and green:

Build-x86_64 / Build-SBSA SUCCESS (also passed on the two earlier runs)
Release-Check SUCCESS
custom_ops/test_deepseek_v4_metadata_ops.py 524 passed, 6 skipped
attention/sparse/test_deepseek_v4_block_table_host_path.py 24 passed
all DeepSeek-V4 cases 1873 passed, 192 skipped, 0 failed
overall 55340 passed / 15703 skipped / 5 failed

The 5 remaining failures are outside this PR's surface —
git diff --name-only <base>...HEAD | grep -iE 'executor|overlap|qwen|scheduler' is empty:

  • test_modeling_qwen_moetorch.OutOfMemoryError on a shared A30 with another process
    already resident (Process 892308 has 246 MiB…), plus 1 aggregator assert rolling those up
  • unittest/_torch/executor / test_overlap_scheduler.py — "Test terminated unexpectedly"

[Test-SBSA-Single-GPU] shows as failed but is an infrastructure abort, not a test result:
ABORTED at exactly 60.03 min with "Queue task was cancelled" and 0 test failures
(9143 passed / 353 skipped) — the Slurm agent never got an allocation. The same job aborted the
same way on three other PRs' builds in the same window.

An earlier run had 5 different failures (llmapi / scaffolding / NanoV3Omni); those did not recur.

Happy to re-trigger once the queue clears if reviewers want a fully green pipeline before review.

Dev Engineer Review

  • Added CUDA kernels and Torch wrappers for DeepSeek-V4 attention metadata.
  • Replaced several host-side metadata operations with CUDA paths.
  • Added persistent buffers and per-step memoization for token positions and copy indices.
  • Limited block-table padding writes to untouched regions while preserving BAD_PAGE_INDEX.
  • Retained CPU fallbacks for non-CUDA execution.
  • Added validation for devices, dtypes, shapes, capacities, ratios, optional inputs, and integer bounds.
  • Updated public APIs consistently for device-output buffers and token-position outputs.
  • Added test_deepseek_v4_metadata_ops.py to the B200 and B300 pre-merge test lists.
  • No configuration errors, duplicate entries, invalid paths, or unintended test-list scope changes were identified.
  • Reported performance improved _prepare_inputs by 24.2%, with no reported serving throughput or latency regression.
  • Latest CI completed successfully.

QA Engineer Review

Added test functions in tests/unittest/_torch/attention/sparse/test_deepseek_v4_block_table_host_path.py:

  • test_copy_index_memo_reuses_one_walk_per_step()
  • test_copy_index_memo_matches_uncached_result()
  • test_copy_index_memo_recomputes_on_changed_arguments()
  • test_copy_index_memo_keys_on_contents_not_identity()
  • test_copy_index_memo_is_reset_each_step()
  • test_sliding_block_table_padding_keeps_bad_page_index()
  • test_sliding_block_table_padding_when_full()
  • test_block_offsets_padding_keeps_bad_page_index()

These host-path tests are not listed in the modified CI or manual QA test-list files.

Added CUDA-gated coverage in tests/unittest/_torch/custom_ops/test_deepseek_v4_metadata_ops.py for index computation, KV lengths, masks, position IDs, token positions, shared block tables, boundary sizes, padding, optional outputs, invalid inputs, scan limits, and large batches. This test file is covered by l0_b200.yml and l0_b300.yml.

Verdict: needs follow-up because CI or manual QA coverage for the host-path test file is not shown.

@hyukn

hyukn commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator Author

/bot run --disable-fail-fast

hyukn and others added 3 commits August 4, 2026 16:58
…ata preparation

`_prepare_inputs` on the DeepSeek-V4 generation worker is pure host work that is
rebuilt from scratch every decode iteration. Profiling a disaggregated
GB300 run (1 generation server, tp16/ep16, attention DP, MTP-3, batch 64,
fp8 KV cache) showed 12.5 ms per iteration spent there, spread over ~1480 ATen
dispatches of which only ~5% launch any GPU work: the rest is interpreter,
dispatcher and allocator cost building index and block-table metadata.

This change removes that work in three ways.

1. Reuse the request->slot mapping within one prepare() step.
   `IndexMapper.get_copy_index` was walked three times per step -- once by
   `compute_sliding_block_tables`, once per compression ratio by
   `copy_batch_compress_block_tables`, and once by
   `copy_batch_indexer_compress_block_tables` -- always with the same
   (request_ids, num_contexts, beam_width). The mapping is deterministic in
   those arguments and returns a view of one shared pinned buffer, so the later
   walks recomputed a result equal to the one already there. Each walk costs
   O(num_seqs) dispatches (select + as_strided + fill_ per entry). It is now
   computed once and memoized for the step, keyed on the request-id list
   contents rather than its identity because callers reuse and mutate one list
   object. The memo is reset at the top of every step.

2. Stop pre-filling block-table regions that are immediately overwritten.
   `copy_batch_sliding_block_tables` and `copy_batch_block_offsets` filled their
   whole destination with BAD_PAGE_INDEX and then overwrote the live rows. The
   destinations are sized for the mapper capacity, not the batch, so the fill
   covered a [layers, sliding_types, capacity, max_blocks] tensor to keep only
   the padding. Only the regions the following copy does not touch are filled
   now. The padding still has to carry BAD_PAGE_INDEX: padded CUDA-graph token
   slots can index those rows through req_idx_per_token.

3. Build the per-iteration index and compressed-KV metadata on the device.
   Six new CUDA ops replace element-wise ATen chains that ran on the decode
   critical path: sparse-attention indices, token positions with the
   cu_seq_lens/req_idx_per_token pair, per-compression-ratio KV lengths, the
   compressed-token mask, context and generation compressed position ids, and
   the shared compression block table. The block-table op also removes a host
   gather of a few hundred KB out of host_kv_cache_block_offsets plus its
   staging copy, per compression ratio. host_kv_cache_block_offsets is now
   uploaded at most once per iteration and shared by all consumers instead of
   once per table. Together these cut the ATen dispatch count in the region
   from ~1480 to ~1290.

Measured on the configuration above, torch profiler, 60 consecutive decode
iterations, 4 generation ranks, comparing against the same build with the
python call sites unchanged so the kernels are compiled into both arms:

  _prepare_inputs                     11.82 ms -> 8.96 ms   (-24.2%)

  compressed KV metadata               0.756 -> 0.112 ms     (-85%)
  sparse-attention indices             0.682 -> 0.112 ms     (-84%)
  block tables                         1.508 -> 0.986 ms     (-35%)
  compressed-token mask                0.250 -> 0.049 ms     (-80%)
  indices conversion                   0.155 -> 0.071 ms     (-55%)
  cu_seq_lens                          0.086 -> 0.045 ms     (-48%)

Regions the change does not touch move by -1.09% +/- 4.01% over 15 samples,
which sets the noise floor; every region above is 8 to 21 sigma outside it and
reproduces on all 4 ranks. Per-rank spread of the region also collapses from
0.111 ms to 0.016 ms.

The two host-side items alone (1 and 2), measured separately on a build without
the kernels, account for 0.771 ms (-6.2%): block tables 1.526 -> 0.929 ms and
indexer K-cache 0.696 -> 0.417 ms.

End-to-end serving throughput and latency are unchanged. The generation loop in
this configuration is 92-95% GPU-busy, so the overlap scheduler absorbs a host
saving of this size; a paired comparison over 6921 identical conversation turns
put TTFT, inter-token latency and output speed all within noise. The value here
is host headroom on the decode critical path, not a throughput number.

Tests: `tests/unittest/_torch/custom_ops/test_deepseek_v4_metadata_ops.py`
pins each new op against the python reference it replaces, reproduced verbatim
in the test file, and covers padded buffers, per-ratio list mismatches and
empty batches. `tests/unittest/_torch/attention/sparse/
test_deepseek_v4_block_table_host_path.py` covers the mapping memo (value
equality with the uncached path, recomputation on changed arguments, in-place
mutation of a reused request-id list, per-step invalidation) and asserts the
partially-filled block-table buffers are bit-identical to the previous
fill-everything behaviour, including the no-tail case.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Yukun He <23156053+hyukn@users.noreply.github.com>
…epSeek-V4 namespace

Two of the kernels added in the previous commit do not depend on any DeepSeek-V4
concept, and naming them `deepseek_v4_*` hid that they are reusable:

- `compute_token_positions` takes only (seq_lens, cached_tokens, batch_size,
  num_tokens). It is the device-side form of the generic
  `pad(cumsum(seq_lens))` + `repeat_interleave(arange(batch), seq_lens)` +
  `searchsorted` triple. The shared DSA metadata path builds exactly this by
  hand today, and that path also serves the separate `dsa` algorithm, so there
  is already a second consumer.
- `compute_shared_block_table` takes (pool_id, scale) as plain ints and computes
  `where(base == BAD_PAGE_INDEX, BAD_PAGE_INDEX, base * scale)` over
  `block_offsets[pool_id, copy_idx, 0, :]`. Any KV-cache manager with a
  shared-page pool can use it.

Both now live in `kernels/attentionMetadataKernels.{h,cu}` with the prefix
dropped, and the registered ops are `trtllm::compute_token_positions` and
`trtllm::compute_shared_block_table`. No behaviour change: same kernels, same
launch configuration, same schemas apart from the names. The bound guards and
the size ladder of the single-block scan are unchanged.

The remaining five kernels stay under the DeepSeek-V4 name because their
signatures carry the per-compression-ratio concept (per-ratio KV lengths,
compressed-token mask, context/generation compressed position ids) or DeepSeek-V4
specifics (`window_size`, `max_comp_128`, `sparse_mla_topk` for the sparse
indices).

Also extends the op tests, which previously stopped at batch 300 and therefore
never exercised the scan's template ladder: added cases at 511/512/513/2047/
2048/2049/4096 for both scan-based ops, and two cases asserting a batch above the
compile-time bound is rejected rather than silently overrunning shared memory.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Yukun He <23156053+hyukn@users.noreply.github.com>
- Apply the repo's clang-format (v16) and ruff / ruff-format (v0.9.4) to the
  files this branch touches.
- Drop a dead `device` local in prepare_for_deepseek_v4_indices: it was only
  needed by the tensor-building code the fused kernel replaced. The CPU fallback
  branch further down still binds its own `device` from `token_positions` and is
  unaffected.

No functional change.

Signed-off-by: Yukun He <23156053+hyukn@users.noreply.github.com>
Co-Authored-By: Claude <noreply@anthropic.com>
@hyukn
hyukn force-pushed the feat/dsv4-gen-metadata-host-overhead branch from b7f18b5 to c04e1a0 Compare August 4, 2026 16:58
@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #63801 [ run ] triggered by Bot. Commit: c04e1a0 Link to invocation

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #63801 [ run ] completed with state SUCCESS. Commit: c04e1a0
/LLM/main/L0_MergeRequest_PR pipeline #51745 completed with status: 'FAILURE'

CI Report

⚠️ Action Required:

  • Please check the failed tests and fix your PR
  • If you cannot view the failures, ask the CI triggerer to share details
  • Once fixed, request an NVIDIA team member to trigger CI again

CI Agent Failure Analysis

Link to invocation

…e lists

CI collected `tests/unittest/_torch/attention/sparse/
test_deepseek_v4_block_table_host_path.py` automatically (the B300 list pulls in
`unittest/_torch/attention` as a directory) but not
`tests/unittest/_torch/custom_ops/test_deepseek_v4_metadata_ops.py`, because the
`custom_ops` directory is enumerated file by file. Add it next to the existing
`test_deepseek_v4_q_norm.py` entry on B200 and to the DeepSeek-V4 block on B300,
so the ops these tests cover are actually exercised pre-merge.

Signed-off-by: Yukun He <23156053+hyukn@users.noreply.github.com>
Co-Authored-By: Claude <noreply@anthropic.com>
@hyukn

hyukn commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator Author

/bot run --disable-fail-fast

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #63839 [ run ] triggered by Bot. Commit: 138f12b Link to invocation

The release license check requires the complete Apache-2.0 notice on C++/CUDA
sources, not the short SPDX-identifier form. My six new/rewritten kernel files
used the short form, which produced `errcount: 6` in [Release-Check] Run while
the same stage passes on other PRs. Copy the header verbatim from the existing
`deepseekV4BlockTable.h` in the same directory.

The two new test files keep the short SPDX form, matching the surrounding test
sources (for example `test_compressor_module.py`), and were not among the
reported errors.

Signed-off-by: Yukun He <23156053+hyukn@users.noreply.github.com>
Co-Authored-By: Claude <noreply@anthropic.com>
@hyukn

hyukn commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator Author

/bot run --disable-fail-fast

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #63841 [ run ] triggered by Bot. Commit: d63bc15 Link to invocation

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #63839 [ run ] completed with state ABORTED. Commit: 138f12b

Link to invocation

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #63841 [ run ] completed with state SUCCESS. Commit: d63bc15
/LLM/main/L0_MergeRequest_PR pipeline #51781 completed with status: 'FAILURE'

CI Report

⚠️ Action Required:

  • Please check the failed tests and fix your PR
  • If you cannot view the failures, ask the CI triggerer to share details
  • Once fixed, request an NVIDIA team member to trigger CI again

CI Agent Failure Analysis

Link to invocation

@hyukn

hyukn commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator Author

/bot run --disable-fail-fast

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #63909 [ run ] triggered by Bot. Commit: d63bc15 Link to invocation

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #63909 [ run ] completed with state SUCCESS. Commit: d63bc15
/LLM/main/L0_MergeRequest_PR pipeline #51846 completed with status: 'SUCCESS'

CI Report

Link to invocation

@hyukn
hyukn marked this pull request as ready for review August 5, 2026 05:53
@hyukn
hyukn requested review from a team as code owners August 5, 2026 05:53
@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Added CUDA kernels and Torch operators for DeepSeek V4 token positions, indices, compressed metadata, and shared block tables. Updated Python metadata preparation with device buffers and fused operations. Added validation tests and pre-merge test coverage.

Changes

DeepSeek V4 metadata pipeline

Layer / File(s) Summary
Attention and index CUDA kernels
cpp/tensorrt_llm/kernels/attentionMetadataKernels.*, cpp/tensorrt_llm/kernels/deepseekV4Indices.*, cpp/tensorrt_llm/kernels/deepseekV4BlockTable.cu
Added kernels and launch helpers for token positions, request indices, shared block tables, sliding-window indices, compressed indices, and top-k lengths.
Compressed metadata CUDA kernels
cpp/tensorrt_llm/kernels/deepseekV4CompressedMeta.*
Added kernels for per-ratio KV lengths, cumulative offsets, compressed masks, and context or generation position IDs.
Torch operator validation and registration
cpp/tensorrt_llm/thop/deepseekV4BlockTableOp.cpp
Added validated Torch operators, CUDA dispatch, schemas, and registrations for the new metadata operations.
Python metadata and block-table integration
tensorrt_llm/_torch/attention_backend/sparse/deepseek_v4/*
Added fused CUDA paths, persistent token-position buffers, device block-table destinations, copy-index memoization, and preserved invalid-page padding.
CUDA correctness and integration coverage
tests/unittest/_torch/custom_ops/test_deepseek_v4_metadata_ops.py, tests/unittest/_torch/attention/sparse/test_deepseek_v4_block_table_host_path.py, tests/integration/test_lists/test-db/*
Added correctness, boundary, invalid-input, padding, memoization, and pre-merge test coverage.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant DeepseekV4Metadata
  participant TorchOperator
  participant CUDAKernels
  participant DeviceBuffers
  DeepseekV4Metadata->>TorchOperator: request fused metadata computation
  TorchOperator->>CUDAKernels: dispatch CUDA kernels
  CUDAKernels->>DeviceBuffers: write positions, indices, masks, and block tables
Loading

Possibly related PRs

Suggested reviewers: schetlur-nv, thorjohnsen

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 41.76% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the DeepSeek-V4 performance improvement implemented by the pull request.
Description check ✅ Passed The description explains the problem, solution, performance impact, test coverage, verification status, and remaining CI failures in sufficient detail.
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.

Actionable comments posted: 3

🧹 Nitpick comments (6)
cpp/tensorrt_llm/kernels/attentionMetadataKernels.h (1)

37-63: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Document the new public interfaces with Doxygen.

The new public CUDA interfaces use regular // comments. Use Doxygen comments so generated API documentation includes these contracts.

  • cpp/tensorrt_llm/kernels/attentionMetadataKernels.h#L37-L63: Convert the launch-interface documentation to Doxygen comments.
  • cpp/tensorrt_llm/kernels/deepseekV4Indices.h#L29-L35: Convert the launch-interface documentation to Doxygen comments.
  • cpp/tensorrt_llm/kernels/deepseekV4CompressedMeta.h#L29-L79: Add Doxygen documentation for the public parameter structures and launch interfaces.

As per coding guidelines, “document new interfaces with Doxygen.”

🤖 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 `@cpp/tensorrt_llm/kernels/attentionMetadataKernels.h` around lines 37 - 63,
Convert the launch-interface comments in
cpp/tensorrt_llm/kernels/attentionMetadataKernels.h lines 37-63 and
cpp/tensorrt_llm/kernels/deepseekV4Indices.h lines 29-35 from regular comments
to Doxygen comments, preserving their documented contracts. Add Doxygen
documentation for the public parameter structures and launch interfaces in
cpp/tensorrt_llm/kernels/deepseekV4CompressedMeta.h lines 29-79, covering the
visible public symbols and their parameters without changing behavior.

Source: Coding guidelines

tensorrt_llm/_torch/attention_backend/sparse/deepseek_v4/deepseek_v4.py (3)

591-591: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Annotate the new signature.

prepare_for_deepseek_v4_indices declares token_positions=None with no type and no return type. The coding guidelines require an annotation on every function and None for procedures.

♻️ Suggested change
-    def prepare_for_deepseek_v4_indices(self, token_positions=None):
+    def prepare_for_deepseek_v4_indices(
+        self, token_positions: Optional[torch.Tensor] = None
+    ) -> None:

As per coding guidelines: "Annotate every function, use None for procedures".

🤖 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/deepseek_v4/deepseek_v4.py` at
line 591, Annotate the prepare_for_deepseek_v4_indices signature with an
appropriate type for token_positions and an explicit None return type,
preserving its existing behavior.

Source: Coding guidelines


923-934: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

gen_output_offsets forces a device-to-host sync per ratio.

Line 920 calls .item() on self.cu_new_comp_kv_cuda[r][num_contexts] for every ratio when num_contexts > 0. Each call is an implicit D2H read plus a stream synchronization on the critical path. The comment at lines 794-796 states that this pattern is what the host-side ctx_output_sizes exists to avoid.

ctx_output_sizes[r] already holds cu_new_comp_kv[num_contexts] for ratio r as a Python int. When ctx_output_sizes is not None, reuse it instead. on_update_kv_lens() can pass None, so keep the .item() path as the fallback.

The .item() line itself is outside the changed range. The new fused call consumes its result, so the fix belongs with this block.

♻️ Suggested change
             gen_output_offsets = {
-                r: self.cu_new_comp_kv_cuda[r][num_contexts].item() if num_contexts > 0 else 0
+                r: (
+                    0
+                    if num_contexts == 0
+                    else (
+                        int(ctx_output_sizes[r])
+                        if ctx_output_sizes is not None
+                        else self.cu_new_comp_kv_cuda[r][num_contexts].item()
+                    )
+                )
                 for r in self._compress_ratios_sorted
             }
🤖 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/deepseek_v4/deepseek_v4.py`
around lines 923 - 934, Update the gen_output_offsets construction immediately
before deepseek_v4_compute_gen_compressed_position_ids to reuse
ctx_output_sizes[r] when ctx_output_sizes is available, avoiding the per-ratio
.item() device-to-host synchronization. Preserve the existing
cu_new_comp_kv_cuda[r][num_contexts].item() fallback when ctx_output_sizes is
None, and keep the resulting offsets passed unchanged to the fused operation.

581-589: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Remove the unused host cu_seq_lens buffer and staging. The True call computes cu_seq_lens_cuda, and the later False call reuses it. Remove the host cumsum, H2D copy, and host-buffer allocation.

🤖 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/deepseek_v4/deepseek_v4.py`
around lines 581 - 589, Remove the host cu_seq_lens allocation, host
cumulative-sum staging, and H2D copy from the token-position preparation flow
around compute_token_positions. Keep the True invocation to compute
cu_seq_lens_cuda, and preserve the later False invocation’s reuse of that device
buffer without referencing the removed host buffer.
cpp/tensorrt_llm/kernels/deepseekV4CompressedMeta.cu (1)

138-152: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Clamp reqIdx in the context kernel, or validate counts[i] against cu[numContexts].

upperBoundRequest(cu, numContexts, t) can return numContexts. This happens when t >= cu[numContexts]. The kernel then reads pastKv[numContexts], which is one element past the validated batchSize region for the context entry point (batchSize == numContexts there).

The current callers pass counts[i] == cu[numContexts], so the branch is unreachable today. The operator layer only checks count >= 0, so nothing enforces that invariant. computeGenCompressedPositionIdsKernel already applies min(max(...)) for the same reason.

♻️ Suggested defensive clamp
     for (int32_t t = blockIdx.x * blockDim.x + threadIdx.x; t < total; t += gridDim.x * blockDim.x)
     {
-        int32_t const reqIdx = upperBoundRequest(cu, numContexts, t);
+        int32_t const reqIdx = min(upperBoundRequest(cu, numContexts, t), numContexts - 1);
         out[t] = (pastKv[reqIdx] + (t - cu[reqIdx])) * 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 `@cpp/tensorrt_llm/kernels/deepseekV4CompressedMeta.cu` around lines 138 - 152,
Prevent out-of-bounds access in computeCtxCompressedPositionIdsKernel by
defensively clamping the upperBoundRequest result to the valid context range
before indexing pastKv. Preserve the existing position calculation while
ensuring reqIdx never reaches numContexts, or alternatively validate counts
against cu[numContexts] before launching the kernel.
tests/unittest/_torch/custom_ops/test_deepseek_v4_metadata_ops.py (1)

219-223: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

The zero-count branches assert nothing.

test_compute_compressed_mask skips the ratio when totals[r] == 0. test_compute_ctx_compressed_position_ids (line 246) and test_compute_gen_compressed_position_ids (line 292) do the same. The PR description lists empty batches as covered, but these continue statements mean the zero-count case only checks that the op does not raise.

The launch helpers return early when maxTotalTokens <= 0 or maxCount <= 0. That early return is worth pinning: assert that the output buffer is unchanged instead of skipping.

♻️ Suggested change for the mask test
     for r in ratios:
         if totals[r] == 0:
+            # The launch helper returns early; the buffer must stay untouched.
+            assert not masks[r].any()
             continue
🤖 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 `@tests/unittest/_torch/custom_ops/test_deepseek_v4_metadata_ops.py` around
lines 219 - 223, Update test_compute_compressed_mask,
test_compute_ctx_compressed_position_ids, and
test_compute_gen_compressed_position_ids so their totals[r] == 0 branches assert
that the corresponding output buffer remains unchanged, matching the launch
helpers’ early-return behavior, rather than continuing without assertions.
Preserve the existing expected-value comparisons for nonzero counts.
🤖 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.

Inline comments:
In `@tensorrt_llm/_torch/attention_backend/sparse/deepseek_v4/cache_manager.py`:
- Around line 1120-1127: Initialize _block_offsets_uploaded, _copy_idx_memo_key,
and _copy_idx_memo_val in the constructor path _prepare_page_table_tensor with
their appropriate initial values, then replace the getattr defaults in
_ensure_device_block_offsets and the related memo-copy logic with direct
attribute access. Make the per-step ordering requirement explicit by asserting
that the step was initialized before _ensure_device_block_offsets proceeds,
while preserving compute_sliding_block_tables as the reset point.
- Around line 1372-1392: Update the shared block-table construction flow around
_compute_shared_block_table_device and _compute_shared_block_table so
host_indexer_k_cache_block_offsets remains synchronized when device_block_table
is provided. Populate the host table as well, or consistently redirect DSA and
speculative replay consumers to the device table, including MTP expansion and
slot-mapping conversion.

In
`@tests/unittest/_torch/attention/sparse/test_deepseek_v4_block_table_host_path.py`:
- Around line 123-182: The three padding tests must invoke the production
methods instead of duplicating their logic inline. Update
test_sliding_block_table_padding_keeps_bad_page_index,
test_sliding_block_table_padding_when_full, and
test_block_offsets_padding_keeps_bad_page_index to construct a
DeepseekV4CacheManager.__new__ instance with _num_tables and
_precomputed_sliding_block_tables configured as _manager_with_mapper does, pass
CUDA tensors to copy_batch_sliding_block_tables or copy_batch_block_offsets, and
apply the existing CUDA skip marker.

---

Nitpick comments:
In `@cpp/tensorrt_llm/kernels/attentionMetadataKernels.h`:
- Around line 37-63: Convert the launch-interface comments in
cpp/tensorrt_llm/kernels/attentionMetadataKernels.h lines 37-63 and
cpp/tensorrt_llm/kernels/deepseekV4Indices.h lines 29-35 from regular comments
to Doxygen comments, preserving their documented contracts. Add Doxygen
documentation for the public parameter structures and launch interfaces in
cpp/tensorrt_llm/kernels/deepseekV4CompressedMeta.h lines 29-79, covering the
visible public symbols and their parameters without changing behavior.

In `@cpp/tensorrt_llm/kernels/deepseekV4CompressedMeta.cu`:
- Around line 138-152: Prevent out-of-bounds access in
computeCtxCompressedPositionIdsKernel by defensively clamping the
upperBoundRequest result to the valid context range before indexing pastKv.
Preserve the existing position calculation while ensuring reqIdx never reaches
numContexts, or alternatively validate counts against cu[numContexts] before
launching the kernel.

In `@tensorrt_llm/_torch/attention_backend/sparse/deepseek_v4/deepseek_v4.py`:
- Line 591: Annotate the prepare_for_deepseek_v4_indices signature with an
appropriate type for token_positions and an explicit None return type,
preserving its existing behavior.
- Around line 923-934: Update the gen_output_offsets construction immediately
before deepseek_v4_compute_gen_compressed_position_ids to reuse
ctx_output_sizes[r] when ctx_output_sizes is available, avoiding the per-ratio
.item() device-to-host synchronization. Preserve the existing
cu_new_comp_kv_cuda[r][num_contexts].item() fallback when ctx_output_sizes is
None, and keep the resulting offsets passed unchanged to the fused operation.
- Around line 581-589: Remove the host cu_seq_lens allocation, host
cumulative-sum staging, and H2D copy from the token-position preparation flow
around compute_token_positions. Keep the True invocation to compute
cu_seq_lens_cuda, and preserve the later False invocation’s reuse of that device
buffer without referencing the removed host buffer.

In `@tests/unittest/_torch/custom_ops/test_deepseek_v4_metadata_ops.py`:
- Around line 219-223: Update test_compute_compressed_mask,
test_compute_ctx_compressed_position_ids, and
test_compute_gen_compressed_position_ids so their totals[r] == 0 branches assert
that the corresponding output buffer remains unchanged, matching the launch
helpers’ early-return behavior, rather than continuing without assertions.
Preserve the existing expected-value comparisons for nonzero counts.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 867ba1a2-6123-4542-9b12-6c656c819eab

📥 Commits

Reviewing files that changed from the base of the PR and between b469fb3 and d63bc15.

📒 Files selected for processing (14)
  • cpp/tensorrt_llm/kernels/attentionMetadataKernels.cu
  • cpp/tensorrt_llm/kernels/attentionMetadataKernels.h
  • cpp/tensorrt_llm/kernels/deepseekV4BlockTable.cu
  • cpp/tensorrt_llm/kernels/deepseekV4CompressedMeta.cu
  • cpp/tensorrt_llm/kernels/deepseekV4CompressedMeta.h
  • cpp/tensorrt_llm/kernels/deepseekV4Indices.cu
  • cpp/tensorrt_llm/kernels/deepseekV4Indices.h
  • cpp/tensorrt_llm/thop/deepseekV4BlockTableOp.cpp
  • tensorrt_llm/_torch/attention_backend/sparse/deepseek_v4/cache_manager.py
  • tensorrt_llm/_torch/attention_backend/sparse/deepseek_v4/deepseek_v4.py
  • tests/integration/test_lists/test-db/l0_b200.yml
  • tests/integration/test_lists/test-db/l0_b300.yml
  • tests/unittest/_torch/attention/sparse/test_deepseek_v4_block_table_host_path.py
  • tests/unittest/_torch/custom_ops/test_deepseek_v4_metadata_ops.py

Comment on lines +1120 to +1127
def _ensure_device_block_offsets(self) -> None:
"""Upload host_kv_cache_block_offsets to the device at most once per iteration."""
if getattr(self, "_block_offsets_uploaded", False):
return
self._device_kv_cache_block_offsets_input.copy_(
self.host_kv_cache_block_offsets, non_blocking=True
)
self._block_offsets_uploaded = True

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.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Initialize _block_offsets_uploaded in the constructor and make the ordering requirement explicit.

Two problems share one cause here. _block_offsets_uploaded is never declared. _ensure_device_block_offsets reads it through getattr(self, ..., False), and only compute_sliding_block_tables resets it to False (line 1230). The same pattern applies to _copy_idx_memo_key and _copy_idx_memo_val at lines 1179-1193.

The correctness of every consumer therefore depends on compute_sliding_block_tables running first in each step. DeepseekV4TrtllmAttentionMetadata.prepare does call it first, so the current code is correct. If any future path builds a compress or indexer table without that call, _ensure_device_block_offsets returns immediately and the kernels read block offsets from the previous iteration. The failure is silent wrong block tables, not a crash.

Declare all three attributes in _prepare_page_table_tensor and drop the getattr defaults. Consider asserting in _ensure_device_block_offsets that the step has been started.

♻️ Suggested change
 # in _prepare_page_table_tensor, next to the other device buffers
+        self._block_offsets_uploaded = False
+        self._copy_idx_memo_key = None
+        self._copy_idx_memo_val = None
     def _ensure_device_block_offsets(self) -> None:
         """Upload host_kv_cache_block_offsets to the device at most once per iteration."""
-        if getattr(self, "_block_offsets_uploaded", False):
+        if self._block_offsets_uploaded:
             return
-        if getattr(self, "_copy_idx_memo_key", None) == key:
+        if self._copy_idx_memo_key == key:
             return self._copy_idx_memo_val

As per coding guidelines: "initialize externally visible class members in the constructor".

🤖 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/deepseek_v4/cache_manager.py`
around lines 1120 - 1127, Initialize _block_offsets_uploaded,
_copy_idx_memo_key, and _copy_idx_memo_val in the constructor path
_prepare_page_table_tensor with their appropriate initial values, then replace
the getattr defaults in _ensure_device_block_offsets and the related memo-copy
logic with direct attribute access. Make the per-step ordering requirement
explicit by asserting that the step was initialized before
_ensure_device_block_offsets proceeds, while preserving
compute_sliding_block_tables as the reset point.

Source: Coding guidelines

Comment on lines +1372 to +1392
device_block_table: Optional[torch.Tensor] = None,
) -> None:
"""Build the shared INDEXER_COMPRESS compatibility block table."""
"""Build the shared INDEXER_COMPRESS compatibility block table.

When `device_block_table` is given the table is produced directly on the
device and `host_block_table` is left untouched.
"""
assert beam_width == 1, "DSV4 only supports beam width 1 now"
copy_idx = self.index_mapper.get_copy_index(request_ids, num_contexts, beam_width)
copy_idx = self._get_copy_index_cached(request_ids, num_contexts, beam_width)
pool_id = self._csa_indexer_compress_pool_id
scale = self._csa_indexer_compress_scale
if pool_id is None or scale is None:
raise RuntimeError("Missing INDEXER_COMPRESS pool metadata")
host_block_table[:num_seqs] = self._compute_shared_block_table(pool_id, scale, copy_idx)
if device_block_table is not None:
# Same rationale as copy_batch_compress_block_tables: build on device
# and skip both the host gather and the staging copy.
self._compute_shared_block_table_device(
pool_id, scale, copy_idx, device_block_table[:num_seqs]
)
else:
host_block_table[:num_seqs] = self._compute_shared_block_table(pool_id, scale, copy_idx)

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.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find every reader of the host indexer block-offset buffer.
rg -nP -C4 'host_indexer_k_cache_block_offsets' --type=py
# Find any other caller of the table builder.
rg -nP -C4 'copy_batch_indexer_compress_block_tables\s*\(' --type=py

Repository: NVIDIA/TensorRT-LLM

Length of output: 157


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- target file symbols ---'
ast-grep outline tensorrt_llm/_torch/attention_backend/sparse/deepseek_v4/cache_manager.py

printf '%s\n' '--- identifier references across the repository ---'
rg -n -C3 'host_indexer_k_cache_block_offsets|copy_batch_indexer_compress_block_tables|prepare_for_indexer_k_cache' .

printf '%s\n' '--- target implementation ---'
sed -n '1325,1410p' tensorrt_llm/_torch/attention_backend/sparse/deepseek_v4/cache_manager.py

printf '%s\n' '--- metadata caller context ---'
rg -l 'class DeepseekV4TrtllmAttentionMetadata|prepare_for_indexer_k_cache' tensorrt_llm

Repository: NVIDIA/TensorRT-LLM

Length of output: 29245


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- DeepseekV4 metadata caller ---'
sed -n '490,555p' tensorrt_llm/_torch/attention_backend/sparse/deepseek_v4/deepseek_v4.py

printf '%s\n' '--- DSA base preparation and host-table consumers ---'
sed -n '1035,1080p' tensorrt_llm/_torch/attention_backend/sparse/dsa.py
sed -n '1495,1530p' tensorrt_llm/_torch/attention_backend/sparse/dsa.py
sed -n '1565,1600p' tensorrt_llm/_torch/attention_backend/sparse/dsa.py
sed -n '2095,2130p' tensorrt_llm/_torch/attention_backend/sparse/dsa.py
sed -n '2208,2235p' tensorrt_llm/_torch/attention_backend/sparse/dsa.py

printf '%s\n' '--- DeepseekV4 class declaration and table attributes ---'
rg -n -C5 'class DeepseekV4|indexer_k_cache_block_offsets|host_indexer_k_cache_block_offsets' \
  tensorrt_llm/_torch/attention_backend/sparse/deepseek_v4/deepseek_v4.py \
  tensorrt_llm/_torch/attention_backend/sparse/dsa.py

Repository: NVIDIA/TensorRT-LLM

Length of output: 33430


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- metadata preparation order ---'
sed -n '700,765p' tensorrt_llm/_torch/attention_backend/sparse/dsa.py
rg -n -C5 'prepare_for_(indexer_k_cache|spec_decode|indices_conversion)|create_buffers_for_indexer|prepare_for_mtp' \
  tensorrt_llm/_torch/attention_backend/sparse/dsa.py \
  tensorrt_llm/_torch/attention_backend/sparse/deepseek_v4/deepseek_v4.py

printf '%s\n' '--- slot-mapping method declarations and callers ---'
rg -n -C4 'def (prepare_for_indices_conversion|prepare_for_spec_decode|prepare_for_block_tables)|_compute_slot_mappings\(' \
  tensorrt_llm/_torch/attention_backend/sparse/dsa.py \
  tensorrt_llm/_torch/attention_backend/sparse/deepseek_v4/deepseek_v4.py

printf '%s\n' '--- speculative state handling ---'
sed -n '135,225p' tensorrt_llm/_torch/speculative/interface.py

Repository: NVIDIA/TensorRT-LLM

Length of output: 33002


🏁 Script executed:

#!/bin/bash
set -eu

python3 - <<'PY'
from pathlib import Path
import re

caller = Path("tensorrt_llm/_torch/attention_backend/sparse/deepseek_v4/deepseek_v4.py").read_text()
manager = Path("tensorrt_llm/_torch/attention_backend/sparse/deepseek_v4/cache_manager.py").read_text()
dsa = Path("tensorrt_llm/_torch/attention_backend/sparse/dsa.py").read_text()
spec = Path("tensorrt_llm/_torch/speculative/interface.py").read_text()

call = re.search(
    r"copy_batch_indexer_compress_block_tables\((.*?)\n\s*\)",
    caller,
    re.S,
)
assert call and "device_block_table=self.indexer_k_cache_block_offsets" in call.group(1)
assert "self.host_indexer_k_cache_block_offsets" in call.group(1)

method = re.search(
    r"def copy_batch_indexer_compress_block_tables\(.*?(?=\n    @|\n    def )",
    manager,
    re.S,
)
assert method and "host_block_table[:num_seqs] = " in method.group(0)
assert "if device_block_table is not None:" in method.group(0)

required_readers = {
    "MTP expansion": "self.host_indexer_k_cache_block_offsets[" in dsa,
    "context slot mapping": "metadata.host_indexer_k_cache_block_offsets,\n" in dsa,
    "full-KV slot mapping": dsa.count("metadata.host_indexer_k_cache_block_offsets,") >= 2,
    "speculative replay": "m.host_indexer_k_cache_block_offsets[:m.num_seqs" in spec,
}
for name, present in required_readers.items():
    print(f"{name}: {'present' if present else 'absent'}")
    assert present

print("DeepseekV4 passes device_block_table and the cache-manager device branch skips the host write.")
print("The host table still has DSA and speculative consumers.")
PY

Repository: NVIDIA/TensorRT-LLM

Length of output: 419


Keep host_indexer_k_cache_block_offsets synchronized. The device path skips the host write, but DSA and speculative replay still read this table for MTP expansion and slot-mapping conversion. Populate the host table or update all consumers to use the device table.

🤖 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/deepseek_v4/cache_manager.py`
around lines 1372 - 1392, Update the shared block-table construction flow around
_compute_shared_block_table_device and _compute_shared_block_table so
host_indexer_k_cache_block_offsets remains synchronized when device_block_table
is provided. Populate the host table as well, or consistently redirect DSA and
speculative replay consumers to the device table, including MTP expansion and
slot-mapping conversion.

Comment on lines +123 to +182
def test_sliding_block_table_padding_keeps_bad_page_index():
"""Only [:_num_tables] is overwritten; the tail must stay BAD_PAGE_INDEX."""
layers, types, capacity, max_blocks, num_tables = 2, 5, 9, 4, 3
src = torch.arange(layers * types * num_tables * max_blocks, dtype=torch.int32).reshape(
layers, types, num_tables, max_blocks
)

# Reference: the original code filled the whole tensor first.
reference = torch.empty((layers, types, capacity, max_blocks), dtype=torch.int32)
reference.fill_(BAD_PAGE_INDEX)
reference[:, :, :num_tables, :].copy_(src)

# Optimized: pre-poison everything, fill only the tail, then copy the head.
optimized = torch.full((layers, types, capacity, max_blocks), -999999, dtype=torch.int32)
if num_tables < optimized.size(2):
optimized[:, :, num_tables:, :].fill_(BAD_PAGE_INDEX)
optimized[:, :, :num_tables, :].copy_(src)

torch.testing.assert_close(optimized, reference, rtol=0, atol=0)
assert not (optimized == -999999).any(), "head was not fully overwritten"


def test_sliding_block_table_padding_when_full():
"""With num_tables == capacity there is no tail to fill."""
layers, types, capacity, max_blocks = 1, 2, 6, 3
src = torch.arange(layers * types * capacity * max_blocks, dtype=torch.int32).reshape(
layers, types, capacity, max_blocks
)

reference = torch.empty((layers, types, capacity, max_blocks), dtype=torch.int32)
reference.fill_(BAD_PAGE_INDEX)
reference[:, :, :capacity, :].copy_(src)

optimized = torch.full((layers, types, capacity, max_blocks), -999999, dtype=torch.int32)
if capacity < optimized.size(2): # false: guard must skip the fill
optimized[:, :, capacity:, :].fill_(BAD_PAGE_INDEX)
optimized[:, :, :capacity, :].copy_(src)

torch.testing.assert_close(optimized, reference, rtol=0, atol=0)


def test_block_offsets_padding_keeps_bad_page_index():
"""copy_batch_block_offsets writes only beam 0 of [:_num_tables]."""
layers, capacity, beams, max_blocks, num_tables = 2, 7, 2, 3, 4
src = torch.arange(layers * num_tables * max_blocks, dtype=torch.int32).reshape(
layers, num_tables, max_blocks
)

reference = torch.empty((layers, capacity, beams, max_blocks), dtype=torch.int32)
reference.fill_(BAD_PAGE_INDEX)
reference[:, :num_tables, 0, :].copy_(src)

optimized = torch.full((layers, capacity, beams, max_blocks), -999999, dtype=torch.int32)
if num_tables < optimized.size(1):
optimized[:, num_tables:, :, :].fill_(BAD_PAGE_INDEX)
optimized[:, :num_tables, 1:, :].fill_(BAD_PAGE_INDEX)
optimized[:, :num_tables, 0, :].copy_(src)

torch.testing.assert_close(optimized, reference, rtol=0, atol=0)
assert not (optimized == -999999).any(), "some region was left unwritten"

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.

📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift

These three tests never call the code they claim to cover.

test_sliding_block_table_padding_keeps_bad_page_index, test_sliding_block_table_padding_when_full, and test_block_offsets_padding_keeps_bad_page_index reimplement the fill-and-copy logic inline and compare it against a local reference. Neither copy_batch_sliding_block_tables nor copy_batch_block_offsets is imported or invoked.

The tests prove that the arithmetic in the test file is self-consistent. They cannot detect a regression in cache_manager.py. If someone removes the dst_tensor[:, :_num_tables, 1:, :].fill_(BAD_PAGE_INDEX) line at cache_manager.py line 1291, every test here still passes. The file docstring states these cover the partial-overwrite behaviour, so the gap is easy to miss.

Call the real methods on a DeepseekV4CacheManager.__new__ instance with _num_tables and _precomputed_sliding_block_tables injected, the same pattern _manager_with_mapper already uses. Both methods assert a CUDA destination, so gate these three with the CUDA skip marker.

🤖 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
`@tests/unittest/_torch/attention/sparse/test_deepseek_v4_block_table_host_path.py`
around lines 123 - 182, The three padding tests must invoke the production
methods instead of duplicating their logic inline. Update
test_sliding_block_table_padding_keeps_bad_page_index,
test_sliding_block_table_padding_when_full, and
test_block_offsets_padding_keeps_bad_page_index to construct a
DeepseekV4CacheManager.__new__ instance with _num_tables and
_precomputed_sliding_block_tables configured as _manager_with_mapper does, pass
CUDA tensors to copy_batch_sliding_block_tables or copy_batch_block_offsets, and
apply the existing CUDA skip marker.

@hyukn

hyukn commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator Author

/bot run --disable-fail-fast --add-multi-gpu-test

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #64006 [ run ] triggered by Bot. Commit: d63bc15 Link to invocation

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #64006 [ run ] completed with state FAILURE. Commit: d63bc15
/LLM/main/L0_MergeRequest_PR pipeline #51939 completed with status: 'FAILURE'

CI Report

⚠️ Action Required:

  • Please check the failed tests and fix your PR
  • If you cannot view the failures, ask the CI triggerer to share details
  • Once fixed, request an NVIDIA team member to trigger CI again

CI Agent Failure Analysis

Link to invocation

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants