Skip to content

feat(cake_kda): share recurrent prefill kernels across SM100 family - #4313

Merged
yyihuang merged 1 commit into
flashinfer-ai:mainfrom
yyihuang:codex/flashkda-prefill-gb300-export
Aug 4, 2026
Merged

yyihuang merged 1 commit into
flashinfer-ai:mainfrom
yyihuang:codex/flashkda-prefill-gb300-export

Conversation

@yyihuang

@yyihuang yyihuang commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator

Important

Follow-up to merged #4262. This PR contains only SM100-family packaging,
routing, validation, and GB300 enablement; it does not add another frozen
prefill kernel body.

Description

Compile the existing frozen recurrent-KDA prefill M64 and M128 kernels once as
sm_100f on CUDA 12.9 and newer, and use those modules on both CC 10.0
(B200/GB200) and CC 10.3 (B300/GB300). CUDA 12.8 B200 keeps the exact
sm_100a compatibility modules because that toolkit predates sm_100f.

The M64 and M128 generated CUDA bodies remain byte-identical to #4262. JIT and
AOT use target-bearing module identities, and the binding accepts the family
module only on CC 10.0 or CC 10.3.

Validation

Publication commit: 8d3bbb27e26795609638f8002d345917c6d802c5 (tree
c859b6d5241b5994cc9ccf4ceb5faacce15f4057), based directly on upstream
main@4433996e after #4279 merged. The incremental prefill implementation is
identical to the candidate validated on both GPUs: the ten non-AOT files are
blob-identical, and their stable patch-id is unchanged. The only replay
resolution was mechanical in flashinfer/aot.py: it preserves the merged
#4279 decode registration while applying the same prefill family registration.

Gate B200 GB300
Public prefill GPU tests 39/39 passed 39/39 passed
Targeted CPU tests at publication commit 26 passed, 30 skipped same commit
Pre-commit at publication commit passed same commit

Strict CUPTI, cold-L2, balanced-process A/B below compares the family target
with the same frozen bodies compiled for the exact device target. Speedup is
exact / sm100f; values near 1 mean target consolidation is performance
neutral.

GPU / run Historical six H6 four All ten
B200 0.9994x 0.9992x 0.9993x
GB300 primary ABBA 0.9996x 0.9905x 0.9959x
GB300 reverse-BAAB confirmation 0.9999x 0.9957x 0.9982x

The only stable tiny-shape difference is h6_packed_n8_t128: sm100f
25.664 us versus exact sm103a 25.360 us (0.9882x, 0.304 us absolute).
Keeping an extra exact M128 target for this one coordinate would reintroduce a
third target and shape-specific routing, so this PR retains the simpler shared
family module.

The existing six-shape comparison with pinned official FlashKDA remains the
performance baseline documented by this PR; the new A/B isolates only the
physical target change.

Related to #4254.

Summary by CodeRabbit

  • New Features

    • Added recurrent KDA prefill support for SM100a and SM103a GPUs.
    • Added automatic selection of compatible kernels based on GPU architecture and CUDA version.
    • Added support for smaller head counts through automatic beta-data padding.
    • Improved hardware information included in benchmark results.
  • Bug Fixes

    • Improved device and input validation, shared-memory checks, and launch error reporting.
  • Documentation

    • Updated KDA prefill documentation with supported architectures and CUDA requirements.

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Recurrent KDA prefill now supports SM100a and SM103a. Target selection depends on compute capability and CUDA version. JIT/AOT loading, CUDA bindings, beta packing, tests, documentation, and benchmark metadata were updated.

Changes

Recurrent KDA prefill

Layer / File(s) Summary
Architecture-aware JIT and AOT targets
flashinfer/jit/flash_kda.py, flashinfer/aot.py, tests/jit/test_flash_kda_jit.py
FlashKDA JIT and AOT generation now accepts sm100a and sm100f targets, uses target-specific flags and cache keys, and registers modules by CUDA-version-specific architecture capabilities.
Compute-capability prefill dispatch
flashinfer/kda_prefill.py, flashinfer/kda.py, tests/kda/test_recurrent_kda_prefill.py
Prefill dispatch now selects targets for SM100a and SM103a, passes targets through module lookup, and updates backend validation messages.
Beta packing and fused CUDA bindings
csrc/kda/flashkda_binding_common.cuh, csrc/kda/flashkda_bf16_fused_m64_binding.cu, csrc/kda/flashkda_bf16_fused_m128_binding.cu
Bindings now validate SM100-family targets, materialize padded beta data for sub-eight-head inputs, and export a validated fused M128 launch path.
Validation, benchmarks, and documentation
tests/kda/test_recurrent_kda_prefill.py, benchmarks/bench_recurrent_kda_prefill.py, docs/api/kda_prefill.rst
Tests cover SM100/SM103 routing, numerical results, beta handling, and CUDA-graph replay. Benchmarks record hardware metadata. Documentation describes CUDA 12.8 and CUDA 12.9+ target behavior.

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

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant recurrent_kda
  participant RecurrentKDAPrefillWorkspace
  participant flash_kda
  participant FlashKDABinding
  Caller->>recurrent_kda: submit prefill tensors
  recurrent_kda->>RecurrentKDAPrefillWorkspace: validate and select target
  RecurrentKDAPrefillWorkspace->>flash_kda: load variant and target module
  flash_kda-->>RecurrentKDAPrefillWorkspace: return compiled module
  RecurrentKDAPrefillWorkspace->>FlashKDABinding: launch with beta_tma
  FlashKDABinding-->>Caller: return output and final state
Loading

Possibly related PRs

Suggested reviewers: kahyunnam, yongwww, yzh119

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.26% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly summarizes the main change: sharing recurrent prefill kernels across the SM100 family.
Description check ✅ Passed The description explains the implementation, scope, related issue, validation results, performance data, and known limitations.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@yzh119

yzh119 commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator

What's the difference between this sm103 kernel compared to the sm100 kernel, any B300 specific features being used here?

Also, where does the host-side overhead coming from, we should remove that overhead.

@yongwww

yongwww commented Aug 2, 2026

Copy link
Copy Markdown
Member

/bot run

@yyihuang
yyihuang marked this pull request as ready for review August 2, 2026 04:54
@yyihuang
yyihuang requested a review from yzh119 as a code owner August 2, 2026 04:54
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@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 (8)
benchmarks/bench_recurrent_kda_prefill.py (1)

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

