Skip to content

[None][feat] add CuteDSL FP8/FP16 MLA decode attention fmha lib - #15138

Open
haow-nv wants to merge 22 commits into
NVIDIA:mainfrom
haow-nv:feat/cutedsl-mla-decode-fp8
Open

[None][feat] add CuteDSL FP8/FP16 MLA decode attention fmha lib#15138
haow-nv wants to merge 22 commits into
NVIDIA:mainfrom
haow-nv:feat/cutedsl-mla-decode-fp8

Conversation

@haow-nv

@haow-nv haow-nv commented Jun 9, 2026

Copy link
Copy Markdown
Collaborator

Description

Adds CuteDSL MLA decode as an internal FMHA library of the TRTLLM attention backend (not a separate attn_backend). The new CuteDslMlaFmha library intercepts the generation-phase (decode) MLA portion of a batch and dispatches it to Blackwell CuTe DSL kernels; context-phase requests and anything the library rejects fall through to the next library in the ordered list (flashinfer_trtllm_gen, then fallback).

Selection & gating

  • Registered as cute_dsl_mla in the FMHA library registry. The default order is cute_dsl_mla,flashinfer_trtllm_gen,fallback; override with TLLM_FMHA_LIBS (exact list, e.g. TLLM_FMHA_LIBS=flashinfer_trtllm_gen,fallback, or deltas, e.g. TLLM_FMHA_LIBS=-cute_dsl_mla).
  • is_available() (static): SM100/103 + nvidia-cutlass-dsl installed + MLA with kv_lora_rank=512, qk_rope_head_dim=64, num_heads <= 128 (DeepSeek geometry).
  • Per-forward: the kernel's own can_implement check, plus a perf allowlist — the library only takes (num_heads, seq_len_q) in {(16, 2), (16, 4), (128, 1)}, the shapes where CuteDSL measured as an end-to-end win over TRTLLM-Gen in DeepSeek-V3 A/B runs (TP8 + MTP, and attention-DP without MTP). All other shapes fall back to TRTLLM-Gen with a debug log.

Main pieces

  • attention_backend/fmha/cute_dsl.py: CuteDslMlaFmha(PhasedFmha) — generation-phase hook that prepares latent/rope Q views, paged-KV page table, and per-request cache lengths, then calls the CuteDSL decode op. FP8 KV cache routes to the FP8 kernel; FP16/BF16 activations route to the FP16/BF16 kernel.
  • cute_dsl_kernels/blackwell/attention/mla/: vendored Blackwell CuTe DSL MLA decode kernels (FP8 and FP16/BF16) + helpers, adapted from the CUTLASS MLA reference and aligned with FlashInfer's mla_decode (persistent and split-KV modes, seq_len_q > 1 causal masking with fold-sq for MTP).
  • custom_ops/cute_dsl_custom_ops.py: registers trtllm::cute_dsl_mla_decode_{fp8,fp16}_blackwell and the kernel runner (JIT compile/cache). split_kv and is_persistent are AutoTuner tactic elements chosen per shape (power-of-two split-KV candidate ladder, batch-bucketed tuning config); o/lse/workspace are mutated in place, and the workspace is sized batch-independently so CUDA-graph capture across batch sizes stays valid.
  • pyexecutor/model_engine.py: adds one mixed context+generation warmup request so TRTLLM-Gen kernels are JIT-warmed for the mixed batches that cute_dsl_mla does not take (it is decode-only).
  • modules/fused_moe/moe_scheduler.py: attention-DP empty-chunk substitution fix — the substitution now updates every rank's slot in the collective sizes (emptiness is deterministic from the cross-rank-consistent all_rank_num_tokens_list), keeping the variable-size all-gather consistent across ranks. Previously each rank only patched its own slot, which deadlocked warmup with imbalanced attention-DP batches.
  • modules/ATTENTION_DEVELOPER_GUIDE.md and lint-exclusion lists (.pre-commit-config.yaml, legacy-files.txt, pyproject.toml, ruff-legacy.toml) updated for the new library and vendored kernel files.

MTP (seq_len_q > 1) and CUDA graphs are supported on the CuteDSL path.

Test Coverage

Validated locally on B200 (SM100); both suites are gated on get_sm_version() in (100, 103) and IS_CUTLASS_DSL_AVAILABLE.

  • tests/unittest/_torch/attention/test_attention_mla.py (existing): full MLA module dispatch passes with cute_dsl_mla in the default library order.

End-to-end: DeepSeek-V3 FP8 on 8x B200 (TP8/EP8, CUDA graphs on) runs green with and without attention-DP and MTP — no fallbacks, correct outputs, MTP acceptance rate identical to the TRTLLM-Gen baseline; throughput on allowlisted shapes is at parity or better, which is what the perf allowlist encodes.

# ADP seq_len_q baseline (tok/s) cutedsl (tok/s) speedup
1 OFF 1 3081.9 3082.0 +0.00%
2 OFF 2 4304.5 4358.6 +1.26%
3 OFF 4 4771.4 4911.6 +2.94%
4 ON 1 2676.1 2719.8 +1.63%
5 ON 2 3918.0 3919.7 +0.04%
6 ON 4 4403.5 4458.4 +1.25%

PR Checklist

  • Please check this after reviewing the above items as appropriate for this PR.

GitHub Bot Help

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

Dev Engineer Review

  • Added a Blackwell-only FMHA backend (CuteDslMlaFmha) under the cute_dsl_mla registry key and exported it from the FMHA package for dispatch of eligible DeepSeek MLA generation-phase decode requests to CuTe DSL kernels (FP8 and FP16/BF16).
  • Implemented strict runtime gating/rejection in tensorrt_llm/_torch/attention_backend/fmha/cute_dsl.py, including CuTe/SM100/103 availability and tight geometry/mode constraints (decode-only, specific lora/head-dim expectations, head-count limits, and rejection of unsupported speculative/beam/tree/custom mask modes).
  • Added CuTe DSL MLA decode custom ops plus an autotuned CUDA kernel runner (CuteDSLNVMlaDecodeBlackwellRunner) in tensorrt_llm/_torch/custom_ops/cute_dsl_custom_ops.py:
    • enumerates feasible MMA tilers via can_implement,
    • autotunes split-K and persistence,
    • validates and slices workspace for LSE/output and optional split-K intermediates,
    • constructs Cute tensors from input page-table/cache/paged KV layouts,
    • includes CUDA-graph-safe caching for FP8 decode scales.
  • Added new Blackwell CuTe DSL MLA kernel modules under tensorrt_llm/_torch/cute_dsl_kernels/blackwell/attention/mla/:
    • mla_decode_fp8.py, mla_decode_fp16.py,
    • mla_helpers.py (static tile scheduling helpers for persistent/non-persistent modes),
    • and updated package __init__.py exports.
  • Updated FMHA documentation to include cute_dsl_mla in the default/ordering guidance (ATTENTION_DEVELOPER_GUIDE.md).
  • Fixed attention-DP empty-chunk collective sizing in ExternalCommMoEScheduler._forward_multiple_chunks (tensorrt_llm/_torch/modules/fused_moe/moe_scheduler.py) by ensuring all ranks use identical sizes vectors when split_chunk yields 0-token chunks on some ranks.
  • Adjusted TRTLLM-Gen FMHA JIT warmup configuration in tensorrt_llm/_torch/pyexecutor/model_engine.py to enable a mixed context+generation warmup only under the specified compatibility conditions.
  • Updated lint/config allowlists so the newly added CuTe DSL MLA Python modules are included in pre-commit and Ruff scopes (e.g., .pre-commit-config.yaml, legacy-files.txt, pyproject.toml, ruff-legacy.toml).

