Skip to content

Recombine fully masked split-KV attention partials on the host - #535

Merged
umangyadav merged 15 commits into
developfrom
users/umayadav/attn-splitkv-host-combine
Oct 2, 2026
Merged

umangyadav merged 15 commits into
developfrom
users/umayadav/attn-splitkv-host-combine

Conversation

@umangyadav

@umangyadav umangyadav commented Sep 23, 2026 •

Copy link
Copy Markdown
Member

Summary

With split-KV (split_kv > 1), the attention kernel writes one partial output O_s and one log-sum-exp L_s per split, and the host wrapper generated by rocmlir-gen merges them into the final output. This PR makes that host merge match the kernel from #534:

  • Causal split-KV now works when seq_len_q is larger than the key tile. rocmlir-gen used to exit with "Causal masking + split-KV is not supported with sequenceLengthQ > nPerBlock".
  • Valid-split counts are computed per query row under causal and prefix-causal masking. Before, every causal row got a count of 1 (hence the limit above), and every prefix-causal row got row 0's count, which could drop splits holding later rows' keys when seq_len_q > 1.
  • The merge is NaN-safe for rows where every split is empty, and runs in f32 for f16/bf16 storage.

Most of this ports host-side changes from ROCm/rocMLIR#2371 and ROCm/rocMLIR#2448 (details under "Relation to rocMLIR").

Technical details

All code changes are in mlir/tools/rocmlir-gen/rocmlir-gen.cpp.

Per-row valid-split counts (computeValidSplitKV)

Under causal or prefix-causal masking, the number of splits holding a row's visible keys depends on the row, so the host emits one count per (batch-head, query row). Non-causal configs keep one count per batch-head. For row q:

  • rowLast = clamp(q + prefix_offset, 0, lastValid), where prefix_offset is 0 for plain causal and lastValid is last_valid_kv_index (default seq_len_k - 1)
  • keysPerSplit = nPerBlock * ceil(ceil((rowLast + 1) / nPerBlock) / split_kv)
  • count = ceil((rowLast + 1) / keysPerSplit)

nPerBlock is GEMM0's N tile, which is the key tile in this lowering: the Triton attention lowering keeps Q * K^T, so keys are GEMM0's N axis. rocMLIR computes the transposed product and uses MPerBlock instead.

The counts are conservative. The kernel partitions each M block's keys using the block's last row, while the host uses each row on its own, so the host can include extra empty splits but never drops a split that holds the row's data. The extra splits are harmless because #534 makes the kernel write zero output and -inf LSE for empty splits.

Per-row mask layout (createMaskSplitKV)

The counts are laid out as [batchHeads, 1, 1, 1] (per batch-head) or [batchHeads, 1, seq_len_q, 1] (per row) and compared against a range over the split axis. Split s is valid for a row when s < count. Invalid splits get LSE -inf and contribute zero to the weighted sum.

NaN-safe merge (computeFinalAttentionStage)

m   = max(max_s L_s, lowest finite)
w_s = exp(L_s - m)
O   = sum_s(w_s * O_s) / max(sum_s w_s, 1)

If a row has at least one finite L_s, both clamps are no-ops. If every split is -inf, all weights are 0 and the output row is 0, matching the kernel's result for a fully masked row. Without the clamps, -inf - (-inf) turned the whole row into NaN.

f32 merge for f16/bf16

The partial outputs and LSE are cast to f32 before the merge, and only the final output is cast back to the storage type.

Causal limitation removed

The old causal count assumed every row's visible keys fit in split 0, which only holds while seq_len_q <= nPerBlock, hence the exit. With per-row counts, the exit is removed.

Relation to rocMLIR

@codecov-commenter

codecov-commenter commented Sep 23, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.23810% with 4 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
mlir/tools/rocmlir-gen/rocmlir-gen.cpp 95.24% 0 Missing and 4 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             develop     #535   +/-   ##
==========================================
  Coverage           ?   84.72%           
==========================================
  Files              ?      208           
  Lines              ?    36300           
  Branches           ?     8179           
==========================================
  Hits               ?    30753           
  Misses             ?     3323           
  Partials           ?     2224           
Flag Coverage Δ
gfx950 84.61% <95.24%> (?)
mfma 84.50% <95.24%> (?)
navi4x 84.53% <95.24%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
...ialect/Rock/Transforms/GridwiseAttnToBlockwise.cpp 86.29% <ø> (ø)
mlir/tools/rocmlir-gen/rocmlir-gen.cpp 87.76% <95.24%> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