Consider detecting untracked files in the provenance check.

--untracked-files=no hides new untracked sources in the checkout. An editable FlashKDA build can compile such files. The reported source_commit then does not fully describe the measured peer. If you want a strict provenance claim, drop the flag or add a separate untracked-file check.

🤖 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 `@benchmarks/bench_recurrent_kda_prefill.py` around lines 168 - 177, Update the
provenance check around _git_output to detect untracked files as well as tracked
modifications by removing "--untracked-files=no" or adding a separate
untracked-file validation. Ensure any untracked checkout contents cause the
existing RuntimeError, so source_commit fully identifies the measured FlashKDA
checkout.
csrc/kda/flashkda_bf16_fused_m64_binding.cu (1)

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

Call PackBetaForTmaIfNeeded for symmetry with the M128 binding.

The M128 binding calls PackBetaForTmaIfNeeded(beta, beta_tma, num_heads, stream) at line 72 of csrc/kda/flashkda_bf16_fused_m128_binding.cu. This binding omits the call.

The omission is correct today. Line 59 enforces num_heads == 64, and PackBetaForTmaIfNeeded returns immediately for num_heads >= kBetaTmaMinHeads. The call is therefore a no-op here.

Add the call anyway. It costs nothing at runtime and it keeps the two launch paths identical, so a future change to the head constraint or to the packing rule cannot silently skip packing in the M64 path.

♻️ Proposed change
   const TmaPointers tma = EncodeTmaPointers<64>(q, k, v, g, beta_tma, out, descriptor_storage,
                                                 prepare_descriptors, stream);
+  // No-op while the M64 variant pins H=64, kept for parity with the M128 path.
+  PackBetaForTmaIfNeeded(beta, beta_tma, num_heads, stream);
🤖 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 `@csrc/kda/flashkda_bf16_fused_m64_binding.cu` around lines 76 - 80, Add a call
to PackBetaForTmaIfNeeded(beta, beta_tma, num_heads, stream) in the M64 binding
before EncodeTmaPointers, matching the M128 binding’s launch setup. Keep the
existing num_heads validation and stream usage unchanged.
csrc/kda/flashkda_binding_common.cuh (3)

127-137: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Every prefill launch repeats five CUDA driver calls for setup that never changes. The shared root cause is that device properties and the kernel shared-memory attribute are queried and set per launch instead of once per device and once per kernel. The compute capability and the opt-in shared-memory limit are fixed for the process lifetime. cudaFuncSetAttribute is idempotent for a given kernel and value. The PR reviewer asked to remove host-side overhead, and these calls sit directly on the prefill launch path.

  • csrc/kda/flashkda_binding_common.cuh#L127-L137: cache the compute capability per device_id in CheckExactFlashKDATarget so later launches skip both cudaDeviceGetAttribute calls.
  • csrc/kda/flashkda_binding_common.cuh#L504-L511: cache cudaDevAttrMaxSharedMemoryPerBlockOptin per device_id in CheckDynamicSmemCapacity so later launches skip the query.
  • csrc/kda/flashkda_bf16_fused_m64_binding.cu#L67-L69: run cudaFuncSetAttribute for kernel_flashkda_bf16_fused_m64 once per device, for example behind a static per-device flag, instead of on every RunM64 call.
  • csrc/kda/flashkda_bf16_fused_m128_binding.cu#L60-L62: run cudaFuncSetAttribute for kernel_flashkda_bf16_fused_m128 once per device, instead of on every RunM128 call.

Keep the validation semantics unchanged. A cached value must still produce the same TVM_FFI_ICHECK failure message when the device does not match the compiled target.

🤖 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 `@csrc/kda/flashkda_binding_common.cuh` around lines 127 - 137, Cache compute
capability per device in CheckExactFlashKDATarget while preserving the existing
validation and failure message; cache opt-in shared-memory capacity per device
in CheckDynamicSmemCapacity. In csrc/kda/flashkda_bf16_fused_m64_binding.cu
lines 67-69 and csrc/kda/flashkda_bf16_fused_m128_binding.cu lines 60-62, guard
each kernel’s cudaFuncSetAttribute call with a per-device one-time flag so it
runs once per device rather than per launch.

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

Add the trailing-dimension guards that EncodeQkTma already has.

EncodeValueTma at line 357 and EncodeGateTma at line 381 compute tensor.numel() / (d1 * d2) without first checking d1 > 0 && d2 > 0, and without checking tensor.ndim() >= 2 before calling tensor.size(tensor.ndim() - 2). EncodeQkTma includes both guards at lines 327 and 330.

CheckCommonInputs validates the v and g shapes before these functions run, so no division by zero occurs today. Add the guards so the helpers stay safe if a future caller skips CheckCommonInputs.