Review notes / risk areas

  • Correctness/performance risks concentrate in eligibility/rejection gating, FP8 vs FP16/BF16 dispatch feasibility (can_implement + tiler selection), workspace sizing/slicing (including split-K paths), and persistent vs non-persistent static tile scheduling.
  • Given the described CI flakiness patterns, failures should be revalidated with the requested multi-GPU CI invocations, paying special attention to CUDA-graph capture/warmup behavior and allowlisted shape routing.

QA Engineer Review

No test changes.

@limin2021
limin2021 requested review from hyukn, limin2021 and yuxianq June 9, 2026 05:06
@haow-nv
haow-nv force-pushed the feat/cutedsl-mla-decode-fp8 branch from 0596464 to e7b9ea6 Compare June 24, 2026 03:07
@haow-nv haow-nv changed the title [None][feat] add CuteDSL FP8/FP16 MLA decode attention backend [None][feat] add CuteDSL FP8/FP16 MLA decode attention fmha lib Jun 30, 2026
@haow-nv

haow-nv commented Jun 30, 2026

Copy link
Copy Markdown
Collaborator Author

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

@haow-nv
haow-nv marked this pull request as ready for review June 30, 2026 07:56
@haow-nv
haow-nv requested review from a team as code owners June 30, 2026 07:56
@haow-nv
haow-nv requested a review from a team June 30, 2026 07:56
@haow-nv
haow-nv requested a review from a team as a code owner June 30, 2026 07:56
@haow-nv
haow-nv requested review from dpitman-nvda and yiqingy0 June 30, 2026 07:56
@coderabbitai

coderabbitai Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds a Blackwell CuTe DSL MLA decode backend with static tile scheduling, FP8 and FP16/BF16 custom operations, FMHA registration, runtime gating, paged-KV execution, warmup integration, and lint configuration. Also fixes cross-rank empty-chunk handling in the MoE scheduler.

Changes

CuTe DSL MLA Decode Backend

Layer / File(s) Summary
MLA tile scheduler and kernel package
tensorrt_llm/_torch/cute_dsl_kernels/blackwell/attention/...
Adds static tile scheduling parameters, work-tile tracking, persistent/non-persistent scheduling, and MLA package exports.
Blackwell MLA custom operations
tensorrt_llm/_torch/custom_ops/cute_dsl_custom_ops.py
Adds tactic selection, compiled-kernel caching, and FP8/FP16/BF16 custom decode operations.
CuteDslMlaFmha backend
tensorrt_llm/_torch/attention_backend/fmha/cute_dsl.py
Adds eligibility checks, dtype and page-table handling, split-KV computation, paged-KV preparation, scaling, and MLA generation execution.
FMHA integration and developer configuration
tensorrt_llm/_torch/attention_backend/fmha/..., tensorrt_llm/_torch/pyexecutor/model_engine.py, tensorrt_llm/_torch/modules/ATTENTION_DEVELOPER_GUIDE.md, .pre-commit-config.yaml, legacy-files.txt, pyproject.toml, ruff-legacy.toml
Registers and exports the backend, adjusts warmup requests and documentation, and adds MLA files to linting scopes.

MoE Scheduler Fix

Layer / File(s) Summary
Cross-rank empty-chunk size substitution
tensorrt_llm/_torch/modules/fused_moe/moe_scheduler.py
Propagates chunk-0 token counts to all ranks with empty chunks while retaining local tensor substitution for unused chunks.

Estimated code review effort: 5 (Critical) | ~90 minutes

Sequence Diagram(s)

sequenceDiagram
  participant FMHARegistry
  participant CuteDslMlaFmha
  participant MLADecodeCustomOp
  participant CuteDSLNVMlaDecodeBlackwellRunner
  participant CompiledMLAKernel
  FMHARegistry->>CuteDslMlaFmha: select cute_dsl_mla backend
  CuteDslMlaFmha->>CuteDslMlaFmha: validate support and prepare paged KV
  CuteDslMlaFmha->>MLADecodeCustomOp: invoke FP8 or FP16/BF16 decode
  MLADecodeCustomOp->>CuteDSLNVMlaDecodeBlackwellRunner: select tactic and launch
  CuteDSLNVMlaDecodeBlackwellRunner->>CompiledMLAKernel: compile-cache and execute
  CompiledMLAKernel->>CuteDslMlaFmha: write output and workspace
Loading

Suggested reviewers: bowenfu, chienchunhung, yihuilu512

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 30.77% 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 is concise, follows the required ticket/type pattern, and accurately summarizes the new CuteDSL MLA decode FMHA library.
Description check ✅ Passed The description matches the template well, with clear Description, Test Coverage, and PR Checklist sections filled in.
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 (1)
tensorrt_llm/_torch/custom_ops/cute_dsl_custom_ops.py (1)

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

Optional: de-duplicate the FP8/FP16 dispatch bodies.

After the per-dtype in_dtype selection, both ops run an identical block (SM gate, runner construction, inputs list, choose_one, runner(...)), and the two register_fake stubs are byte-identical. Folding the shared portion into one helper keeps the FP8 and FP16 paths from diverging during future maintenance.

♻️ Sketch of a shared dispatch helper
def _run_cute_dsl_mla_decode_blackwell(
    op_name, in_dtype, q_latent, q_rope, c_latent, c_rope, page_table,
    cache_seqs, block_split_kvs, o, lse, workspace, num_heads, seq_len_q,
    page_size, is_persistent, is_var_seq, is_var_split_kv,
    split_kv, softmax_scale, output_scale,
):
    runner = CuteDSLNVMlaDecodeBlackwellRunner(
        in_dtype=in_dtype, num_heads=num_heads, seq_len_q=seq_len_q,
        page_size=page_size, is_persistent=is_persistent,
        is_var_seq=is_var_seq, is_var_split_kv=is_var_split_kv,
    )
    inputs = [q_latent, q_rope, c_latent, c_rope, page_table, cache_seqs,
              block_split_kvs, o, lse, workspace]
    _, best_tactic = AutoTuner.get().choose_one(
        op_name, [runner], runner.get_tuning_config(), inputs)
    runner(inputs, tactic=best_tactic, split_kv=split_kv,
           softmax_scale=softmax_scale, output_scale=output_scale)

Each @torch.library.custom_op then keeps only its SM check + dtype resolution and delegates the rest. The register_fake bodies (return None) can share a single function passed to both registrations.

