Skip to content

[None][feat] Add DSA sparse attention + indexer reference to VanillaAttention; unify sparse backend tests - #16309

Open
yihwang-nv wants to merge 12 commits into
NVIDIA:mainfrom
yihwang-nv:vanilla-dsa-attention
Open

[None][feat] Add DSA sparse attention + indexer reference to VanillaAttention; unify sparse backend tests#16309
yihwang-nv wants to merge 12 commits into
NVIDIA:mainfrom
yihwang-nv:vanilla-dsa-attention

Conversation

@yihwang-nv

@yihwang-nv yihwang-nv commented Jul 13, 2026

Copy link
Copy Markdown
Collaborator

Summary by CodeRabbit

  • New Features

    • Added support for DSA sparse attention through the Vanilla attention backend.
    • Added sparse MLA processing, including RoPE handling, selected-token attention, and latent-cache support.
    • Added a reference indexer for sparse query scoring and top-k selection.
  • Bug Fixes

    • Improved validation and error reporting for unsupported or incomplete sparse-attention inputs.
  • Tests

    • Expanded backend coverage for sparse MLA scenarios.
    • Added deterministic sparse selection checks and Vanilla golden-output comparisons.

Description

Integrate DSA (DeepSeek Sparse Attention) into the VanillaAttention reference backend and express sparse-attention tests through the unified model-driven backend harness, so a sparse algorithm plugs into Vanilla the same way it does into TRTLLM. Vanilla is intended as the reference platform for the planned TRTLLM sparse refactor.

  1. Vanilla DSA attention-over-selection (vanilla.py). Backend-neutral sparse_kv_predict / sparse_attn_predict hooks matching the production contract (results written into forward_args.sparse_prediction), plus the _mla_forward_sparse golden: applies MLA RoPE, appends and reconstructs paged latent KV across context and generation, validates top-k shape/dtype/padding/bounds/causality before mutating the cache, gathers selected latent K/V, and evaluates absorbed MLA with an FP32 softmax.

  2. Unified sparse test harness (backend_case.py, model_attn_config.py, backend_capability.py, test_attention_backends.py). A sparse workload is an ordinary ModelAttnConfig (deepseekv3_2_dsa_mla) sitting next to the dense MLA configs. It carries the real user-facing sparse_attention_config — lowered via the production to_sparse_params / to_sparse_metadata_params / sparse KV-cache manager — plus a backend-neutral SparseHarnessConfig. The runner injects deterministic causal selections and compares every supported backend against the Vanilla golden. This replaces the standalone DSA test files.

  3. VanillaIndexer (vanilla.py). An FP32 reference for the production DSA / DeepSeek-V4 Indexer, wrapping a production indexer instance so it runs against that indexer's own weights. It is a standalone reference (not wired into forward), and test_sparse_mla_forward.py now builds its DeepSeek-V4 reference top-k on it, removing the duplicated projection / scoring / top-k helpers.

  4. Block-selection scaffold (backend_case.py). A reusable causal block-index builder plus validation and dispatch stubs for the future selection_unit="block" family (e.g. RocketKV); inert until a block config lands.

Scope is limited to the internal Vanilla backend and sparse tests. The DSA indexer stays external, the production backend remains DSATrtllmAttention, and no other sparse algorithm or public API is changed. The explicit Vanilla paths are correctness oracles, not performance paths.

Test Coverage

Verified on a Blackwell GPU (sm 100), where the TRTLLM DSA path runs and is compared against the Vanilla golden:

  • pytest tests/unittest/_torch/attention/test_attention_backends.py -k dsa → 6 passed (deepseekv3_2_dsa_mla × {ctx, gen, mix} × {random, singleton}).
  • pytest tests/unittest/_torch/attention/sparse/test_sparse_mla_forward.py -k "deepseek_v4 and auto and (small_prefill or small_decode or small_mixed)" → passed (DeepSeek-V4 indexer reference now driven by VanillaIndexer).
  • py_compile and import checks pass.

On sm < 100, TRTLLM DSA is skipped and only the Vanilla golden (plus its analytic singleton oracle) runs.

PR Checklist

  • PR description clearly explains what and why.
  • PR follows the TensorRT-LLM coding guidelines to the best of my knowledge.
  • Test cases cover the new code path.
  • No public API change or new dependency is introduced.
  • CODEOWNERS and the architecture diagram do not require updates.

GitHub Bot Help

To see a list of available CI bot commands, comment /bot help.

Signed-off-by: Yihan Wang <yihwang@nvidia.com>
Signed-off-by: Yihan Wang <yihwang@nvidia.com>
@yihwang-nv
yihwang-nv requested a review from wenmingw July 15, 2026 06:13
@yihwang-nv

Copy link
Copy Markdown
Collaborator Author

/bot run --disable-fail-fast

@yihwang-nv yihwang-nv changed the title [None][feat] Add DSA sparse attention to VanillaAttention [None][feat] Add DSA sparse attention + indexer reference to VanillaAttention; unify sparse backend tests Jul 15, 2026
@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #59376 [ run ] triggered by Bot. Commit: b3238f5 Link to invocation

# (production replays a captured decode graph); it must still match the
# eager golden.
if case.is_gen_only:
if case.is_gen_only and not case.is_sparse:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Why we cannot enable cuda graph for sparse case?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This backend-only path is an eager correctness oracle: the standalone sparse runner validates the injected request-local selections on the host and rebuilds each request's logical cache with Python loops, neither of which is graph-capturable. Capturing would require rewriting the runner into a static-buffer form that no longer matches its role. Graph coverage of the DSA decode kernel is exercised at the model level, not by this backend oracle. I added a code comment making the exclusion explicit; happy to file a follow-up if you'd like graph coverage added here.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Please add a TODO here, let's try to add it in a follow-up PR

Comment thread tests/unittest/_torch/attention/backend_case.py Outdated
Comment thread tests/unittest/_torch/attention/backend_case.py Outdated
Comment thread tests/unittest/_torch/attention/model_attn_config.py Outdated
@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #59376 [ run ] completed with state SUCCESS. Commit: b3238f5
/LLM/main/L0_MergeRequest_PR pipeline #47850 completed with status: 'SUCCESS'

CI Report

Link to invocation

- Register DSA in the vanilla sparse-backend factory so VanillaAttention is
  built through create_attention like every other backend (no special-casing
  in the sparse runner); update_quant_config is now called unconditionally.
- Derive sparse family / selection unit / top-k from is_mla and
  sparse_attention_config, and move the model-agnostic sweep dimensions to
  shared constants, so ModelAttnConfig no longer carries a SparseHarnessConfig
  or hf_config_overrides/atol/rtol. Sparse tolerance is derived in _tolerances.
- Clarify why the backend-only sparse oracle skips the captured-CUDA-graph path.

Signed-off-by: Yihan Wang <yihwang@nvidia.com>
@yihwang-nv

Copy link
Copy Markdown
Collaborator Author

/bot run --disable-fail-fast

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #59435 [ run ] triggered by Bot. Commit: 108df3f Link to invocation

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #59435 [ run ] completed with state SUCCESS. Commit: 108df3f
/LLM/main/L0_MergeRequest_PR pipeline #47904 completed with status: 'SUCCESS'
Pipeline passed with automatic retried tests. Check the rerun report for details.

CI Report

Link to invocation

@yihwang-nv
yihwang-nv marked this pull request as ready for review July 15, 2026 15:05
@yihwang-nv
yihwang-nv requested a review from a team as a code owner July 15, 2026 15:06
@yihwang-nv
yihwang-nv requested review from PerkzZheng and yunruis July 15, 2026 15:06
@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

DSA sparse MLA support is added to VanillaAttention, including RoPE preparation, paged latent-cache loading, selected-token attention, sparse prediction routing, and an fp32 VanillaIndexer. Backend capability detection, sparse case generation, execution harnesses, and golden comparisons are updated.

Changes

DSA sparse MLA support