umangyadav and others added 3 commits September 23, 2026 22:38
Pure clang-format run over the existing file. No functional change; the
diff is whitespace and comment rewrapping only. Split out of the fully
masked attention row work so that change reviews as functional-only.

Co-authored-by: Cursor <cursoragent@cursor.com>
Gate a finite running-max sentinel and denominator clamp on compile-time empty-row risk, look through arith.extf scaling, and keep the CPU softmax reference defined for all-masked rows.

Co-authored-by: Cursor <cursoragent@cursor.com>
Allow causal split-KV with seq_len_q larger than the tile, emit per-row validity counts, and keep empty splits defined so host combine matches the GPU kernel.

Co-authored-by: Cursor <cursoragent@cursor.com>
umangyadav and others added 5 commits September 24, 2026 13:58
Treat compile-time scales as safe only when |c| <= 1, drop prePadG0M from
the gate, and keep prefix-causal padding coverage without emitting the
denominator clamp.

Co-authored-by: Cursor <cursoragent@cursor.com>
…v/attn-splitkv-host-combine

Co-authored-by: Cursor <cursoragent@cursor.com>

# Conflicts:
#	mlir/test/fusion/pr-e2e/attention/rock-attention-fully-masked.mlir
@umangyadav
umangyadav requested a balanced review from Copilot September 24, 2026 23:04
@umangyadav umangyadav self-assigned this Sep 24, 2026
@umangyadav umangyadav added the claude-review Request a Claude PR review label Sep 24, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot review overview

🟢 Approval recommended

The recombination logic is consistent with kernel partitioning and includes targeted regression coverage.

Review effort: Balanced
Findings: None

What changed in this PR

Updates host-side split-KV attention recombination to correctly handle fully masked rows and causal multi-tile queries.

Changes:

  • Adds per-row split validity masks for causal attention.
  • Performs narrow-type recombination in f32 with safe normalization.
  • Adds IR and end-to-end regression coverage.
File Description
mlir/​tools/​rocmlir-gen/​rocmlir-gen.cpp Implements robust host recombination and per-row masks.
mlir/​test/​rocmlir-gen/​attention-splitkv-host-perrow-mask.mlir Tests causal per-row validity layout.
mlir/​test/​rocmlir-gen/​attention-splitkv-host-f32-combine.mlir Tests f32 recombination for f16/bf16 storage.
mlir/​test/​rocmlir-gen/​attention-splitkv-host-combine.mlir Tests safe all-masked normalization.
mlir/​test/​fusion/​pr-e2e/​attention/​rock-attention-fully-masked.mlir Adds split-KV end-to-end coverage.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread mlir/tools/rocmlir-gen/rocmlir-gen.cpp
Comment thread mlir/tools/rocmlir-gen/rocmlir-gen.cpp Outdated
Comment thread mlir/test/rocmlir-gen/attention-splitkv-host-perrow-mask.mlir Outdated

@rocmlir-pr-reviewer rocmlir-pr-reviewer 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.

Verdict: COMMENT  ·  Findings: 3 (0 Critical, 0 Major, 3 Minor)


Scope

Host-side (rocmlir-gen) split-KV attention recombination. The PR removes the causalMasking && sequenceLengthQ > nPerBlock bail-out, extends computeValidSplitKV() to emit per-(batch-head, query-row) validity counts under causal / prefix-causal masking, teaches createMaskSplitKV() the [batch, 1, seqQ, 1] constant layout, guards the LSE re-normalization against an all--inf split column (clamp the reduce_max to the lowest finite value, clamp the exp-sum to 1.0), and runs the combine in f32 for f16/bf16 storage. Three new mlir/test/rocmlir-gen/ FileCheck tests plus four split-KV RUN lines in rock-attention-fully-masked.mlir.

Findings

Three Minor findings, none blocking:

  • mlir/tools/rocmlir-gen/rocmlir-gen.cpp:3566 — the f32-combine guard is a two-type allow-list rather than a width test, so other sub-f32 float storage types silently combine at storage precision.
  • mlir/tools/rocmlir-gen/rocmlir-gen.cpp:1471 — the numPerBlock / itersPerBlock / numValidKV computation is duplicated verbatim in both branches of computeValidSplitKV().
  • mlir/test/rocmlir-gen/attention-splitkv-host-perrow-mask.mlir:30 — the per-row validity counts, the thing this PR actually computes, are wildcarded in the tosa.const CHECK.

Notes

  • I worked through the safety argument in the new computeValidSplitKV() comment and it holds. For a row q inside an M block whose last row is q_last >= q, itersPerBlock_host <= itersPerBlock_gpu, so numValidKV_host = ceil((q+1)/itersPerBlock_host) >= ceil((q+1)/itersPerBlock_gpu), i.e. the host can only over-count. Over-counted splits carry the kernel's defined 0 / -inf contribution from the stacked kernel PR and drop out of the weighted sum. numValidKV is also bounded by splitKV and is always >= 1, so the new assert(nPerBlock > 0) is the only precondition that needed stating.
  • Both new clamps are no-ops outside the degenerate case: reduce_max is only -inf when every split LSE is -inf, and sum exp(lse - max) is >= 1 whenever any split is finite. Good.
  • Decode-path behaviour is preserved: for seqLenQ = 1, q = 0, the per-row branch reproduces the old effectiveLastValidKVIndex = 0 result exactly.
  • License headers on all three new .mlir files match the first-party rocmlirTriton template.
  • rocMLIR back-port: mlir/tools/rocmlir-gen/ is on the shared-with-rocMLIR path list. The description carries "no rocMLIR backport necessary", which satisfies option (b), but a one-line reason (e.g. that it depends on the Triton-only kernel guard in the stacked PR) would make the note auditable later.
  • The PR description notes this is stacked and should be retargeted to develop after the base merges; the diff here is correctly limited to the single split-KV commit.

CI status

No failed or cancelled checks. Jenkins (Build and Test, MIGraphX, Code coverage) is still pending at the reviewed SHA, so the new E2E RUN lines are unverified as of this review.

@rocmlir-pr-reviewer rocmlir-pr-reviewer Bot removed the claude-review Request a Claude PR review label Sep 24, 2026
Check the exact per-row validity counts in the per-row mask test and add a
prefix-causal case whose counts differ across rows. Share the split-count
formula in computeValidSplitKV() between the per-row and per-batch paths.

Co-authored-by: Cursor <cursoragent@cursor.com>
@umangyadav umangyadav added the claude-review Request a Claude PR review label Sep 25, 2026
@umangyadav
umangyadav marked this pull request as ready for review September 25, 2026 00:28
Comment thread mlir/test/rocmlir-gen/attention-splitkv-host-perrow-mask.mlir
Comment thread mlir/tools/rocmlir-gen/rocmlir-gen.cpp

@rocmlir-pr-reviewer rocmlir-pr-reviewer 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.

Verdict: COMMENT  ·  New findings: 2 (0 Critical, 0 Major, 2 Minor)


Scope

Removes the rocmlir-gen restriction that rejected causal masking combined with split-KV when sequenceLengthQ > nPerBlock, and reworks the host-side split-KV recombination to match. computeValidSplitKV() now emits per-(batch-head, query-row) validity counts under causal / prefix-causal masking, createMaskSplitKV() grows a [batch, 1, seqQ, 1] constant layout to broadcast them, and computeFinalAttentionStage() upcasts f16/bf16 partials to f32 for the combine while clamping the LSE max to the lowest finite value and the exp-sum to 1 so an all-masked reduction stays defined. Three new rocmlir-gen lit tests plus four new split-KV RUN lines in rock-attention-fully-masked.mlir cover it.

Findings

Two Minor findings, both about defensive coverage rather than correctness:

  • mlir/test/rocmlir-gen/attention-splitkv-host-perrow-mask.mlir:18 — the newly-enabled seqLenQ > nPerBlock causal configuration has no rocmlir-gen lit coverage.
  • mlir/tools/rocmlir-gen/rocmlir-gen.cpp:3514 — the per-row constant layout hardcodes the split axis as dim 1 while index is a parameter.

All three findings from the previous review round are addressed on this revision: the duplicated numPerBlock/itersPerBlock/numValidKV chain is now the single numValidSplits lambda at rocmlir-gen.cpp:1449, the per-row tosa.const CHECKs pin dense<1> instead of wildcarding values, and the new STRADDLE RUN line adds a case where the counts genuinely differ across rows ([[2], [2], [3], [3]], which I re-derived by hand and confirms the min(q + prefixOffset, lastValidIndex) chain). The narrow-storage-type discussion is closed by the author's rationale that rock.attention restricts result/LSE to F32/F16/BF16.

Notes

  • I checked the conservativeness claim in the new computeValidSplitKV() comment. Because the per-row itersPerBlock is derived from a lastValid no larger than the block's, its denominator is never larger than the GPU's, so the host count is always at least the number of splits holding real data for that row — the comment's "cannot exclude a split containing valid row data" direction holds. The count also can never exceed splitKV.
  • tosa.maximum(sumExpNorm, 1.0) is a no-op whenever at least one split is valid, since the max-normalized term contributes exp(0) = 1; it only bites in the all-masked case. The subsequent applyMask(outExp, ..., 0.0f) clears the 0 * -inf NaN that the -inf result mask would otherwise produce.
  • mlir/tools/rocmlir-gen/ is on the checklist's shared-with-rocMLIR path list; the PR description's "no rocMLIR backport necessary" note satisfies option (b), though a one-line reason would make it auditable.

CI status

No failing checks. Jenkins (Build and Test, MIGraphX, Code coverage) is still pending at the reviewed SHA, so the new E2E RUN lines in rock-attention-fully-masked.mlir — including MULTIBLOCK-SPLIT, the only coverage of the newly-enabled regime — have not reported yet.

@rocmlir-pr-reviewer rocmlir-pr-reviewer Bot removed the claude-review Request a Claude PR review label Sep 25, 2026
Add a causal per-row mask case with seq_len_q above the 32-key tile, where
rows 64..95 spread three key tiles over two splits and so get two valid
splits, not three. Assert in createMaskSplitKV() that per-row masks use
dim 1 as the split axis, which the per-row layout assumes.

Co-authored-by: Cursor <cursoragent@cursor.com>
@umangyadav umangyadav added the claude-review Request a Claude PR review label Sep 25, 2026
// build a per-row mask whose validity counts are laid out along the query-row
// axis ([batch, 1, seqQ, 1]) rather than the batch axis ([batch, 1, 1, 1]).
//
// This test exercises that per-row layout (causal, seqLenQ = 4, splitKV = 4)

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.

This header comment still describes only the original configuration ("causal, seqLenQ = 4, splitKV = 4" plus the f16 f32-combine check), but the file now carries four RUN lines: CHECK, PREFIX, STRADDLE (prefix_offset=62, where counts genuinely differ across rows), and the new MULTITILE (seq_len_q 96 against nPerBlockG0=32). MULTITILE is the regime the removed causal exit used to reject, so it is the PR's headline behavior change and a reader scanning the comment would not know it is covered here. Please extend the comment to name the four configurations and what each pins. Checklist Minor: "Lit test missing // RUN: line, -verify-diagnostics, or FileCheck prefix coverage."

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.

Resolved -- addressed in this revision.

// Causal split-KV spanning several query and key tiles, so host recombination
// also exercises per-row validity counts beyond the first query block.
// RUN: rocmlir-gen --arch %arch --operation attention --causal -return_lse -split_kv=8 -seq_len_q 256 -seq_len_k 512 -head_dim_qk 32 -head_dim_v 32 -t f32 -rand 1 -rand_type float -pv | rocmlir-driver --host-pipeline=highlevel | rocmlir-driver -c | mlir-runner -O2 --shared-libs=%linalg_test_lib_dir/libmlir_rocm_runtime%shlibext,%conv_validation_wrapper_library_dir/libconv-validation-wrappers%shlibext,%linalg_test_lib_dir/libmlir_runner_utils%shlibext,%linalg_test_lib_dir/libmlir_float16_utils%shlibext --entry-point-result=void | FileCheck %s --check-prefix=MULTIBLOCK-SPLIT
// MULTIBLOCK-SPLIT: [1 1 1]

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.

The four new split-KV RUN lines each pin a single unanchored [1 1 1], while the pre-existing DIRECT / MIXED-DIRECT / F16-DIRECT / PREFIX-DIRECT lines each pin two ([1 1 1] followed by a -NEXT: [1 1 1]). Because a non-NEXT FileCheck directive scans forward to the first match, a single unanchored [1 1 1] can be satisfied by a different verification result than the intended one, so a regression in the split-KV output verification could be masked by another passing result line. MULTIBLOCK-SPLIT here is the weakest of the four since it pins nothing else. Please either pin the full expected set of result lines (with -NEXT anchoring, as the DIRECT prefixes do) or add a short comment stating why only one result is checked for the split-KV path. Checklist Minor: "Lit test missing // RUN: line, -verify-diagnostics, or FileCheck prefix coverage."

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.

Resolved -- addressed in this revision.

@rocmlir-pr-reviewer rocmlir-pr-reviewer 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.

Verdict: COMMENT  ·  New findings: 2 (0 Critical, 0 Major, 2 Minor)


Scope

Host-side recombination of split-KV attention partials in mlir/tools/rocmlir-gen/rocmlir-gen.cpp: per-row valid-split counts under causal / prefix-causal masking (computeValidSplitKV), a per-row mask layout (createMaskSplitKV), a NaN-safe merge that runs in f32 for f16/bf16 storage (computeFinalAttentionStage), and removal of the seq_len_q > nPerBlock causal exit. Test changes add three new rocmlir-gen lit tests and four split-KV RUN lines to the fully-masked E2E test.

Findings

Two Minor, both in tests:

  • mlir/test/rocmlir-gen/attention-splitkv-host-perrow-mask.mlir:13 — header comment still describes only the original single configuration although the file now has four RUN lines.
  • mlir/test/fusion/pr-e2e/attention/rock-attention-fully-masked.mlir:52 — new split-KV RUN lines each pin a single unanchored result line, unlike the pre-existing DIRECT lines which pin two.

No Critical or Major findings remain, hence COMMENT.

Notes

  • Both previously-flagged items are addressed in commit de63ba40: the new MULTITILE RUN line exercises seq_len_q (96) greater than the key tile (nPerBlockG0=32), and createMaskSplitKV now asserts the split axis is dim 1 on the per-row path.
  • Hand-checked the expected counts in the new tests against computeValidSplitKV: MULTITILE's 32x1 then 64x2 and STRADDLE's 2,2,3,3 are both correct, and the {{...}} regex brackets balance.
  • The conservativeness argument in the new computeValidSplitKV comment holds: using each row's own last index rather than the block's last row can only raise the count, so the host never drops a split that holds valid data.
  • Both createFloatSplatTensor overloads (const APFloat & and float) exist, so the two new call sites resolve.
  • rocMLIR back-port: mlir/tools/rocmlir-gen/ is a shared path, and the PR description's "No rocMLIR back-port needed" note satisfies the requirement.
  • License headers and trailing newlines are correct on all three new .mlir files.

CI status

No checks in the fail or cancel bucket. The only non-passing entry is this auto-review job itself (review, in progress), which is expected. Note that no build or test job ran on this SHA, so there is no compile signal for the new C++.

@rocmlir-pr-reviewer rocmlir-pr-reviewer Bot removed the claude-review Request a Claude PR review label Sep 25, 2026
List the four configurations in the header of the per-row mask test. In the
fully masked E2E test, explain why the split-KV runs check one verification
result: rocmlir-gen verifies the LSE only without split-KV.

Co-authored-by: Cursor <cursoragent@cursor.com>
@umangyadav umangyadav added the claude-review Request a Claude PR review label Sep 25, 2026

@rocmlir-pr-reviewer rocmlir-pr-reviewer 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.

Verdict: APPROVE -- submitted as COMMENT (automated reviews are advisory)  ·  New findings: 0 (0 Critical, 0 Major, 0 Minor)


Scope

Host-side split-KV attention recombination in mlir/tools/rocmlir-gen/rocmlir-gen.cpp: per-(batch-head, query-row) valid-split counts under causal / prefix-causal masking, a NaN-safe merge (clamped LSE max and weight sum), an f32 merge for f16/bf16 storage, and removal of the sequenceLengthQ > nPerBlock && causalMasking exit. Four LIT/E2E tests cover the new behaviour.

Findings

No remaining issues. All seven previously raised findings are addressed in this revision: the duplicated split-count arithmetic is now a single numValidSplits lambda, createMaskSplitKV asserts the split axis is dim 1, the per-row constants are pinned exactly (STRADDLE [2],[2],[3],[3]; MULTITILE 32x1 + 64x2), and both test-header comments now document the configurations and the single-result split-KV checks.

Notes

I re-derived the per-row counts for every FileCheck expectation and they match: MULTITILE (splitKV=2, nPerBlock=32, seqLenQ=96) yields 1 for rows 0-31 and 2 for rows 32-95; STRADDLE (prefixOffset=62) yields [2,2,3,3]. The conservativeness argument in the computeValidSplitKV comment holds: the host itersPerBlock is never larger than the kernel's, so the count can only over-approximate, and count <= splitKV always. Over-counted splits rely on the empty-split zero-output / -inf-LSE guarantee from the base branch, which the PR description calls out.

rocMLIR back-port: mlir/tools/rocmlir-gen/ is a shared path, and the description's "Relation to rocMLIR" section supplies the required justification. No vendored-subtree (external/) paths are touched.

CI status

No failing or cancelled checks.

@rocmlir-pr-reviewer rocmlir-pr-reviewer Bot removed the claude-review Request a Claude PR review label Sep 25, 2026
Base automatically changed from users/umayadav/attn-empty-row-guard to develop September 25, 2026 23:00
umangyadav and others added 2 commits September 27, 2026 22:27
…n-splitkv-host-combine

Co-authored-by: Cursor <cursoragent@cursor.com>

# Conflicts:
#	mlir/lib/Dialect/Rock/Transforms/GridwiseAttnToBlockwise.cpp
#	mlir/test/fusion/pr-e2e/attention/rock-attention-fully-masked.mlir
…pile.

The predicate is only read by asserts, which -DNDEBUG strips out.

Co-authored-by: Cursor <cursoragent@cursor.com>
@dhernandez0

dhernandez0 commented Oct 2, 2026 •

Copy link
Copy Markdown
Collaborator

The counts are conservative. The kernel partitions each M block's keys using the block's last row, while the host uses each row on its own, so the host can include extra empty splits but never drops a split that holds the row's data. The extra splits are harmless because #534 makes the kernel write zero output and -inf LSE for empty splits.

Note that at some point, we might want to have real early exit. Where if a workgroup has not work to do, it exits without even writing to memory, which is more efficient. If that is implemented, the CPU version needs to be updated as well. Because we are assuming memory is initialized.

It might be worth adding a note/comment or a TODO.

Also, the original implementation of rocmlir-gen (before this PR) assumed that data was not initialized, because originally that's how early exit was done. But we found some bugs that made us revert that to always init memory.

//
// rocmlir-gen verifies the LSE only without split-KV; with split-KV the LSE is
// an intermediate of the host recombination. So the direct runs check two
// verification results (output and LSE) and the split-KV runs check one.

@dhernandez0 dhernandez0 Oct 2, 2026 •

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 reason for this is that split-kv chooses where each split happens dynamically, based on seqLen and tile size. That would be hard to replicate on the CPU (but possible).

If the split doesn't match, LSE/output of attention won't match. So, we decided to just match the output after the "second kernel", then it should match.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

can you check if this is explained somewhere? if not it might be worth adding as a comment.

// dimension is GEMM0's N axis. Split-KV partitions the first GEMM along
// the key-sequence dimension, hence we use gemm0NPerBlock here.
static SmallVector<int32_t> computeValidSplitKV(int64_t nPerBlock) {
assert(nPerBlock > 0 && "tile size must be positive");

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.

nit: if we assume memory is initialized. I think we can simplify this even more. Just do the reduction over all splits?

"Number of valid split KV must match last valid K/V indices");
if (!lastValidKVIndex.empty()) {
[[maybe_unused]] size_t perBatchHead = numHeadsQ * lastValidKVIndex.size();
[[maybe_unused]] size_t perRow = perBatchHead * sequenceLengthQ;

@dhernandez0 dhernandez0 Oct 2, 2026 •

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 reminds me, we should add a Release compilation to CI. Otherwise, we could merge PRs without [[maybe_unused]] here.

auto elementType = cast<ShapedType>(lseTensor.getType()).getElementType();
Type storageType = cast<ShapedType>(lseTensor.getType()).getElementType();
Type computeType = storageType;
if (isa<Float16Type, BFloat16Type>(storageType))

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.

nit: floating point < f32? just in case attn is extended to smaller data types in the future

umangyadav and others added 2 commits October 2, 2026 15:46
…n-splitkv-host-combine

Co-authored-by: Cursor <cursoragent@cursor.com>
@umangyadav
umangyadav enabled auto-merge (squash) October 2, 2026 15:53
@umangyadav
umangyadav merged commit 8d8f016 into develop Oct 2, 2026
8 of 9 checks passed
@umangyadav
umangyadav deleted the users/umayadav/attn-splitkv-host-combine branch October 2, 2026 20:19
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.

4 participants