🤖 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/custom_ops/cute_dsl_custom_ops.py` around lines 7666 -
7857, The FP8 and FP16 Blackwell MLA decode ops currently duplicate the same SM
gate, runner setup, tuning, and execution flow in
cute_dsl_mla_decode_fp8_blackwell and cute_dsl_mla_decode_fp16_blackwell, and
the two register_fake stubs are identical. Extract the shared dispatch logic
into a helper such as _run_cute_dsl_mla_decode_blackwell that takes op_name and
in_dtype, then have each custom_op only handle its dtype validation/resolution
before delegating. Reuse a single fake implementation for both
`@torch.library.register_fake` registrations so the two paths stay aligned and
easier to maintain.
🤖 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/fmha/cute_dsl.py`:
- Around line 522-523: The page-table adjustment in the `cute_dsl.py` path that
folds in the interleaved layer slot is using only `layer_in_pool`, which can
point to the wrong KV page after `kv_pool` is re-viewed as packed combined
slots. Update the logic around `page_table` so the layer contribution is scaled
by `layers_in_pool` (matching the logical mapping used by the packed view) and
keep the behavior in the same `layers_in_pool > 1` branch.
- Around line 539-644: The split-KV selection in cute_dsl.py can still enable
is_var_split_kv even when fixed-sequence mode is forced, which creates an
invalid kernel configuration. In the decode path around
_split_kv_from_max_splits, _compute_block_split_kvs, and the is_var_seq toggle,
gate the variable split-KV logic on is_var_seq being true, and force split_kv
back to 1 when TLLM_CUTE_DSL_VAR_SEQ=0 so is_var_split_kv can never be set in
fixed-sequence mode. Ensure the workspace allocation and downstream kernel
arguments follow the adjusted split_kv value.

In `@tests/unittest/_torch/attention/test_cute_dsl_mla_decode.py`:
- Around line 82-92: The FP16 decode path is still advertised by
CuteDslMlaFmha._get_kernel_dtype() and routes to
cute_dsl_mla_decode_fp16_blackwell, but the test matrix in
test_cute_dsl_mla_decode.py now omits torch.float16 and hides the crash. Update
the runtime in tensorrt_llm/_torch/attention_backend/fmha/cute_dsl.py to gate
FP16 out until it is fixed, or add an explicit xfail/regression test that
exercises the FP16 path and documents the aborting behavior. Keep the test
coverage aligned with the actual supported kernel dtypes so the unsupported FP16
path is not silently masked.

---

Nitpick comments:
In `@tensorrt_llm/_torch/custom_ops/cute_dsl_custom_ops.py`:
- Around line 7666-7857: The FP8 and FP16 Blackwell MLA decode ops currently
duplicate the same SM gate, runner setup, tuning, and execution flow in
cute_dsl_mla_decode_fp8_blackwell and cute_dsl_mla_decode_fp16_blackwell, and
the two register_fake stubs are identical. Extract the shared dispatch logic
into a helper such as _run_cute_dsl_mla_decode_blackwell that takes op_name and
in_dtype, then have each custom_op only handle its dtype validation/resolution
before delegating. Reuse a single fake implementation for both
`@torch.library.register_fake` registrations so the two paths stay aligned and
easier to maintain.
🪄 Autofix (Beta)

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: e1c74753-bc97-42cb-8418-7ace8aa2ef65

📥 Commits

Reviewing files that changed from the base of the PR and between cdd6230 and cd3edb0.

📒 Files selected for processing (19)
  • .pre-commit-config.yaml
  • legacy-files.txt
  • pyproject.toml
  • ruff-legacy.toml
  • tensorrt_llm/_torch/attention_backend/fmha/__init__.py
  • tensorrt_llm/_torch/attention_backend/fmha/cute_dsl.py
  • tensorrt_llm/_torch/attention_backend/fmha/registry.py
  • tensorrt_llm/_torch/custom_ops/cute_dsl_custom_ops.py
  • tensorrt_llm/_torch/cute_dsl_kernels/blackwell/attention/__init__.py
  • tensorrt_llm/_torch/cute_dsl_kernels/blackwell/attention/mla/__init__.py
  • tensorrt_llm/_torch/cute_dsl_kernels/blackwell/attention/mla/mla_decode_fp16.py
  • tensorrt_llm/_torch/cute_dsl_kernels/blackwell/attention/mla/mla_decode_fp8.py
  • tensorrt_llm/_torch/cute_dsl_kernels/blackwell/attention/mla/mla_helpers.py
  • tensorrt_llm/_torch/modules/ATTENTION_DEVELOPER_GUIDE.md
  • tensorrt_llm/_torch/modules/fused_moe/moe_scheduler.py
  • tensorrt_llm/_torch/pyexecutor/model_engine.py
  • tensorrt_llm/tools/layer_wise_benchmarks/runner.py
  • tests/unittest/_torch/attention/test_attention_mla.py
  • tests/unittest/_torch/attention/test_cute_dsl_mla_decode.py

Comment on lines +522 to +523
if layers_in_pool > 1:
page_table = page_table + layer_in_pool

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 | 🔴 Critical | ⚡ Quick win

Scale page IDs when folding in the interleaved layer slot.

After kv_pool is re-viewed as packed combined slots, logical page p for layer slot l maps to p * layers_in_pool + l; adding only layer_in_pool can read KV pages from the wrong layer/page.

🐛 Proposed fix
         page_table = page_table_layer[meta.num_contexts:].transpose(0, 1).to(torch.int32)
         if layers_in_pool > 1:
-            page_table = page_table + layer_in_pool
+            page_table = page_table * layers_in_pool + layer_in_pool
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if layers_in_pool > 1:
page_table = page_table + layer_in_pool
page_table = page_table_layer[meta.num_contexts:].transpose(0, 1).to(torch.int32)
if layers_in_pool > 1:
page_table = page_table * layers_in_pool + layer_in_pool
🤖 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/fmha/cute_dsl.py` around lines 522 -
523, The page-table adjustment in the `cute_dsl.py` path that folds in the
interleaved layer slot is using only `layer_in_pool`, which can point to the
wrong KV page after `kv_pool` is re-viewed as packed combined slots. Update the
logic around `page_table` so the layer contribution is scaled by
`layers_in_pool` (matching the logical mapping used by the packed view) and keep
the behavior in the same `layers_in_pool > 1` branch.

Comment on lines +539 to +644
if os.environ.get("TLLM_CUTE_DSL_VAR_SPLIT_KV", "1") != "0":
max_active_blocks = self._get_max_active_blocks()
# Host upper bound on KV length: per-sequence page capacity * page
# size (page_table is [pages_per_seq, batch], a fixed shape under
# CUDA-graph capture). Yields the grid's split_kv max on the host.
k_max = page_table.shape[0] * page_size
max_splits = max(1, (k_max + self._CUTE_DSL_QK_TILE_K - 1) // self._CUTE_DSL_QK_TILE_K)
split_kv = self._split_kv_from_max_splits(
max_splits, batch_size, seq_len_q, max_active_blocks
)
if split_kv > 1:
is_var_split_kv = True
block_split_kvs = self._compute_block_split_kvs(
cache_seqs_base, batch_size, seq_len_q, max_active_blocks, split_kv
)
# get_workspace_size = B*H*S*split_kv*(D+1)*acc_width//8; fold
# cancels (H_eff*S_eff == H*S), acc=fp32 (width 32 -> //8 = 4).
ws_bytes = batch_size * num_heads * seq_len_q * split_kv * (d_latent + 1) * 4
workspace = torch.empty(ws_bytes, dtype=torch.int8, device=q.device)
else:
split_kv = 1

softmax_scale = float(1.0 / (math.sqrt(qk_nope_head_dim + d_rope) * attn.q_scaling))
output_scale = 1.0
if kernel_dtype == torch.float8_e4m3fn:
if params.fwd.mla_bmm1_scale is None or params.fwd.mla_bmm2_scale is None:
raise RuntimeError("FP8 CuTe DSL MLA decode requires MLA FP8 scales.")
cached = getattr(self, "_cute_dsl_fp8_scale", None)
if cached is None:
if torch.cuda.is_current_stream_capturing():
raise RuntimeError(
"CuTe DSL MLA FMHA: fp8 decode scale was not cached for "
f"layer {attn.layer_idx} before CUDA graph capture."
)
softmax_scale = float(params.fwd.mla_bmm1_scale[1].item()) / _LOG2_E
output_scale = float(params.fwd.mla_bmm2_scale[0].item())
self._cute_dsl_fp8_scale = (softmax_scale, output_scale)
else:
softmax_scale, output_scale = cached
if (
os.environ.get("TLLM_CUTE_DSL_SCALE_DUMP")
and not torch.cuda.is_current_stream_capturing()
):
live_softmax_scale = float(params.fwd.mla_bmm1_scale[1].item()) / _LOG2_E
live_output_scale = float(params.fwd.mla_bmm2_scale[0].item())
print(
"[CUTEDSL_SCALE] layer=%d cached=(%.8f,%.8f) "
"live=(%.8f,%.8f) drift=%s"
% (
attn.layer_idx,
softmax_scale,
output_scale,
live_softmax_scale,
live_output_scale,
abs(live_softmax_scale - softmax_scale) > 1e-6
or abs(live_output_scale - output_scale) > 1e-6,
),
flush=True,
)

out_kernel_dtype = torch.bfloat16 if kernel_dtype == torch.float8_e4m3fn else kernel_dtype
output_view = output.view(batch_size, seq_len_q, num_heads, d_latent)

# Single fused decode over all ``seq_len_q`` query tokens. For
# multi-query (MTP / linear spec-decode) the kernel applies the causal
# mask internally: query token ``t`` attends to KV positions
# ``[0, K - (seq_len_q - 1) + t)``. ``cache_seqs_base`` already counts
# every freshly-appended token of this step (K), so token ``t``'s bound
# equals ``cache_seqs_base - (seq_len_q - 1) + t`` -- exactly the
# per-query trim the previous one-token-at-a-time loop applied. For
# ``seq_len_q == 1`` this reduces to a plain decode.
q_latent = q_view[..., :d_latent].permute(2, 3, 1, 0)
q_rope = q_view[..., d_latent:].permute(2, 3, 1, 0)

# When the kernel output dtype matches the module output dtype (the
# common fp8-KV case: both bf16), have the kernel write straight into
# ``output`` instead of a temp buffer that is then D2D-copied back. That
# copy was ~1.7us/call and, at small batch where the decode win is only
# a few us, ate the win (MLA module went flat/slightly slower despite a
# faster decode kernel). ``output_view`` is a contiguous
# [B, S_q, H, d_latent] view, so its permute(2,3,1,0) is byte-identical
# in layout to a fresh contiguous o_storage's -- the op's compact-shape
# marking still holds. Only fall back to the temp+copy on a dtype
# mismatch (the kernel can only emit out_kernel_dtype).
write_output_direct = output.dtype == out_kernel_dtype
if write_output_direct:
o_storage = output_view
else:
o_storage = torch.empty(
(batch_size, seq_len_q, num_heads, d_latent),
dtype=out_kernel_dtype,
device=q.device,
)
o_kernel = o_storage.permute(2, 3, 1, 0)
lse_storage = torch.empty(
(batch_size, seq_len_q, num_heads),
dtype=torch.float32,
device=q.device,
)
lse = lse_storage.permute(2, 1, 0)

# The decode path uses variable-seq mode by default (real serving has
# unequal per-request KV lengths). Experiment toggle: set
# TLLM_CUTE_DSL_VAR_SEQ=0 to force the fixed-length path -- only valid
# when every sequence shares one KV length (e.g. profiling/microbench).
is_var_seq = os.environ.get("TLLM_CUTE_DSL_VAR_SEQ", "1") != "0"

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.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Disable variable split-KV when fixed-sequence mode is forced.

The kernel rejects is_var_split_kv=True with is_var_seq=False; with TLLM_CUTE_DSL_VAR_SEQ=0, the current default split-KV path can still set that invalid combination.

🐛 Proposed fix
+        # The decode path uses variable-seq mode by default (real serving has
+        # unequal per-request KV lengths). Experiment toggle: set
+        # TLLM_CUTE_DSL_VAR_SEQ=0 to force the fixed-length path -- only valid
+        # when every sequence shares one KV length (e.g. profiling/microbench).
+        is_var_seq = os.environ.get("TLLM_CUTE_DSL_VAR_SEQ", "1") != "0"
+
         if os.environ.get("TLLM_CUTE_DSL_VAR_SPLIT_KV", "1") != "0":
+            if not is_var_seq:
+                split_kv = 1
+            else:
-            max_active_blocks = self._get_max_active_blocks()
+                max_active_blocks = self._get_max_active_blocks()
                 ...
-
-        # The decode path uses variable-seq mode by default (real serving has
-        # unequal per-request KV lengths). Experiment toggle: set
-        # TLLM_CUTE_DSL_VAR_SEQ=0 to force the fixed-length path -- only valid
-        # when every sequence shares one KV length (e.g. profiling/microbench).
-        is_var_seq = os.environ.get("TLLM_CUTE_DSL_VAR_SEQ", "1") != "0"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if os.environ.get("TLLM_CUTE_DSL_VAR_SPLIT_KV", "1") != "0":
max_active_blocks = self._get_max_active_blocks()
# Host upper bound on KV length: per-sequence page capacity * page
# size (page_table is [pages_per_seq, batch], a fixed shape under
# CUDA-graph capture). Yields the grid's split_kv max on the host.
k_max = page_table.shape[0] * page_size
max_splits = max(1, (k_max + self._CUTE_DSL_QK_TILE_K - 1) // self._CUTE_DSL_QK_TILE_K)
split_kv = self._split_kv_from_max_splits(
max_splits, batch_size, seq_len_q, max_active_blocks
)
if split_kv > 1:
is_var_split_kv = True
block_split_kvs = self._compute_block_split_kvs(
cache_seqs_base, batch_size, seq_len_q, max_active_blocks, split_kv
)
# get_workspace_size = B*H*S*split_kv*(D+1)*acc_width//8; fold
# cancels (H_eff*S_eff == H*S), acc=fp32 (width 32 -> //8 = 4).
ws_bytes = batch_size * num_heads * seq_len_q * split_kv * (d_latent + 1) * 4
workspace = torch.empty(ws_bytes, dtype=torch.int8, device=q.device)
else:
split_kv = 1
softmax_scale = float(1.0 / (math.sqrt(qk_nope_head_dim + d_rope) * attn.q_scaling))
output_scale = 1.0
if kernel_dtype == torch.float8_e4m3fn:
if params.fwd.mla_bmm1_scale is None or params.fwd.mla_bmm2_scale is None:
raise RuntimeError("FP8 CuTe DSL MLA decode requires MLA FP8 scales.")
cached = getattr(self, "_cute_dsl_fp8_scale", None)
if cached is None:
if torch.cuda.is_current_stream_capturing():
raise RuntimeError(
"CuTe DSL MLA FMHA: fp8 decode scale was not cached for "
f"layer {attn.layer_idx} before CUDA graph capture."
)
softmax_scale = float(params.fwd.mla_bmm1_scale[1].item()) / _LOG2_E
output_scale = float(params.fwd.mla_bmm2_scale[0].item())
self._cute_dsl_fp8_scale = (softmax_scale, output_scale)
else:
softmax_scale, output_scale = cached
if (
os.environ.get("TLLM_CUTE_DSL_SCALE_DUMP")
and not torch.cuda.is_current_stream_capturing()
):
live_softmax_scale = float(params.fwd.mla_bmm1_scale[1].item()) / _LOG2_E
live_output_scale = float(params.fwd.mla_bmm2_scale[0].item())
print(
"[CUTEDSL_SCALE] layer=%d cached=(%.8f,%.8f) "
"live=(%.8f,%.8f) drift=%s"
% (
attn.layer_idx,
softmax_scale,
output_scale,
live_softmax_scale,
live_output_scale,
abs(live_softmax_scale - softmax_scale) > 1e-6
or abs(live_output_scale - output_scale) > 1e-6,
),
flush=True,
)
out_kernel_dtype = torch.bfloat16 if kernel_dtype == torch.float8_e4m3fn else kernel_dtype
output_view = output.view(batch_size, seq_len_q, num_heads, d_latent)
# Single fused decode over all ``seq_len_q`` query tokens. For
# multi-query (MTP / linear spec-decode) the kernel applies the causal
# mask internally: query token ``t`` attends to KV positions
# ``[0, K - (seq_len_q - 1) + t)``. ``cache_seqs_base`` already counts
# every freshly-appended token of this step (K), so token ``t``'s bound
# equals ``cache_seqs_base - (seq_len_q - 1) + t`` -- exactly the
# per-query trim the previous one-token-at-a-time loop applied. For
# ``seq_len_q == 1`` this reduces to a plain decode.
q_latent = q_view[..., :d_latent].permute(2, 3, 1, 0)
q_rope = q_view[..., d_latent:].permute(2, 3, 1, 0)
# When the kernel output dtype matches the module output dtype (the
# common fp8-KV case: both bf16), have the kernel write straight into
# ``output`` instead of a temp buffer that is then D2D-copied back. That
# copy was ~1.7us/call and, at small batch where the decode win is only
# a few us, ate the win (MLA module went flat/slightly slower despite a
# faster decode kernel). ``output_view`` is a contiguous
# [B, S_q, H, d_latent] view, so its permute(2,3,1,0) is byte-identical
# in layout to a fresh contiguous o_storage's -- the op's compact-shape
# marking still holds. Only fall back to the temp+copy on a dtype
# mismatch (the kernel can only emit out_kernel_dtype).
write_output_direct = output.dtype == out_kernel_dtype
if write_output_direct:
o_storage = output_view
else:
o_storage = torch.empty(
(batch_size, seq_len_q, num_heads, d_latent),
dtype=out_kernel_dtype,
device=q.device,
)
o_kernel = o_storage.permute(2, 3, 1, 0)
lse_storage = torch.empty(
(batch_size, seq_len_q, num_heads),
dtype=torch.float32,
device=q.device,
)
lse = lse_storage.permute(2, 1, 0)
# The decode path uses variable-seq mode by default (real serving has
# unequal per-request KV lengths). Experiment toggle: set
# TLLM_CUTE_DSL_VAR_SEQ=0 to force the fixed-length path -- only valid
# when every sequence shares one KV length (e.g. profiling/microbench).
is_var_seq = os.environ.get("TLLM_CUTE_DSL_VAR_SEQ", "1") != "0"
# The decode path uses variable-seq mode by default (real serving has
# unequal per-request KV lengths). Experiment toggle: set
# TLLM_CUTE_DSL_VAR_SEQ=0 to force the fixed-length path -- only valid
# when every sequence shares one KV length (e.g. profiling/microbench).
is_var_seq = os.environ.get("TLLM_CUTE_DSL_VAR_SEQ", "1") != "0"
if os.environ.get("TLLM_CUTE_DSL_VAR_SPLIT_KV", "1") != "0":
if not is_var_seq:
split_kv = 1
else:
max_active_blocks = self._get_max_active_blocks()
# Host upper bound on KV length: per-sequence page capacity * page
# size (page_table is [pages_per_seq, batch], a fixed shape under
# CUDA-graph capture). Yields the grid's split_kv max on the host.
k_max = page_table.shape[0] * page_size
max_splits = max(1, (k_max + self._CUTE_DSL_QK_TILE_K - 1) // self._CUTE_DSL_QK_TILE_K)
split_kv = self._split_kv_from_max_splits(
max_splits, batch_size, seq_len_q, max_active_blocks
)
if split_kv > 1:
is_var_split_kv = True
block_split_kvs = self._compute_block_split_kvs(
cache_seqs_base, batch_size, seq_len_q, max_active_blocks, split_kv
)
# get_workspace_size = B*H*S*split_kv*(D+1)*acc_width//8; fold
# cancels (H_eff*S_eff == H*S), acc=fp32 (width 32 -> //8 = 4).
ws_bytes = batch_size * num_heads * seq_len_q * split_kv * (d_latent + 1) * 4
workspace = torch.empty(ws_bytes, dtype=torch.int8, device=q.device)
else:
split_kv = 1
softmax_scale = float(1.0 / (math.sqrt(qk_nope_head_dim + d_rope) * attn.q_scaling))
output_scale = 1.0
if kernel_dtype == torch.float8_e4m3fn:
if params.fwd.mla_bmm1_scale is None or params.fwd.mla_bmm2_scale is None:
raise RuntimeError("FP8 CuTe DSL MLA decode requires MLA FP8 scales.")
cached = getattr(self, "_cute_dsl_fp8_scale", None)
if cached is None:
if torch.cuda.is_current_stream_capturing():
raise RuntimeError(
"CuTe DSL MLA FMHA: fp8 decode scale was not cached for "
f"layer {attn.layer_idx} before CUDA graph capture."
)
softmax_scale = float(params.fwd.mla_bmm1_scale[1].item()) / _LOG2_E
output_scale = float(params.fwd.mla_bmm2_scale[0].item())
self._cute_dsl_fp8_scale = (softmax_scale, output_scale)
else:
softmax_scale, output_scale = cached
if (
os.environ.get("TLLM_CUTE_DSL_SCALE_DUMP")
and not torch.cuda.is_current_stream_capturing()
):
live_softmax_scale = float(params.fwd.mla_bmm1_scale[1].item()) / _LOG2_E
live_output_scale = float(params.fwd.mla_bmm2_scale[0].item())
print(
"[CUTEDSL_SCALE] layer=%d cached=(%.8f,%.8f) "
"live=(%.8f,%.8f) drift=%s"
% (
attn.layer_idx,
softmax_scale,
output_scale,
live_softmax_scale,
live_output_scale,
abs(live_softmax_scale - softmax_scale) > 1e-6
or abs(live_output_scale - output_scale) > 1e-6,
),
flush=True,
)
out_kernel_dtype = torch.bfloat16 if kernel_dtype == torch.float8_e4m3fn else kernel_dtype
output_view = output.view(batch_size, seq_len_q, num_heads, d_latent)
# Single fused decode over all ``seq_len_q`` query tokens. For
# multi-query (MTP / linear spec-decode) the kernel applies the causal
# mask internally: query token ``t`` attends to KV positions
# ``[0, K - (seq_len_q - 1) + t)``. ``cache_seqs_base`` already counts
# every freshly-appended token of this step (K), so token ``t``'s bound
# equals ``cache_seqs_base - (seq_len_q - 1) + t`` -- exactly the
# per-query trim the previous one-token-at-a-time loop applied. For
# ``seq_len_q == 1`` this reduces to a plain decode.
q_latent = q_view[..., :d_latent].permute(2, 3, 1, 0)
q_rope = q_view[..., d_latent:].permute(2, 3, 1, 0)
# When the kernel output dtype matches the module output dtype (the
# common fp8-KV case: both bf16), have the kernel write straight into
# ``output`` instead of a temp buffer that is then D2D-copied back. That
# copy was ~1.7us/call and, at small batch where the decode win is only
# a few us, ate the win (MLA module went flat/slightly slower despite a
# faster decode kernel). ``output_view`` is a contiguous
# [B, S_q, H, d_latent] view, so its permute(2,3,1,0) is byte-identical
# in layout to a fresh contiguous o_storage's -- the op's compact-shape
# marking still holds. Only fall back to the temp+copy on a dtype
# mismatch (the kernel can only emit out_kernel_dtype).
write_output_direct = output.dtype == out_kernel_dtype
if write_output_direct:
o_storage = output_view
else:
o_storage = torch.empty(
(batch_size, seq_len_q, num_heads, d_latent),
dtype=out_kernel_dtype,
device=q.device,
)
o_kernel = o_storage.permute(2, 3, 1, 0)
lse_storage = torch.empty(
(batch_size, seq_len_q, num_heads),
dtype=torch.float32,
device=q.device,
)
lse = lse_storage.permute(2, 1, 0)
🤖 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/fmha/cute_dsl.py` around lines 539 -
644, The split-KV selection in cute_dsl.py can still enable is_var_split_kv even
when fixed-sequence mode is forced, which creates an invalid kernel
configuration. In the decode path around _split_kv_from_max_splits,
_compute_block_split_kvs, and the is_var_seq toggle, gate the variable split-KV
logic on is_var_seq being true, and force split_kv back to 1 when
TLLM_CUTE_DSL_VAR_SEQ=0 so is_var_split_kv can never be set in fixed-sequence
mode. Ensure the workspace allocation and downstream kernel arguments follow the
adjusted split_kv value.

Comment on lines +82 to +92
# kernel name -> (activation dtype, kv cache dtype)
#
# NOTE: the float16 instance of the FP16 decode op
# (``cute_dsl_mla_decode_fp16_blackwell`` with dtype=float16 / fp16 KV cache)
# aborts the process (SIGABRT) on SM100 in this environment, so that exact
# dtype is excluded until the crash is root-caused. The bf16 instance uses the
# same op and is covered below for DeepSeek-V3 bf16 runs.
_KERNEL_DTYPES = {
"fp8": (torch.bfloat16, torch.float8_e4m3fn),
"bf16": (torch.bfloat16, torch.bfloat16),
}

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.

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Don't mask the FP16 crash while the runtime still advertises FP16 support.

This test drops the torch.float16 case, but CuteDslMlaFmha._get_kernel_dtype() still routes real FP16 decode batches into cute_dsl_mla_decode_fp16_blackwell. So the suite is currently hiding a supported production path that is documented here as aborting the process. Either gate FP16 out in tensorrt_llm/_torch/attention_backend/fmha/cute_dsl.py until it is fixed, or keep an explicit xfail-style regression that proves the unsupported behavior. Based on path instructions, coverage in tests/unittest/_torch/attention/test_cute_dsl_mla_decode.py is insufficient for the advertised FP16 path and needs follow-up outside this PR.

🤖 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/test_cute_dsl_mla_decode.py` around lines 82
- 92, The FP16 decode path is still advertised by
CuteDslMlaFmha._get_kernel_dtype() and routes to
cute_dsl_mla_decode_fp16_blackwell, but the test matrix in
test_cute_dsl_mla_decode.py now omits torch.float16 and hides the crash. Update
the runtime in tensorrt_llm/_torch/attention_backend/fmha/cute_dsl.py to gate
FP16 out until it is fixed, or add an explicit xfail/regression test that
exercises the FP16 path and documents the aborting behavior. Keep the test
coverage aligned with the actual supported kernel dtypes so the unsupported FP16
path is not silently masked.

Source: Path instructions

@haow-nv
haow-nv force-pushed the feat/cutedsl-mla-decode-fp8 branch 2 times, most recently from fc530d6 to cf75689 Compare July 6, 2026 06:45
@haow-nv

haow-nv commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator Author

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

@haow-nv
haow-nv force-pushed the feat/cutedsl-mla-decode-fp8 branch from bd40e10 to 5dceec8 Compare July 6, 2026 07:12
@haow-nv

haow-nv commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator Author

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

@haow-nv

haow-nv commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator Author

/bot run

@yuxianq

yuxianq commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

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

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #57746 [ run ] triggered by Bot. Commit: cc8a8fc Link to invocation

@yuxianq

yuxianq commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

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

Comment thread tensorrt_llm/_torch/pyexecutor/model_engine.py

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.

make the file name match with registered name, how about using cute_dsl_mla.py?

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.

We need to integrate cutedsl fmha/mla prefill kernel latter. Shall we update the fmha lib name from "CuteDslMlaFmha" to "CuteDslFmha" 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.

If we want to use a single fmha lib to integrate both mla and non-mla kernels, we can use CuteDslFmha and cute_dsl as registration name.

Comment thread .pre-commit-config.yaml

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.

For these 5 new files, we can apply new ruff fules for them. We can revert all changes in .pre-commit-config.yaml, legacy-files.txt, pyproject.toml and ruff-legacy.toml.

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.

Thanks. Will revert all changes.

@haow-nv haow-nv Jul 28, 2026

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.

Discussed offline. We don't change the sync files (mla_decode.* + mla_helper.py) unless necessary fix.

So still need to add the three cutedsl kernel files into legacy-files.txt. Then call python3 scripts/legacy_utils.py check-configs to update .pre-commit-config.yaml, pyproject.toml and ruff-legacy.toml

if attn.qk_nope_head_dim is None or attn.qk_nope_head_dim <= 0:
logger.debug("CuTe DSL MLA FMHA is unavailable: qk_nope_head_dim must be positive.")
return False
if attn.kv_lora_rank != 512 or attn.qk_rope_head_dim != 64:

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 is stricter condition, don't need to check attn.kv_lora_rank <= 0 and attn.qk_rope_head_dim <= 0 before

) -> tuple[bool, str]:
if fwd.attention_input_type != AttentionInputType.generation_only:
return False, "CuTe DSL MLA FMHA only supports generation-only attention."
if meta.num_contexts != 0 or meta.num_generations <= 0:

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 condition seems to be equivalent to fwd.attention_input_type != AttentionInputType.generation_only, can we remove it?

or meta.num_sparse_topk > 0
or attn.sparse_params is not None
):
return False, "CuTe DSL MLA FMHA does not support sparse attention."

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.

We have already checked sparse_params in is_available, is it necessary to check sparse attention again here?

or getattr(meta, "is_spec_dec_dynamic_tree", False)
):
return False, "CuTe DSL MLA FMHA does not support custom/tree speculative masks."
if q.shape[0] % meta.num_generations != 0:

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.

We have checked gen-only batch before, this condition is true for gen-only batch, should we check it in runtime again?

f"num_generations ({meta.num_generations}).",
)
seq_len_q = q.shape[0] // meta.num_generations
if seq_len_q < 1:

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.

is_supported is not for runtime assertion, it is for quick dispatch, we should avoid adding too much obvious conditions here. This condition must be false for correct q shape in gen-only case, can we remove it?

num_heads: int,
batch_size: int,
seq_len_q: int,
predicted_tokens_per_seq: int,

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.

predicted_tokens_per_seq is unused.

return False, reason
if meta.kv_cache_block_offsets is None:
return False, "Paged KV block offsets are required."
if meta.kv_cache_manager is 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 condition is enough to exclude no-cache case, kv_cache_block_offsets/pool_mapping checks are unnecessary.

pool_mapping = meta.host_kv_cache_pool_mapping
if pool_mapping is None:
return False, "KV cache pool mapping is required."
if fwd.latent_cache is 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.

latent_cache is not None for MLA case, which is checked, can we remove this condition?

tokens_per_block = meta.tokens_per_block
if tokens_per_block is None:
tokens_per_block = getattr(meta.kv_cache_manager, "tokens_per_block", 0)
if tokens_per_block <= 1:

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 check the correctness of tokens_per_block here

f"has_fp8_kv_cache={getattr(attn, 'has_fp8_kv_cache', False)}.",
)
if kernel_dtype == torch.float8_e4m3fn and (
fwd.quant_q_buffer is None or fwd.mla_bmm1_scale is None or fwd.mla_bmm2_scale is 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.

MLA module has ensured it, don't need to check here.

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #61509 [ run ] completed with state FAILURE. Commit: f842131
/LLM/main/L0_MergeRequest_PR pipeline #49726 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

`TLLM_FMHA_LIBS=-flashinfer_trtllm_gen` to force the fallback path. Each FMHA
library exposes `is_available()` for module/static environment checks and
`is_supported()` for per-forward request checks.
`cute_dsl_mla,flashinfer_trtllm_gen,fallback`; use `TLLM_FMHA_LIBS=fallback`

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.

Seems that we forget to add msa_sparse_gqa in ATTENTION_DEVELOPER_GUIDE.md, can you also add it?

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.

Sure. Will add it.

for i in range(local_split_kv):
for j in cutlass.range_constexpr(elements_per_thread):
element_idx = tidx + j * self.threads_per_warp * self.num_compute_warps
rAccO[j] += gAccO[i, element_idx] * smem_lse_scale[i]

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.

Bound split-KV before indexing the fixed-size reduction table

smem_lse_scale has exactly MAX_SPLITS entries, but this loop runs to runtime local_split_kv; neither kernel's can_implement() bounds split_kv. For example, seq_len_k=32896, split_kv=257 produces 257 K tiles with the default 128-column tile, and iteration 256 reads smem_lse_scale[256] out of bounds. The FP16 reduction has the same issue.

Suggested fix: reject split_kv < 1 and split_kv > MAX_SPLITS in both can_implement() methods. For variable split-KV, also validate every block_split_kvs[b] against min(split_kv, MAX_SPLITS), or consume scales in bounded chunks.

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.

Discussed offline. The functionality is not affected even split_kv > MAX_SPLITS. But will add checking to make sure splitkv is in [1,32] for performance.

Comment thread tensorrt_llm/_torch/attention_backend/fmha/cute_dsl.py

@hyukn hyukn left a comment

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.

Overall LGTM.

# fallback (cache not warmed), supply the default 4-tuple so split_kv +
# is_persistent still come from a length-4 tactic.
if not (isinstance(best_tactic, tuple) and len(best_tactic) == 4):
best_tactic = runner.default_tactic(int(q_latent.shape[-1]))

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.

I think we do not have default coverage for -1 during warmup as the default (needs confirmation). Then the fallback may cause another runtime cute.compile. This should not be a big deal because normally we do not expect fallback to happen.

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.

The unit test without autotuner will get -1 .

Comment thread tensorrt_llm/_torch/custom_ops/cute_dsl_custom_ops.py Outdated
mutates_args=("o", "workspace"),
device_types="cuda",
)
def cute_dsl_mla_decode_fp8_blackwell(

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.

So we can directly rely on the current attention unit tests to cover the new custom ops. Can the new tuning path be covered correctly?

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.

The current attention unit test could cover the new custom ops, but doesn't cover autotune.

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.

@yihwang-nv is adding DSA unit tests for attention backends in #16309, if we add unittest/_torch/attention/test_attention_backends.py to tests/integration/test_lists/test-db/l0_rtx_pro_6000.yml, will it cover autotune?

@haow-nv

haow-nv commented Jul 27, 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 #61851 [ run ] triggered by Bot. Commit: f842131 Link to invocation

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #61851 [ run ] completed with state SUCCESS. Commit: f842131
/LLM/main/L0_MergeRequest_PR pipeline #50043 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

…ce, trim gate

Gate changes in `CuteDslMlaFmha`:
- `is_supported()` now rejects `helix_position_offsets`; the kernel has no
  Helix position handling and would silently compute wrong results.
- Collapse the sparse rejection to the module-level `attn.sparse_params`
  signal instead of also re-checking the per-forward sparse/topk index
  tensors, which are derived from it.
- Drop the `kv_lora_rank` / `qk_rope_head_dim` positivity checks and the
  `num_tokens % num_generations` divisibility check, all implied by the MLA
  decode path that reaches this gate.
- Drop the unused `predicted_tokens_per_seq` argument of the perf gate.

Workspace:
- Factor the required-size computation into `_required_workspace_size()` and
  reuse it from both `prepare_workspace()` and the forward path.
- Slice `params.workspace` down to what this kernel actually owns before
  handing it to the op. The shared attention workspace is sized from
  `max_num_tokens` and can reach several GiB; the AutoTuner rebuilds every
  input with a dynamic dim as a float32 `torch.rand` tensor (4x the int8
  byte count) while profiling, which could OOM.

Docs and style:
- `ATTENTION_DEVELOPER_GUIDE.md`: include `msa_sparse_gqa` in the documented
  default `TLLM_FMHA_LIBS` order.
- `registry.py`: add the missing blank line before `init_fmha_libs()`.

Signed-off-by: haow <haow@nvidia.com>
@haow-nv
haow-nv force-pushed the feat/cutedsl-mla-decode-fp8 branch from ab37c6d to 462c0d2 Compare July 28, 2026 05:56
@haow-nv

haow-nv commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator Author

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

@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.

♻️ Duplicate comments (1)
tensorrt_llm/_torch/attention_backend/fmha/cute_dsl.py (1)

386-425: 🗄️ Data Integrity & Integration | 🔴 Critical | ⚡ Quick win

Page IDs still not scaled by layers_in_pool in the packed view.

After kv_pool is re-viewed as packed combined slots (line 394-398), logical page p for layer slot l maps to p * layers_in_pool + l in the combined index space. Lines 424-425 only add layer_in_pool, so for any p > 0 the kernel indexes the wrong page/layer whenever layers_in_pool > 1 (interleaved KV cache manager layout). This was flagged on an earlier revision and remains unresolved.

🐛 Proposed fix
         page_table = page_table_layer[meta.num_contexts:].transpose(0, 1).to(torch.int32)
         if layers_in_pool > 1:
-            page_table = page_table + layer_in_pool
+            page_table = page_table * layers_in_pool + layer_in_pool
🤖 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/fmha/cute_dsl.py` around lines 386 -
425, Update the page-table transformation after the packed KV cache view in the
surrounding attention setup: when layers_in_pool > 1, scale every logical page
ID in page_table by layers_in_pool, then add layer_in_pool to select the current
layer slot. Preserve the existing unmodified page table behavior when
layers_in_pool equals 1.
🧹 Nitpick comments (2)
tensorrt_llm/_torch/attention_backend/fmha/cute_dsl.py (2)

157-160: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicated tiler constants risk drift between eligibility check and runner.

mma_qk_tiler_mn/mma_pv_tiler_mn are hardcoded here to mirror CuteDSLNVMlaDecodeBlackwellRunner's actual launch config in cute_dsl_custom_ops.py. The comment flags the sync requirement, but nothing enforces it — if the runner's defaults change, this eligibility check silently goes stale and can_implement validates against the wrong tiler.

Consider exposing the default tiler as a shared class attribute/classmethod on CuteDSLNVMlaDecodeBlackwellRunner and importing it here instead of re-declaring the literals.

🤖 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/fmha/cute_dsl.py` around lines 157 -
160, Replace the hardcoded default tilers in the eligibility check with a shared
class attribute or classmethod exposed by CuteDSLNVMlaDecodeBlackwellRunner, and
import/reuse that value in cute_dsl.py. Update the runner’s default
configuration to be the single source of truth while preserving the existing
(128, 128) and (128, 256) behavior.

45-91: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Public interface methods lack docstrings.

is_available, is_supported, run_mla_generation, and prepare_workspace are the public surface of this FMHA backend but have no docstrings documenting arguments/behavior (only inline comments on individual checks). Given the non-obvious eligibility rules and workspace-sizing contract, Google-style docstrings would materially help future maintainers.

As per coding guidelines: "Prefer docstrings for external interfaces, use Google-style docstrings, document public function arguments, and include tensor dimensions and constrained dtypes where applicable."

Also applies to: 189-205, 485-505, 529-551

🤖 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/fmha/cute_dsl.py` around lines 45 - 91,
The public methods is_available, is_supported, run_mla_generation, and
prepare_workspace lack interface documentation. Add concise Google-style
docstrings to each method describing arguments, return or operational behavior,
eligibility constraints, workspace-sizing requirements, and relevant tensor
dimensions and constrained dtypes, while leaving the implementation unchanged.

Source: Coding guidelines

🤖 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.

Duplicate comments:
In `@tensorrt_llm/_torch/attention_backend/fmha/cute_dsl.py`:
- Around line 386-425: Update the page-table transformation after the packed KV
cache view in the surrounding attention setup: when layers_in_pool > 1, scale
every logical page ID in page_table by layers_in_pool, then add layer_in_pool to
select the current layer slot. Preserve the existing unmodified page table
behavior when layers_in_pool equals 1.

---

Nitpick comments:
In `@tensorrt_llm/_torch/attention_backend/fmha/cute_dsl.py`:
- Around line 157-160: Replace the hardcoded default tilers in the eligibility
check with a shared class attribute or classmethod exposed by
CuteDSLNVMlaDecodeBlackwellRunner, and import/reuse that value in cute_dsl.py.
Update the runner’s default configuration to be the single source of truth while
preserving the existing (128, 128) and (128, 256) behavior.
- Around line 45-91: The public methods is_available, is_supported,
run_mla_generation, and prepare_workspace lack interface documentation. Add
concise Google-style docstrings to each method describing arguments, return or
operational behavior, eligibility constraints, workspace-sizing requirements,
and relevant tensor dimensions and constrained dtypes, while leaving the
implementation unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 0644b885-7858-4af0-992c-b59fecf1dfef

📥 Commits

Reviewing files that changed from the base of the PR and between f842131 and ab37c6d.

📒 Files selected for processing (3)
  • tensorrt_llm/_torch/attention_backend/fmha/cute_dsl.py
  • tensorrt_llm/_torch/custom_ops/cute_dsl_custom_ops.py
  • tensorrt_llm/_torch/modules/ATTENTION_DEVELOPER_GUIDE.md
🚧 Files skipped from review as they are similar to previous changes (2)
  • tensorrt_llm/_torch/modules/ATTENTION_DEVELOPER_GUIDE.md
  • tensorrt_llm/_torch/custom_ops/cute_dsl_custom_ops.py

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #62126 [ run ] triggered by Bot. Commit: 462c0d2 Link to invocation

… split_kv

- Perf gate: _PERF_MIN_BATCH_FP8[(128, 1)] 8 -> 64. The measured crossover for
  the unsharded 128-head fp8 decode shape is at batch 64; the old floor let
  small per-rank batches through, where the default TRTLLM kernels are faster.
- Drop the duplicate sparse_params check in _is_supported_with_reason.
  is_available already rejects sparse layers, so the second check was dead code.
- can_implement (both fp8 and fp16 kernels): reject split_kv outside [1, 32],
  the range the kernel is tuned for.
- test_attention_mla: add a 64-request context list and raise max_num_contexts
  to 64 so the fp8 decode gate is actually exercised. Verified on B200: 96/96
  pass and 12 of the parametrized cells now reach the CuTe DSL decode kernel
  (fp8, num_heads=128, batch 50/64, seq_len_q 1 and 4, v1 and v2 KV cache).
- Legacy lint: drop the two CuTe DSL attention __init__.py from legacy-files.txt
  and regenerate the managed blocks in ruff-legacy.toml / pyproject.toml /
  .pre-commit-config.yaml.

Signed-off-by: haow <haow@nvidia.com>
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.

6 participants