Layer / File(s) Summary
Sparse configuration and sweep generation
tests/unittest/_torch/attention/model_attn_config.py, tests/unittest/_torch/attention/backend_capability.py, tests/unittest/_torch/attention/test_attention_backends.py
Adds sparse configuration fields and DSA MLA coverage, marks VANILLA sparse support, gates TRTLLM DSA cases by SM version, and generates sparse backend cases.
Vanilla sparse MLA execution
tensorrt_llm/_torch/attention_backend/sparse/utils.py, tensorrt_llm/_torch/attention_backend/vanilla.py
Routes DSA to VanillaAttention, adds sparse MLA RoPE and cache handling, validates selected indices, executes selected-token attention, and exposes VanillaIndexer.
Sparse backend harness execution
tests/unittest/_torch/attention/backend_case.py
Stores sparse case contracts, generates sparse inputs, builds sparse cache managers, dispatches sparse MLA runs, applies sparse tolerances, and validates singleton selections.
Sparse golden comparison tests
tests/unittest/_torch/attention/sparse/dsa/test_dsa_sparse_mla.py, tests/unittest/_torch/attention/sparse/test_sparse_mla_forward.py
Uses deterministic top-k selections, compares TRTLLM outputs with VANILLA outputs, and reuses VanillaIndexer for top-k reference calculations.

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

Sequence Diagram(s)

sequenceDiagram
  participant VanillaAttention
  participant SparsePredictors
  participant PagedKVCache
  participant DSAAttention
  VanillaAttention->>SparsePredictors: compute sparse KV and attention selections
  VanillaAttention->>DSAAttention: pass selected indices and MLA inputs
  DSAAttention->>PagedKVCache: load latent-cache prefix
  DSAAttention->>VanillaAttention: return selected-token attention output
Loading

Suggested reviewers: wenmingw, yuxianq, lfr-0531, bobboli

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% 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
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.
Title check ✅ Passed The title matches the main change and follows the required ticket/type format.
Description check ✅ Passed The description includes the required summary, description, test coverage, and checklist sections with relevant details.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

Comment thread tests/unittest/_torch/attention/backend_capability.py Outdated
Comment thread tests/unittest/_torch/attention/model_attn_config.py Outdated
Comment thread tests/unittest/_torch/attention/test_attention_backends.py Outdated
Comment thread tests/unittest/_torch/attention/test_attention_backends.py Outdated
Comment thread tests/unittest/_torch/attention/test_attention_backends.py Outdated
Comment thread tests/unittest/_torch/attention/test_attention_backends.py Outdated
Comment thread tests/unittest/_torch/attention/test_attention_backends.py Outdated
Comment thread tests/unittest/_torch/attention/test_attention_backends.py Outdated
@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #59621 [ run ] completed with state ABORTED. Commit: 419e2fe

Link to invocation

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #59643 [ kill ] completed with state SUCCESS. Commit: b5a8270
Successfully killed previous jobs for commit b5a8270

Link to invocation

if case.is_mla and case.sparse_selection_unit == "token":
inputs = generate_sparse_mla_inputs(case, seed)
elif case.sparse_selection_unit == "block":
inputs = generate_sparse_block_inputs(case, seed)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Since sparse mla uses absorption, can we view it as a special case of generate_mla_gen_inputs? Thus we can use a single generate_mla_gen_inputs for all three cases generate_mla_gen_inputs/generate_sparse_block_inputs/generate_sparse_mla_inputs, just need to generate an extra topk_indices for sparse case.

block_size = getattr(sparse_params, "indices_block_size")
selection_gen = torch.Generator(device="cuda").manual_seed(seed + 1)
_ = _build_sparse_block_indices(case, selection_gen, block_size)
raise NotImplementedError(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

If we don't support block sparse attention now, I think we should avoid to introduce abstraction for it, e.g., sparse_selection_unit. Whether an abstraction is proper should be validated by an real implementation, otherwise agents will inject some seems-good garbage in some corner.

return indices, singleton_oracle_mask


def _build_sparse_block_indices(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Same comments for all block sparse related code.

if sparse_config is not None and backend != "VANILLA":
cls = get_sparse_attn_kv_cache_manager(sparse_config)
if model_config is None or pretrained_config is None:
raise ValueError("Sparse cache manager requires model and pretrained configs")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

model_config/pretrained_config are optional for sparse cache manager, so that we don't need to create and pass them.

dtype=torch_dtype_to_binding(case.compute_dtype),
)

if sparse_config is not None and backend != "VANILLA":

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Can vanilla use sparse cache manager? If it can, we don't need to exclude vanilla.

cu_q_seqlens = torch.empty(case.num_seqs + 1, dtype=torch.int32, device="cuda")
cu_kv_seqlens = torch.empty(case.num_seqs + 1, dtype=torch.int32, device="cuda")
fmha_scheduler_counter = torch.empty(1, dtype=torch.uint32, device="cuda")
attn.mla_rope_generation(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Don't need to call mla_rope_generation for trtllm backend. trtllm backend does not call rope inside backend forward in this case, we should remove rope from vanilla to align with it (vanilla does not fuse rope, MLA will run rope for it, we should keep this behavior). But mla_rope_generation also update KV cache, to ensure KV cache is updated for trtllm backend, we should pass skip_mla_rope_generation=True in forward args like _run_mla_gen_backend, which hints the trtllm backend that mla_rope_generation is skipped, and it will add an update KV cache step before running fmha.

torch.testing.assert_close(actual, expected, atol=atol, rtol=rtol)


def _run_sparse_mla_backend(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Can we merge _run_sparse_mla_backend back to _run_mla_gen_backend?

num_kv_heads: Optional[int] = None,
quant_config: Optional[QuantConfig] = None,
q_scaling: Optional[float] = None,
pos_embd_params: Optional[PositionalEmbeddingParams] = None,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

vanilla does not fuse rope, should we remove rope from vanilla?

return attn_output


class VanillaIndexer:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

It is only used by tests/unittest/_torch/attention/sparse/test_sparse_mla_forward.py, not our test suite. It has different API with DSA's Indexer (

). Only DSA and its derived ds v4 attention use Indexer class, not other sparse attention uses it, so it is no general. How about removing VanillaIndexer and revert change for tests/unittest/_torch/attention/sparse/test_sparse_mla_forward.py?

"Vanilla selected MLA currently supports only DSA")
kv_idx, kv_off = self.sparse_kv_predict(q, k, metadata,
forward_args)
at_idx, at_off = self.sparse_attn_predict(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Use full name for kv_idx/kv_off/at_idx/at_off to make them readable. E.g., you can use the same field name with sparse_prediction sparse_kv_indices/sparse_kv_offsets/sparse_attn_indices/sparse_attn_offsets so that we don't need to convert names in our mind.

A token selection is a block selection with block_size == 1, so the harness no
longer distinguishes token vs block selection units. Selection granularity is
derived from the config (get_indices_block_size(); 1 for DSA) and the single
selected-MLA path is exercised by DSA. Removes the unverified block-selection
scaffolding (builder + runner/generator stubs) and the selection-unit plumbing;
VanillaAttention._mla_forward_sparse guards block_size == 1. block_size > 1 is
deferred to a follow-up PR (RocketKV / MiniMax-M3).

Signed-off-by: Yihan Wang <yihwang@nvidia.com>
…la forward

Signed-off-by: Yihan Wang <yihwang@nvidia.com>
sparse_attn_indices,
forward_args.attention_input_type,
)
if self.sparse_params is not None:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This block is unreachable, we can remove it.

rotated = torch.stack((out1, out2), dim=-1).flatten(-2)
return torch.cat((rotated, x_pass), dim=-1)

def _prepare_sparse_mla_inputs(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Without rope, both _apply_rotary_embedding and _prepare_sparse_mla_inputs are unnecessary.

…o its test

VanillaIndexer is a test-only fp32 reference used only by test_sparse_mla_forward.py
and has a different API from the production DSA Indexer, so it does not belong in
the production attention backend.

Signed-off-by: Yihan Wang <yihwang@nvidia.com>
raise ValueError(
"Vanilla sparse MLA expects absorbed queries and latent cache, "
"not explicit K/V tensors")
return self._mla_forward_sparse(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The main diff between _mla_forward_sparse and _mla_forward_generation is that _mla_forward_sparse use paged cache but _mla_forward_generation uses linear cache. Can we make vanilla always use linear cache, so that we can rename _mla_forward_generation to _mla_forward_absorption and add topk_indices handle to it to support sparse attention?

…_config

The sparse KV-cache manager takes sparse_attention_config directly, so
model_config/pretrained_config are not needed, and Vanilla can use the same
sparse cache manager as the other backends (no VANILLA special-case).

Signed-off-by: Yihan Wang <yihwang@nvidia.com>
mla_bmm2_scale = None
quant_q_buffer = None

gen_layers[layer_idx].mla_rope_generation(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Both of the rope and page cache design come from the fact that tests/unittest/_torch/attention/sparse/dsa/test_dsa_sparse_mla.py uses this design. We can also update the tests in this file to remove rope for both vanilla and trtllm backend, and use linear cache for vanilla, so it aligns with the original design of vanilla.

…tion

- Remove VanillaAttention's internal sparse-MLA RoPE. The harness now applies
  RoPE on the host and feeds pre-formed inputs, using skip_mla_rope_generation
  for the absorbed generation path (matches production: the MLA module runs
  RoPE, the attention backend does not).
- Context phase feeds raw inputs so the TRTLLM MLA context kernel ropes once
  (avoids double RoPE); Vanilla and the generation path use the pre-RoPE'd
  inputs.
- Gate TRTLLM DSA to sm>=100: the trtllm-gen DynamicTokenSparse FMHA kernels
  are Blackwell-only; on Hopper MLA generation falls back to dense FlashMLA,
  which has no per-token sparse path.
- Drop the block-unit (sparse_block_size) abstraction; the suite is
  token-selection only (DSA / DeepSeek-V4). RocketKV block sparse is out of
  scope for this suite.

Signed-off-by: Yihan Wang <yihwang@nvidia.com>
@yihwang-nv

Copy link
Copy Markdown
Collaborator Author

/bot run --disable-fail-fast

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #60331 [ run ] triggered by Bot. Commit: 1592fb9 Link to invocation

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #60331 [ run ] completed with state FAILURE. Commit: 1592fb9
/LLM/main/L0_MergeRequest_PR pipeline #48676 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

num_kv_heads: Optional[int] = None,
quant_config: Optional[QuantConfig] = None,
q_scaling: Optional[float] = None,
pos_embd_params: Optional[PositionalEmbeddingParams] = None,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

pos_embd_params is unused now

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Removed — dropped pos_embd_params from VanillaAttention.__init__ and its import; it only fed the sparse-MLA RoPE that was removed. create_attention passes it as a kwarg, which the base backend swallows, so it's a no-op.

HAS_FLASH_MLA = False


class VanillaIndexer:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

If we don't use VanillaIndexer in our test suite, should we fully revert changes in this file?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Done — fully reverted this file to main. VanillaIndexer is not used by the model-driven suite and has a different API than the production DSA Indexer, so the original free-function version is restored.

…indexer test

- Remove the now-unused pos_embd_params from VanillaAttention.__init__ (and its
  import); it only fed the sparse-MLA RoPE that was removed. create_attention
  passes it as a kwarg, which the base backend swallows, so this is a no-op.
- Fully revert tests/unittest/_torch/attention/sparse/test_sparse_mla_forward.py:
  VanillaIndexer is not used by the model-driven suite and has a different API
  than the production DSA Indexer, so restore the original file.

Signed-off-by: Yihan Wang <yihwang@nvidia.com>

per_token_outputs = []
for token_idx in range(q_len):
row = topk_indices[token_offset + token_idx]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Can we use topk_indices to select kv and concat them in advance, so that we can directly use torch.nn.functional.scaled_dot_product_attention like dense mla case? If so, we can easily merge _mla_forward_sparse to _mla_forward_generation.

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.

3 participants