Skip to content

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

Open
haow-nv wants to merge 21 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 21 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 the cute_dsl_mla FMHA backend for Blackwell SM100/SM103 (CuTe DSL required) to dispatch eligible DeepSeek MLA generation-phase requests to FP8 or FP16/BF16 CuTe DSL decode kernels.
  • Implemented CuteDslMlaFmha with strong request gating/rejection (decode-only, geometry/paging metadata requirements, unsupported masks/speculation/beam/custom cases), kernel feasibility checks, KV page-table prep, and CuTe layout normalization.
  • Added MLA decode custom ops and autotuned kernel runner (CuteDSLNVMlaDecodeBlackwellRunner) with:
    • split-KV support (host heuristic + device per-sequence split-KV),
    • CUDA-graph-safe caching/persistence handling,
    • workspace handling to avoid illegal writes for split-K==1,
    • FP8/FP16/BF16 typed entry points.
  • Added static tile scheduling helpers for MLA (mla_helpers.py) to support persistent/non-persistent scheduling and correct tile indexing.
  • Updated FMHA library registration/export so cute_dsl_mla becomes available by default unless overridden by TLLM_FMHA_LIBS, and expanded the attention developer guide accordingly.
  • Updated lint/pre-commit configuration to include the newly added MLA decode modules under both standard and legacy Ruff scopes.
  • Fixed MoE attention-DP empty-chunk collective sizing by making per-rank sizes vectors consistent when split_chunk yields 0-token chunks on some ranks.
  • Expanded TRTLLM-Gen FMHA JIT warmup to include mixed context+generation warmup only under the specified compatibility conditions (e.g., not using draft model / guided decoder).

Review notes / risk areas

  • The change is broad and performance-sensitive (custom ops, autotuning, persistent tile scheduling, FP8 paths, CUDA-graph behavior, and fallback correctness). Focus regression testing on:
    • eligibility/gating correctness for all reject paths,
    • split-KV variability and workspace==empty behavior,
    • CUDA graph capture compatibility for the metadata/cached kernels,
    • correctness across KV layout/paged-KV metadata variants.
  • CI attempts reported mixed outcomes: multiple runs completed but downstream L0 pipelines failed, some runs failed outright, and one run was triggered without a completion result. Ensure the previously failing test causes are identified and rerun with the requested multi-GPU test coverage before merge.

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

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: bfa9d658-2c40-4817-b79a-cecb65e44fd4

📥 Commits

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

📒 Files selected for processing (1)
  • tensorrt_llm/_torch/attention_backend/fmha/registry.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • tensorrt_llm/_torch/attention_backend/fmha/registry.py

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, variable 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, LSE, and workspace
Loading

Possibly related PRs

Suggested labels: api-compatible

Suggested reviewers: bowenfu, yuxianq, mikeiovine, dpitman-nvda, qijune

🚥 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 clearly summarizes the main change: adding CuteDSL FP8/FP16 MLA decode support as an FMHA library.
Description check ✅ Passed The description is well structured and covers the change, behavior, test coverage, and checklist items required by the template.
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

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #61265 [ run ] triggered by Bot. Commit: f842131 Link to invocation

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #60959 [ run ] completed with state ABORTED. Commit: ae9d2af

Link to invocation

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #61268 [ run ] triggered by Bot. Commit: f842131 Link to invocation

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #61265 [ run ] completed with state ABORTED. Commit: f842131

Link to invocation

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

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

@haow-nv

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

@haow-nv

haow-nv commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator Author

/bot run --add-multi-gpu-test

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #61509 [ run ] triggered by Bot. Commit: f842131 Link to invocation

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #61502 [ run ] completed with state ABORTED. Commit: f842131

Link to invocation

if (not self.is_draft_model and self.guided_decoder is None
and not self.mapping.has_pp()):
# Add generation request to warmup the autotuner cache.
warmup_configs.append((1 + self.max_total_draft_tokens, 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.

The context-only case uses (curr_max_num_tokens, 0), to avoid violating curr_max_num_tokens, we can use (curr_max_num_tokens, 1) 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.

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.

MAX_SPLITS = 256


def ceil_div(a: int, b: int) -> 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.

Please use the ceil_div in tensorrt_llm/math_utils.py instead of re-refining it.

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.

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.

"""
if split_kv == 1:
return 0
return B * H * S * split_kv * (D + 1) * acc_dtype.width // 8

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.

Pad the split-KV workspace to the 128-row MMA store

Both kernel variants accept every H <= 128, but size and lay out the split-KV partials with logical H. For the production H=16, S_q=1, split_kv>1 path, the physical MMA store writes 128 rows: padded head 16 starts immediately after the H-sized acc_o, so the store aliases acc_lse and can continue beyond the raw workspace. The host-side maximum-workspace formula also uses H, so this can corrupt reduction inputs or trigger an illegal memory access.

Suggested fix: use workspace_H = max(H, 128) in both host sizing paths and the equivalent cutlass.max in both initialize_workspace() implementations, then derive the acc_o/acc_lse shapes and strides from workspace_H. Until that is fixed, reject split-KV when the post-fold head count is below 128.

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.

meta: "TrtllmAttentionMetadata",
fwd: AttentionForwardArgs,
) -> tuple[bool, str]:
if fwd.attention_input_type != AttentionInputType.generation_only:

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.

Reject Helix until the CuTe path exports softmax statistics

This support gate does not reject Helix. MLA._attn_forward_gen() allocates an uninitialized softmax_stats tensor and later consumes it in _helix_post_process(), while the CuTe path ignores softmax_stats_tensor and keeps its LSE private in its workspace. Helix therefore combines partial outputs with uninitialized statistics and can silently return incorrect tokens.

Suggested fix: mirror the sibling FlashInfer guard:

if meta.helix_position_offsets is not None:
    return False, "CuTe DSL MLA FMHA does not support Helix parallelism."

Supporting Helix instead requires exporting the correct per-token statistics and honoring inactive-rank semantics.

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.

5 participants