🛡️ Proposed guards
 template <int ValueRows>
 inline CUtensorMap EncodeValueTma(const TensorView& tensor) {
   static_assert(ValueRows == 64 || ValueRows == 128);
+  TVM_FFI_ICHECK(tensor.ndim() >= 2) << "v must have at least two dimensions";
   const int64_t d1 = tensor.size(tensor.ndim() - 1);
   const int64_t d2 = tensor.size(tensor.ndim() - 2);
+  TVM_FFI_ICHECK(d1 > 0 && d2 > 0) << "v has invalid trailing dimensions";
   const int64_t outer2 = tensor.numel() / (d1 * d2);
 inline CUtensorMap EncodeGateTma(const TensorView& tensor) {
+  TVM_FFI_ICHECK(tensor.ndim() >= 2) << "g must have at least two dimensions";
   const int64_t d1 = tensor.size(tensor.ndim() - 1);
   const int64_t d2 = tensor.size(tensor.ndim() - 2);
+  TVM_FFI_ICHECK(d1 > 0 && d2 > 0) << "g has invalid trailing dimensions";
   const int64_t outer2 = tensor.numel() / (d1 * d2);
🤖 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 `@csrc/kda/flashkda_binding_common.cuh` around lines 352 - 399, Add the same
ndim and trailing-dimension validation used by EncodeQkTma to both
EncodeValueTma and EncodeGateTma before accessing d2 or computing outer2:
require tensor.ndim() >= 2, then require d1 > 0 and d2 > 0 before dividing.
Preserve the existing TMA encoding and shape checks after these guards.

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

Replace the literal 8 and 32 with named constants.

Line 220 uses the literal 8 while kBetaTmaMinHeads holds the same value and is used at line 227. Line 223 uses the literal 32 for the minimum padded token count. EncodeBetaTma repeats both literals at line 405, and _beta_tma_source in flashinfer/kda_prefill.py repeats 32 at line 384.

Use kBetaTmaMinHeads at line 220. Add a kBetaTmaMinTokens = 32 constant and use it at line 223 and at line 405.

♻️ Proposed change
-  const int64_t beta_tma_heads = std::max<int64_t>(num_heads, 8);
+  const int64_t beta_tma_heads = std::max<int64_t>(num_heads, kBetaTmaMinHeads);
   TVM_FFI_ICHECK(beta_tma.ndim() >= 2 && beta_tma.size(beta_tma.ndim() - 1) == beta_tma_heads &&
                  beta_tma.numel() % beta_tma_heads == 0 &&
-                 beta_tma.numel() / beta_tma_heads >= std::max<int64_t>(token_count, 32))
+                 beta_tma.numel() / beta_tma_heads >= std::max<int64_t>(token_count, kBetaTmaMinTokens))

Add the constant next to kBetaTmaMinHeads:

constexpr int64_t kBetaTmaMinTokens = 32;
🤖 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 `@csrc/kda/flashkda_binding_common.cuh` around lines 220 - 225, Replace the
literal 8 in the beta_tma_heads calculation and validation with the existing
kBetaTmaMinHeads constant. Define kBetaTmaMinTokens as an int64_t constant
alongside kBetaTmaMinHeads, then use it for the minimum token validation and the
corresponding padding logic in EncodeBetaTma; preserve the existing thresholds
and behavior.
flashinfer/kda.py (1)

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

The module description is not the module docstring.

The license block at lines 1-15 is the module docstring. The second string literal at lines 17-25 is a bare expression statement. flashinfer.kda.__doc__ and help(flashinfer.kda) show only the license text. Merge the description into the first string literal, or convert lines 17-25 into a comment block.

The same pattern exists in flashinfer/kda_prefill.py at lines 17-24.

🤖 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 `@flashinfer/kda.py` around lines 17 - 25, Merge the KDA facade description
into the leading module docstring in kda.py so flashinfer.kda.__doc__ and help()
include it, rather than leaving it as a separate string expression. Apply the
same correction to the module description in kda_prefill.py, preserving the
existing license text and descriptive content.
flashinfer/kda_prefill.py (2)

606-627: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

final_state_arg is assigned in two places with a duplicated condition.

On the path where initial_state is None, output_final_state is True, and prefill_workspace is not None, this block leaves final_state_arg unbound. Line 668 binds it, guarded by the same condition restated. The code is correct today. If either condition changes, line 710 raises UnboundLocalError.

Bind final_state_arg once. Set it to dummy_state at line 621 and overwrite it at line 668. Also remove the redundant if initial_state is None: at line 673, because line 667 already establishes it.

♻️ Proposed restructure
     elif output_final_state:
         initial_state_arg = dummy_state
         if prefill_workspace is None:
             final_state_arg = torch.empty(
                 state_shape, dtype=torch.bfloat16, device=q.device
             )
             returned_state = final_state_arg
         else:
             # Assigned to caller-owned stable state scratch under its lock.
+            final_state_arg = dummy_state
             returned_state = None
         store_final_state = True
         if initial_state is None and output_final_state and explicit_workspace:
             final_state_arg = _state_scratch(
                 workspace=workspace,
                 device=q.device,
                 shape=state_shape,
             )
-            if initial_state is None:
-                returned_state = final_state_arg
+            returned_state = final_state_arg
🤖 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 `@flashinfer/kda_prefill.py` around lines 606 - 627, Initialize final_state_arg
to dummy_state in the output_final_state branch before the prefill_workspace
check, then overwrite it where the caller-owned stable state is assigned. Remove
the redundant initial_state is None guard around that later assignment, relying
on the existing branch structure to establish the condition and ensure
final_state_arg is always bound.

254-267: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

The global tensor cache and the stream workspace map grow without bound.

_flash_kda_tensor_cache adds one entry per distinct (kind, device_index, stream_ptr, shape...) tuple and never removes an entry. _flash_kda_stream_workspaces adds one _FlashKDAStreamWorkspace per stream and never removes an entry. A long-running server that uses many batch sizes, sequence lengths, or short-lived CUDA streams accumulates device allocations for the process lifetime. Each workspace also retains beta padding and two 768-byte descriptor buffers.

The stream key uses the raw cuda_stream handle. CUDA can reuse a handle value after a stream is destroyed, so a later stream can hit an entry created for a destroyed stream.

Add a bound or an eviction path. One option is an LRU with a size cap. Another option is to key the stream workspace map by a weakref to the torch.cuda.Stream object so the entry is dropped when the stream is released.

Based on learnings: "ensure that every tensor whose data_ptr() is included in the key also participates in cache eviction/invalidation... verify the cache invalidation triggers when those key tensors go out of scope."

🤖 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 `@flashinfer/kda_prefill.py` around lines 254 - 267, Bound and add
eviction/invalidation for both _flash_kda_tensor_cache and
_flash_kda_stream_workspaces so allocations do not grow for the process
lifetime. Ensure stream workspaces cannot be reused solely from stale raw
cuda_stream handles, and invalidate every cached tensor entry when any tensor
whose data_ptr() contributes to its key is released or otherwise becomes
invalid. Preserve cache reuse for live resources while removing entries and
associated workspace buffers when their key tensors or streams go out of scope.

Source: Learnings

🤖 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 `@benchmarks/bench_recurrent_kda_prefill.py`:
- Around line 238-241: Update the state rotation sizing around _make_state_pool
and state_cursors so state_rotations covers every iteration executed by the full
bench_gpu_time block, including warmup_ms and bench_ms across both timing
backends. Derive the pool size from a measured post-warmup per-call duration, or
increase and document DEFAULT_STATE_ROTATIONS with its memory cost, while
preserving the existing cursor behavior.

In `@csrc/kda/flashkda_binding_common.cuh`:
- Around line 464-494: Add a tensormap release fence at the end of
PublishTensorMaps after all destination writes, using the
fence.proxy.tensormap::generic.release.gpu instruction so descriptor updates
through the generic proxy are visible to TMA consumers. Keep the existing launch
and descriptor preparation flow unchanged.

In `@flashinfer/jit/flash_kda.py`:
- Around line 40-70: Update gen_flash_kda_module to follow the standard JIT
generator flow: compute a unique URI, create its directory under
FLASHINFER_GEN_SRC_DIR, copy the sources from _get_flash_kda_csrc_dir() into
that directory, and pass the copied paths to gen_jit_spec. Preserve the
per-schedule translation-unit behavior and ensure no generated or compiled
artifacts are written to package source directories.

---

Nitpick comments:
In `@benchmarks/bench_recurrent_kda_prefill.py`:
- Around line 168-177: Update the provenance check around _git_output to detect
untracked files as well as tracked modifications by removing
"--untracked-files=no" or adding a separate untracked-file validation. Ensure
any untracked checkout contents cause the existing RuntimeError, so
source_commit fully identifies the measured FlashKDA checkout.

In `@csrc/kda/flashkda_bf16_fused_m64_binding.cu`:
- Around line 76-80: Add a call to PackBetaForTmaIfNeeded(beta, beta_tma,
num_heads, stream) in the M64 binding before EncodeTmaPointers, matching the
M128 binding’s launch setup. Keep the existing num_heads validation and stream
usage unchanged.

In `@csrc/kda/flashkda_binding_common.cuh`:
- Around line 127-137: Cache compute capability per device in
CheckExactFlashKDATarget while preserving the existing validation and failure
message; cache opt-in shared-memory capacity per device in
CheckDynamicSmemCapacity. In csrc/kda/flashkda_bf16_fused_m64_binding.cu lines
67-69 and csrc/kda/flashkda_bf16_fused_m128_binding.cu lines 60-62, guard each
kernel’s cudaFuncSetAttribute call with a per-device one-time flag so it runs
once per device rather than per launch.
- Around line 352-399: Add the same ndim and trailing-dimension validation used
by EncodeQkTma to both EncodeValueTma and EncodeGateTma before accessing d2 or
computing outer2: require tensor.ndim() >= 2, then require d1 > 0 and d2 > 0
before dividing. Preserve the existing TMA encoding and shape checks after these
guards.
- Around line 220-225: Replace the literal 8 in the beta_tma_heads calculation
and validation with the existing kBetaTmaMinHeads constant. Define
kBetaTmaMinTokens as an int64_t constant alongside kBetaTmaMinHeads, then use it
for the minimum token validation and the corresponding padding logic in
EncodeBetaTma; preserve the existing thresholds and behavior.

In `@flashinfer/kda_prefill.py`:
- Around line 606-627: Initialize final_state_arg to dummy_state in the
output_final_state branch before the prefill_workspace check, then overwrite it
where the caller-owned stable state is assigned. Remove the redundant
initial_state is None guard around that later assignment, relying on the
existing branch structure to establish the condition and ensure final_state_arg
is always bound.
- Around line 254-267: Bound and add eviction/invalidation for both
_flash_kda_tensor_cache and _flash_kda_stream_workspaces so allocations do not
grow for the process lifetime. Ensure stream workspaces cannot be reused solely
from stale raw cuda_stream handles, and invalidate every cached tensor entry
when any tensor whose data_ptr() contributes to its key is released or otherwise
becomes invalid. Preserve cache reuse for live resources while removing entries
and associated workspace buffers when their key tensors or streams go out of
scope.

In `@flashinfer/kda.py`:
- Around line 17-25: Merge the KDA facade description into the leading module
docstring in kda.py so flashinfer.kda.__doc__ and help() include it, rather than
leaving it as a separate string expression. Apply the same correction to the
module description in kda_prefill.py, preserving the existing license text and
descriptive content.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 9fe5a4b1-fccd-4c66-88ce-57659c012d10

📥 Commits

Reviewing files that changed from the base of the PR and between d020372 and 3dd1887.

📒 Files selected for processing (18)
  • benchmarks/bench_recurrent_kda_prefill.py
  • csrc/kda/flashkda_bf16_fused_m128.cu
  • csrc/kda/flashkda_bf16_fused_m128_binding.cu
  • csrc/kda/flashkda_bf16_fused_m64.cu
  • csrc/kda/flashkda_bf16_fused_m64_binding.cu
  • csrc/kda/flashkda_binding_common.cuh
  • docs/api/kda.rst
  • docs/api/kda_prefill.rst
  • docs/index.rst
  • flashinfer/__init__.py
  • flashinfer/aot.py
  • flashinfer/jit/__init__.py
  • flashinfer/jit/flash_kda.py
  • flashinfer/kda.py
  • flashinfer/kda_kernels/__init__.py
  • flashinfer/kda_prefill.py
  • tests/jit/test_flash_kda_jit.py
  • tests/kda/test_recurrent_kda_prefill.py

Comment on lines +238 to +241
candidate_state_pool = _make_state_pool(initial_state, state_rotations)
candidate_output = torch.empty_like(q)
candidate_workspace = RecurrentKDAPrefillWorkspace(q.device)
state_cursors = {"pr": [0], "adapted": [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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect bench_gpu_time to confirm how dry_run_time_ms/repeat_time_ms map to iteration counts.
fd -t f 'profiler.py|utils.py|__init__.py' flashinfer/testing | while IFS= read -r f; do
  rg -n -C 20 'def bench_gpu_time' "$f"
done

Repository: flashinfer-ai/flashinfer

Length of output: 7041


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== target context =="
sed -n '220,280p' benchmarks/bench_recurrent_kda_prefill.py

echo "== bench implementation =="
sed -n '1546,1660p' flashinfer/testing/profiler.py

echo "== _make_state_pool and cursor use =="
sed -n '198,270p' benchmarks/bench_recurrent_kda_prefill.py

echo "== searches for candidate_run / bench_gpu_time in benchmark =="
rg -n 'candidate_run|state_rotations|bench_gpu_time|dry_run|repeat' benchmarks/bench_recurrent_kda_prefill.py flashinfer/testing/profiler.py

Repository: flashinfer-ai/flashinfer

Length of output: 2064


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== files =="
git ls-files | rg '(^|/)bench_recurrent_kda_prefill\.py$|profiler\.py$|testing'
echo

echo "== target context =="
file_target="$(git ls-files | rg '(^|/)bench_recurrent_kda_prefill\.py$' | head -n1)"
sed -n '180,310p' "$file_target"

echo
echo "== search profiler symbols =="
rg -n -C 12 'def bench_gpu_time|def bench_gpu_time_with_cupti|def bench_gpu_time_with_cugraph|repeat_time_ms|dry_run_time_ms|for iter_idx in range\(repeat_iters\)' .

echo
echo "== _make_state_pool and cursor use complete =="
rg -n -C 15 '_make_state_pool|state_rotations|state_cursors|candidate_run|bench_gpu_time' "$file_target"

Repository: flashinfer-ai/flashinfer

Length of output: 50381


🏁 Script executed:

#!/bin/bash
set -euo pipefail

target=$(git ls-files | rg '(^|/)bench_recurrent_kda_prefill\.py$' | head -n1)
profiler_file=$(git ls-files | rg '(^|/)profiler\.py$' | head -n1)
utils_file=$(git ls-files | rg '(^|/)utils\.py$' | head -n1)

echo "== benchmark function invocation =="
sed -n '440,530p' "$target"

echo
echo "== _make_state_pool implementation references =="
rg -n -C 8 '_make_state_pool|state_rotations\s*=\s*|default.*state|--state|state-rotations' benchmarks docs .

echo
echo "== profiler wrapper sequence =="
line=$(rg -n 'def bench_gpu_time\(' "$profiler_file" | head -n1 | cut -d: -f1)
sed -n "${line},$((line+180))p" "$profiler_file"

echo
echo "== profiling backend wrappers relevant sections =="
for name in bench_gpu_time_with_cuda_event bench_gpu_time_with_cupti bench_gpu_time_with_cudagraph; do
  line=$(rg -n "def $name" "$profiler_file" | head -n1 | cut -d: -f1)
  echo "--- $name: $line"
  sed -n "${line},$((line+160))p" "$profiler_file"
done

echo
echo "== argument parser for state rotations =="
rg -n -C 8 'StateRotations|state_rotations|add_argument\(--.*state|add_argument\('--state' "$target"

Repository: flashinfer-ai/flashinfer

Length of output: 162


🏁 Script executed:

#!/bin/bash
set -euo pipefail

target=$(git ls-files | rg '(^|/)bench_recurrent_kda_prefill\.py$' | head -n1)
echo "target=$target"

echo "== benchmark function invocation =="
sed -n '440,530p' "$target"

echo
echo "== _make_state_pool implementation references =="
rg -n -C 8 '_make_state_pool|state_rotations\s*=|default.*state|--state-rotations|add_argument.*state' "$target" docs .

echo
echo "== profiler wrapper sequence =="
profiler_file=$(git ls-files | rg '(^|/)profiler\.py$' | head -n1)
echo "profiler_file=$profiler_file"
line=$(rg -n 'def bench_gpu_time\(' "$profiler_file" | head -n1 | cut -d: -f1)
sed -n "${line},$((line+180))p" "$profiler_file"

echo
echo "== profiling backend wrappers relevant sections =="
for name in bench_gpu_time_with_cuda_event bench_gpu_time_with_cupti bench_gpu_time_with_cudagraph; do
  line=$(rg -n "def $name" "$profiler_file" | head -n1 | cut -d: -f1)
  echo "--- $name: $line"
  sed -n "${line},$((line+160))p" "$profiler_file"
done

Repository: flashinfer-ai/flashinfer

Length of output: 50381


🏁 Script executed:

#!/bin/bash
set -euo pipefail

target=$(git ls-files | rg '(^|/)bench_recurrent_kda_prefill\.py$' | head -n1)
echo "== DEFAULT_STATE_ROTATIONS and constants =="
rg -n -C 4 'DEFAULT_STATE_ROTATIONS|DEFAULT.*ROTATION|class Case|seq_lens|NUM.*HEADS' "$target"

echo
echo "== profiler imports =="
rg -n 'from .*profiler import|import .*profiler|bench_gpu_time_with_cuda_event|bench_gpu_time_with_cupti|bench_gpu_time_with_cudagraph' flashinfer/testing/__init__.py flashinfer/testing/utils.py "$target"

Repository: flashinfer-ai/flashinfer

Length of output: 4967


🌐 Web query:

CUDA profiler CUPTI activity synchronization per iteration timing loop bench_gpu_time_with_cupti

💡 Result:

To accurately measure GPU kernel execution time while utilizing CUPTI activity synchronization, it is essential to understand that the CUPTI Activity API operates asynchronously [1][2][3]. CUPTI does not provide direct "per-iteration" timing hooks in the way a simple start/stop timer would; instead, it records activity events—such as kernel executions and synchronization calls—with high-precision hardware timestamps [4][5]. To benchmark or profile a loop involving kernel execution and synchronization, follow these practices: 1. Enable Synchronization Tracking: Synchronization operations (like cudaDeviceSynchronize or stream synchronization) can be tracked by enabling CUPTI_ACTIVITY_KIND_SYNCHRONIZATION [4][6]. You can use cuptiActivityEnableAllSyncRecords(1) to ensure these records are captured [7][8]. 2. Correlation via IDs: CUPTI activity records, such as CUpti_ActivityKernel* and CUpti_ActivitySynchronization*, contain fields (like contextId, streamId, and often unique synchronization or event IDs) that allow you to correlate synchronization events with the specific kernel or memory operation they are waiting for [4][5][9]. 3. Asynchronous Buffer Handling: Because CUPTI is asynchronous, you must register callbacks (using cuptiActivityRegisterCallbacks) to process activity records as they are written to buffers [10][1][2]. Attempting to "synchronize" timing inside the loop by directly reading these buffers will introduce significant overhead and break the asynchronous nature of the profiler [1][2]. Instead, the typical workflow is: - Launch your kernels and perform synchronization in your application loop normally [10]. - Allow CUPTI to asynchronously populate buffers via the registered callback [10][1]. - Parse the buffers after the loop or at a later point to extract the start and end timestamps (in nanoseconds) from the kernel activity records and compare them against the synchronization activity records to derive the effective duration [10][5][9]. 4. Flushing Buffers: To ensure all records are captured for a specific benchmark iteration, use cuptiActivityFlushAll after your timed loop completes [10][1][2]. This forces CUPTI to deliver the remaining records in the buffers so they can be processed and correlated with your iteration data [1][2]. For precise timing, rely on the nanosecond-precision start and end timestamps provided within the activity records themselves rather than relying on host-side timing, which is prone to skew due to the asynchronous nature of kernel launches [5][9].

Citations:


Make state_rotations account for the full timed block.

state_cursors["pr"][0] += 1 once per bench_gpu_time() call, and the default call covers warmup_ms + bench_ms via both CUPTI and CUDA graph backends. With DEFAULT_STATE_ROTATIONS = 512, a 120 ms block fails with PR state rotations exhausted if iterations average under ~235 us. Size the pool from an measured per-call time after warmup, or document a sufficient default and its memory cost.

Comment on lines +464 to +494
static __global__ void PublishTensorMaps(uint64_t* destination, TensorMapWords source) {
const uint32_t index = threadIdx.x;
if (index < TensorMapWords::kWordCount) {
destination[index] = source.words[index];
}
}

template <int ValueRows>
inline TmaPointers EncodeTmaPointers(const TensorView& q, const TensorView& k, const TensorView& v,
const TensorView& g, const TensorView& beta_tma,
const TensorView& out, const TensorView& descriptor_storage,
int64_t prepare_descriptors, cudaStream_t stream) {
if (prepare_descriptors != 0) {
cudaStreamCaptureStatus capture_status = cudaStreamCaptureStatusNone;
CheckCuda(cudaStreamIsCapturing(stream, &capture_status),
"cudaStreamIsCapturing(TMA descriptor preparation)");
TVM_FFI_ICHECK(capture_status == cudaStreamCaptureStatusNone)
<< "prepare_descriptors must be 0 during CUDA graph capture; warm "
"this exact workspace and tensor signature before capture";

const std::array<CUtensorMap, kTensorMapCount> host_maps = {
EncodeQkTma(q, "q"), EncodeQkTma(k, "k"), EncodeValueTma<ValueRows>(v),
EncodeGateTma(g), EncodeBetaTma(beta_tma), EncodeOutputTma<ValueRows>(out),
};
static_assert(sizeof(host_maps) == kDescriptorStorageBytes);
TensorMapWords words{};
std::memcpy(words.words, host_maps.data(), sizeof(host_maps));
PublishTensorMaps<<<1, 128, 0, stream>>>(
reinterpret_cast<uint64_t*>(descriptor_storage.data_ptr()), words);
CheckCuda(cudaGetLastError(), "PublishTensorMaps launch");
}

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

🧩 Analysis chain

🌐 Web query:

PTX fence.proxy.tensormap generic release required device-side tensormap update kernel boundary

💡 Result:

The PTX instruction fence.proxy.tensormap::generic.release is a uni-directional fence used to establish ordering between memory accesses performed via different proxies [1][2]. Specifically, it acts as a release fence that synchronizes with an acquire sequence containing the fence.proxy.tensormap::generic.acquire instruction [1][2]. These instructions are primarily used on NVIDIA GPU architectures with compute capability 9.0 or higher (Hopper and later) to manage the coherence of Tensor Map (TMA) descriptors [3][4]. When a tensor map object is updated—such as when modifying its base address, dimensions, or strides in global or shared memory—these fence operations are required to ensure that the modifications (performed via the generic proxy) are visible to the hardware TMA engine (which reads through the tensormap proxy) [5][6]. Key technical aspects include: - Functionality: The release fence establishes a release sequence, and the acquire fence consumes it, ensuring that memory updates become visible to the subsequent TMA operations [1][2]. - Scope: Both release and acquire fences support various scopes (e.g.,.cta,.cluster,.gpu,.sys) to define the extent of the visibility [7][8]. - Implementation: In low-level development, these are often utilized alongside tensormap.cp_fenceproxy, which performs a fused copy and fence operation when updating TMA descriptors from shared memory to global memory [9][3][8]. - Usage Context: Failure to correctly use these fence sequences—or choosing an insufficient scope—can lead to stale data being consumed by the TMA engine, as the hardware may otherwise rely on cached or inconsistent descriptor states [6]. These PTX instructions were introduced starting in PTX ISA 8.3 [7][8]. Developers typically access them via specialized intrinsics provided in the CUDA C++ Core Compute Libraries (CCCL) or through inline assembly wrappers in libraries like CUTLASS [7][3][8].

Citations:


Add the tensormap release fence after updating the descriptors.

PublishTensorMaps writes CUtensorMap data through the generic proxy, while the TMA kernels acquire through the tensormap proxy. The kernel boundary does not establish this cross-proxy ordering, so the consumer can acquire a stale descriptor state. Add fence.proxy.tensormap::generic.release.gpu after the global-store sequence.

🤖 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 `@csrc/kda/flashkda_binding_common.cuh` around lines 464 - 494, Add a tensormap
release fence at the end of PublishTensorMaps after all destination writes,
using the fence.proxy.tensormap::generic.release.gpu instruction so descriptor
updates through the generic proxy are visible to TMA consumers. Keep the
existing launch and descriptor preparation flow unchanged.

Comment on lines +40 to +70
def _get_flash_kda_csrc_dir() -> Path:
"""Locate frozen FlashKDA sources in installed and source checkouts."""

installed = jit_env.FLASHINFER_CSRC_DIR / "kda"
if installed.exists():
return installed

checkout = Path(__file__).resolve().parents[2] / "csrc" / "kda"
if checkout.exists():
return checkout

raise FileNotFoundError(
"FlashKDA CUDA sources were not found. Checked:\n"
f" - {installed}\n"
f" - {checkout}"
)


def _get_flash_kda_include_dir() -> Path:
"""Locate FlashInfer headers in installed and source checkouts."""

if jit_env.FLASHINFER_INCLUDE_DIR.exists():
return jit_env.FLASHINFER_INCLUDE_DIR
checkout = Path(__file__).resolve().parents[2] / "include"
if checkout.exists():
return checkout
raise FileNotFoundError(
"FlashInfer headers were not found. Checked:\n"
f" - {jit_env.FLASHINFER_INCLUDE_DIR}\n"
f" - {checkout}"
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Sources are compiled directly from csrc_dir instead of being copied to FLASHINFER_GEN_SRC_DIR.

gen_flash_kda_module passes binding (a path inside _get_flash_kda_csrc_dir(), which can resolve to the installed, potentially read-only FLASHINFER_CSRC_DIR) straight into gen_jit_spec(sources=[binding], ...). Other JIT generators in this repository compute a unique URI, create a directory under FLASHINFER_GEN_SRC_DIR, copy the csrc sources there, and pass the copies as sources, even when no Jinja rendering is needed.

The docstring explains why each schedule compiles in its own translation unit, but it does not explain this departure from the standard copy-to-FLASHINFER_GEN_SRC_DIR step. Align with the established pattern, or add an explicit comment documenting why this frozen-source case intentionally reads sources in place.

♻️ Proposed alignment with the established JIT generator pattern
+import shutil
+
 `@functools.cache`
 def gen_flash_kda_module(
     variant: FlashKDAVariant, arch: FlashKDAArch = "sm100a"
 ) -> JitSpec:
     ...
     csrc_dir = _get_flash_kda_csrc_dir()
     include_dir = _get_flash_kda_include_dir()
     uri = get_flash_kda_uri(variant, arch)
-    binding = csrc_dir / f"flashkda_bf16_fused_{variant}_binding.cu"
-    if not binding.exists():
-        raise FileNotFoundError(f"FlashKDA binding source not found: {binding}")
+    binding_name = f"flashkda_bf16_fused_{variant}_binding.cu"
+    binding_src = csrc_dir / binding_name
+    if not binding_src.exists():
+        raise FileNotFoundError(f"FlashKDA binding source not found: {binding_src}")
+    gen_directory = jit_env.FLASHINFER_GEN_SRC_DIR / uri
+    gen_directory.mkdir(parents=True, exist_ok=True)
+    binding = gen_directory / binding_name
+    shutil.copy(binding_src, binding)

As per path instructions: "JIT generators should compute a unique URI, generate files under FLASHINFER_GEN_SRC_DIR, copy source files there, and return a JitSpec; do not write generated or compiled artifacts into package source directories."

Also applies to: 83-118

🤖 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 `@flashinfer/jit/flash_kda.py` around lines 40 - 70, Update
gen_flash_kda_module to follow the standard JIT generator flow: compute a unique
URI, create its directory under FLASHINFER_GEN_SRC_DIR, copy the sources from
_get_flash_kda_csrc_dir() into that directory, and pass the copied paths to
gen_jit_spec. Preserve the per-schedule translation-unit behavior and ensure no
generated or compiled artifacts are written to package source directories.

Source: Coding guidelines

@yyihuang

yyihuang commented Aug 2, 2026

Copy link
Copy Markdown
Collaborator Author

/bot run

@yyihuang

yyihuang commented Aug 2, 2026

Copy link
Copy Markdown
Collaborator Author

/bot run tests/kda

Compile the frozen M64 and M128 prefill kernels once as sm_100f for CUDA 12.9+ and route both CC 10.0 and CC 10.3 through the shared modules. Preserve the exact sm_100a CUDA 12.8 compatibility path.
@yyihuang
yyihuang force-pushed the codex/flashkda-prefill-gb300-export branch from aafaad1 to 8d3bbb2 Compare August 3, 2026 13:44
@yyihuang yyihuang changed the title feat(cake_kda): add optimized SM103a recurrent prefill backend feat(cake_kda): share recurrent prefill kernels across SM100 family Aug 3, 2026
@yyihuang

yyihuang commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator Author

/bot run tests/kda

@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: 1

🤖 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 `@flashinfer/kda_prefill.py`:
- Around line 167-172: Update _flash_kda_prefill_is_eligible to reject SM103a
devices when _is_cuda_version_at_least("12.9") is false, matching the
CUDA-version requirement in _select_flash_kda_prefill_target. Preserve
eligibility for other supported compute capabilities and ensure this combination
falls back instead of reaching _run_flash_kda_prefill and raising.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 5c8dd0cf-bd3b-41d7-8940-f74a82ae368b

📥 Commits

Reviewing files that changed from the base of the PR and between aafaad1 and 8d3bbb2.

📒 Files selected for processing (11)
  • benchmarks/bench_recurrent_kda_prefill.py
  • csrc/kda/flashkda_bf16_fused_m128_binding.cu
  • csrc/kda/flashkda_bf16_fused_m64_binding.cu
  • csrc/kda/flashkda_binding_common.cuh
  • docs/api/kda_prefill.rst
  • flashinfer/aot.py
  • flashinfer/jit/flash_kda.py
  • flashinfer/kda.py
  • flashinfer/kda_prefill.py
  • tests/jit/test_flash_kda_jit.py
  • tests/kda/test_recurrent_kda_prefill.py
🚧 Files skipped from review as they are similar to previous changes (6)
  • flashinfer/kda.py
  • docs/api/kda_prefill.rst
  • csrc/kda/flashkda_bf16_fused_m64_binding.cu
  • csrc/kda/flashkda_binding_common.cuh
  • csrc/kda/flashkda_bf16_fused_m128_binding.cu
  • tests/kda/test_recurrent_kda_prefill.py

Comment thread flashinfer/kda_prefill.py
Comment on lines 167 to 172
if (
not q.is_cuda
or get_compute_capability(q.device) != _FLASH_KDA_B200_COMPUTE_CAPABILITY
or get_compute_capability(q.device)
not in _FLASH_KDA_SUPPORTED_COMPUTE_CAPABILITIES
):
return False

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Eligibility check does not require CUDA 12.9 for SM103a, so unsupported calls crash instead of falling back.

_flash_kda_prefill_is_eligible only checks that compute capability is in _FLASH_KDA_SUPPORTED_COMPUTE_CAPABILITIES (line 170). It does not check the CUDA toolkit version. For compute capability (10, 3), _select_flash_kda_prefill_target requires CUDA 12.9 or newer (lines 542-546); otherwise it raises RuntimeError.

If a call is on an SM103a device with an older CUDA toolkit available for JIT (_is_cuda_version_at_least("12.9") is False), _flash_kda_prefill_is_eligible still returns True, so recurrent_kda routes into _run_flash_kda_prefill. That function then raises RuntimeError instead of falling back to the CuTe DSL backend. This breaks the PR's stated goal of retaining existing fallback behavior for this specific combination.

Add the same CUDA-version condition to the eligibility check so unsupported combinations fall back instead of raising.

🐛 Proposed fix to align eligibility with target-selection requirements
     if (
         not q.is_cuda
-        or get_compute_capability(q.device)
-        not in _FLASH_KDA_SUPPORTED_COMPUTE_CAPABILITIES
     ):
         return False
+    compute_capability = get_compute_capability(q.device)
+    if compute_capability not in _FLASH_KDA_SUPPORTED_COMPUTE_CAPABILITIES:
+        return False
+    if compute_capability == (10, 3) and not _is_cuda_version_at_least("12.9"):
+        # SM103a only has the sm_100f family target, which requires CUDA
+        # 12.9+. Without this check, an eligible-but-unbuildable call falls
+        # through to `_run_flash_kda_prefill`, which raises instead of
+        # falling back to the CuTe DSL backend.
+        return False

Also applies to: 530-550

🤖 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 `@flashinfer/kda_prefill.py` around lines 167 - 172, Update
_flash_kda_prefill_is_eligible to reject SM103a devices when
_is_cuda_version_at_least("12.9") is false, matching the CUDA-version
requirement in _select_flash_kda_prefill_target. Preserve eligibility for other
supported compute capabilities and ensure this combination falls back instead of
reaching _run_flash_kda_prefill and raising.

@yyihuang
yyihuang merged commit d7e390c into flashinfer-ai:main Aug 4, 2026
33 checks passed
yyihuang added a commit that referenced this pull request Aug 5, 2026
Follow-up to merged #4262 and #4313.

## What changed

- pad the beta-only TMA source to `round_up(H, 8)` instead of only
padding `H < 8`;
- teach the beta pack kernel and binding validation to use the dynamic
padded-head stride;
- retain the caller-visible `H`, state shape, launch grid, and frozen
M64/M128 CUDA bodies unchanged;
- add H=12 eager, packed, full-chunk-plus-tail, final-state, and CUDA
graph replay coverage.

The frozen kernels load beta in 8-head TMA boxes. For H=12, the original
BF16 row stride is 24 bytes and `cuTensorMapEncodeTiled` rejects it.
Padding the descriptor source to 16 heads gives a 32-byte row stride;
heads 12–15 are padding only and are never assigned CTAs.

This generalizes the fix to every positive head count that is not
divisible by eight, while aligned head counts retain the existing
zero-copy beta path.

## Correctness

Both runs used the public `flashinfer.recurrent_kda` facade and compared
BF16 output plus the complete final state against the PyTorch reference
with `atol=rtol=1e-2`.

| GPU | Compute capability | CUDA | Result |
| --- | --- | --- | --- |
| NVIDIA B200 | 10.0 | 12.9 | `62 passed, 0 skipped, 0 failed` |
| NVIDIA GB300 | 10.3 | 12.9 | `62 passed, 0 skipped, 0 failed` |

The test gate runs:

```text
tests/jit/test_flash_kda_jit.py
tests/kda/test_recurrent_kda_prefill.py
```

H=12 coverage includes fixed T=32, fixed T=33 (one full TMA chunk plus
the direct-load tail), packed sequence lengths `[32, 3]`, in-place
initial/final state, and CUDA graph replay after beta is changed. The
JIT contract test also verifies that the frozen generated M64/M128
bodies remain unchanged.

`pre-commit run --files <changed files>` passes.

## Performance

The H=12 path was benchmarked through the public
`flashinfer.recurrent_kda` facade with fallback forbidden. Speedup is
the official FlashKDA raw GPU span divided by this PR's public-API GPU
span. The baseline is the same official FlashKDA source used for #4262:
[`MoonshotAI/FlashKDA@d2ff19a`](MoonshotAI/FlashKDA@d2ff19a),
with CUTLASS `5c149f5`.

Measurements use strict CUPTI first-to-last correlated compute-kernel
span, cold L2, no CUDA Graph, and two independent 128-sample blocks in
symmetric ABCCBA order. The PR span includes both the beta pack and
frozen M128 recurrence kernels. All six benchmark shapes passed output
and complete-final-state correctness against the official peer with BF16
`atol=rtol=1e-2`.

| H=12 shape | SM100 / B200: PR / baseline | Speedup | SM103 / GB300: PR
/ baseline | Speedup |
| --- | ---: | ---: | ---: | ---: |
| packed `[512] x 32` | 136.159 / 240.239 us | **1.7644x** | 128.264 /
233.496 us | **1.8204x** |
| packed `[128] x 8` | 23.760 / 46.448 us | **1.9549x** | 25.712 /
52.904 us | **2.0576x** |
| fixed `[512]` | 46.184 / 76.383 us | **1.6539x** | 47.712 / 82.240 us
| **1.7237x** |
| fixed `[8192]` | 514.197 / 814.435 us | **1.5839x** | 487.161 /
779.426 us | **1.5999x** |
| mixed `[1300, 547, 2048, 963, 271, 3063]` | 208.647 / 351.550 us |
**1.6849x** | 198.216 / 340.913 us | **1.7199x** |
| uniform `[1024] x 8` | 82.080 / 162.703 us | **1.9823x** | 78.440 /
162.505 us | **2.0717x** |
| **Six-shape geometric mean** | | **1.7645x** | | **1.8238x** |

| Comparison | Result |
| --- | --- |
| FlashInfer upstream main at H=12 | `unsupported / N/A` |

Upstream main fails before kernel launch with `cuTensorMapEncodeTiled
failed for beta_tma with CUresult=1`, so there is no valid upstream H=12
timing or speedup claim. This PR leaves the frozen compute kernel
unchanged; it adds the required beta packing only for non-8-aligned head
counts.

Related to #4254.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- **Bug Fixes**
- Improved beta padding for head counts that are not divisible by eight.
- Ensured packed inputs correctly handle larger, non-aligned head
counts.
  - Added validation for padded storage requirements.

- **Documentation**
  - Clarified beta padding behavior and public tensor shapes.

- **Tests**
- Expanded coverage for 12-head inputs, varied sequence lengths, chunk
boundaries, packed inputs, and CUDA graph updates.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Co-authored-by: Yingyi Huang <averyh@nvidia.com>
@yyihuang
yyihuang deleted the codex/flashkda-prefill-gb300-export branch August 6, 2026 01:06
@yyihuang
yyihuang restored the codex/flashkda-prefill-gb300-export branch August 6, 2026 01:23
@flashinfer-ai flashinfer-ai deleted a comment from flashinfer-bot Aug 18, 2026
@flashinfer-ai flashinfer-ai deleted a comment from flashinfer-bot Aug 18, 2026
@flashinfer-ai flashinfer-ai deleted a comment from flashinfer-bot Aug 18, 2026
@flashinfer-ai flashinfer-ai deleted a comment from flashinfer-bot Aug 18, 2026
@flashinfer-ai flashinfer-ai deleted a comment from flashinfer-bot Aug 18, 2026
@flashinfer-ai flashinfer-ai deleted a comment from flashinfer-bot Aug 18, 2026
@flashinfer-ai flashinfer-ai deleted a comment from flashinfer-bot Aug 18, 2026
@kahyunnam kahyunnam added the op: linear attention KDA, mamba, GDN, etc. review filtering. label Sep 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

op: linear attention KDA, mamba, GDN, etc. review filtering.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants