Skip to content

perf(kda): optimize indexed recurrent decode dispatch and paging - #4182

Closed
djmmoss wants to merge 13 commits into
flashinfer-ai:mainfrom
djmmoss:dmoss/recurrent-kda-dispatch-heuristic
Closed

djmmoss wants to merge 13 commits into
flashinfer-ai:mainfrom
djmmoss:dmoss/recurrent-kda-dispatch-heuristic

Conversation

@djmmoss

@djmmoss djmmoss commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Page indexed recurrent state directly inside both CuTe DSL kernel architectures,
    removing the full-state gather and scatter around standard decode. Dense calls
    reuse the same path through a cached identity mapping.
  • Add a table-free SM100/SM103 dispatch heuristic that selects the one-warp or grouped
    architecture and its row/reduction or key/value split schedule from the workload.
  • Cover arbitrary state slots, padded state strides, CUDA Graph replay, and dispatch
    boundaries.

B200 performance

BF16 cold-L2 CUPTI timing with CUDA Graphs disabled; each row is the median of five
independent rounds. This compares upstream pre-PR recurrent_kda at 76c583655
against this PR at 6039adef. Both columns use the CuTe DSL backend.

Matrix Dimensions Cases Pre-PR geomean (us) This PR geomean (us) Speedup
Cake PR #4279 D128; T=1–6; H=16; HV=16/32; N=1–128 30 31.281 29.875 1.047x
Kimi K3 PR #4182 D128; T=1; H=HV=12/24/48/96; N=1–256 17 182.230 13.526 13.472x
Legacy PR #2572 D64/128; T=1/4; H=HV=8/16/32; N=1–128 48 24.981 22.192 1.126x
Overall All matrices 95 38.272 22.310 1.715x

The current implementation passed output and full-state correctness across all 95
shapes.

djmmoss added 5 commits July 27, 2026 17:21
Use one-warp for D128 single-token decode below 1,920 sequence-heads and
grouped-CTA at/above that cutoff, based on Kimi K3 B300 sweeps. Retain the
existing D64 minimum-grid policy and add backend-selection regression tests.
Use the same T=1 selection rule for both head dims: one-warp below a
sequence-head cutoff, grouped-CTA at/above it. Set D128 to 1,920 and D64 to
7,680 from B300 crossover sweeps, replacing the old inverted D64 minimum-grid
policy. Extend backend-selection tests for both cutoffs.
Replace per-dim constants and helper lookup with GROUPED_MIN_SEQUENCE_HEADS
and use NUM_TOKENS consistently at both dispatch call sites. Table-drive the
backend-selection regression tests.
Cover B∈{1,4,32} at H=96/D=128 with in-kernel gated decode and lower_bound=-5.0
using the existing vLLM-style cu_seqlens correctness test.
Exercise the vLLM logits path in test_vllm_decode for K3-shaped decode
cases so beta sigmoid stays in-kernel rather than only in spec-decode tests.
@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 Jul 27, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

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

Use the following commands to manage reviews:

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

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Recurrent KDA now resolves explicit or identity state indices for dense and packed decoding. Indexed execution updates the caller-owned state pool and returns compact standard-decode state. Dispatch adds architecture- and workload-specific kernel schedules. Tests and trace callers cover the new contracts.

Changes

Recurrent KDA state indexing and dispatch

Layer / File(s) Summary
State-index API contract
flashinfer/kda_decode.py, flashinfer/kda_kernels/recurrent_kda.py, flashinfer/trace/templates/kda.py
Documents standard and packed state-index shapes, identity behavior, direct state-pool updates, and batch-sized traced final state.
Indexed recurrence and state-pool execution
flashinfer/kda_kernels/recurrent_kda.py
Derives state slots and token activity from resolved indices, preserves the caller-owned state pool, and returns indexed standard-decode state.
Architecture-specific dispatch and scheduling
flashinfer/kda_kernels/recurrent_kda.py
Adds head-dimension and SM100 one-warp rules, SM100 tile and reduction selection, grouped schedules, and schedule-aware compilation keys.
Validation and trace integration
tests/kda/test_recurrent_kda.py, tests/trace/*
Adds coverage for indexed layouts, identity parity, CUDA graphs, beta representations, dispatch boundaries, schedules, empty rows, and trace state indices.

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

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant recurrent_kda
  participant run_recurrent_kda
  participant recurrent_kda_decode_kernel
  participant StatePool
  Caller->>recurrent_kda: provide decode inputs and optional ssm_state_indices
  recurrent_kda->>run_recurrent_kda: pass state indices and decode inputs
  run_recurrent_kda->>run_recurrent_kda: select one-warp or grouped schedule
  run_recurrent_kda->>recurrent_kda_decode_kernel: launch with resolved state indices
  recurrent_kda_decode_kernel->>StatePool: update indexed state slots
  run_recurrent_kda->>StatePool: return full pool or state[ssi]
Loading

Possibly related PRs

Suggested labels: run-ci, op: attention

Suggested reviewers: yzh119, kahyunnam, aleozlx

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly summarizes the main changes to indexed recurrent KDA decode dispatch and state paging.
Description check ✅ Passed The description provides a clear change summary, benchmark results, correctness claims, and test coverage.
✨ 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.

@djmmoss
djmmoss enabled auto-merge (squash) July 28, 2026 16:45
@djmmoss

djmmoss commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator Author

/bot run tests/kda

@flashinfer-bot

Copy link
Copy Markdown
Collaborator

GitLab MR !1063 has been created, and the CI pipeline #59980611 is currently running. I'll report back once the pipeline job completes.

@flashinfer-bot

Copy link
Copy Markdown
Collaborator

[SUCCESS] Pipeline #59980611: 18/18 executed test jobs passed

@djmmoss djmmoss changed the title perf(kda): fix one-warp vs grouped recurrent decode dispatch perf(kda): optimize indexed recurrent decode dispatch and paging Jul 30, 2026

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
flashinfer/kda_kernels/recurrent_kda.py (1)

1409-1449: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

No validation that standard-decode ssm_state_indices are non-negative before indexing an uninitialized out_buf.

Docs reserve -1 for packed/spec rows only (not the plain [B] standard-decode format), but nothing here enforces that. If a negative index slips through: out_buf is torch.empty(...)-allocated (line 1449, never zeroed in this branch), and the one-warp kernel's is_active gate (lines 433-435) skips writing that row's output entirely — leaving uninitialized memory in the result. The grouped-CTA kernel's equivalent path (_grouped_kda_kernel, lines 689-692, same file) explicitly zero-fills the analogous case, so which failure mode occurs is dependent on the _use_one_warp cutoff this PR changes.

🛡️ Suggested guard
         grid_seqs = B
         sequence_heads = grid_seqs * HV
         use_one_warp = _use_one_warp(K, NUM_TOKENS, sequence_heads)
         cu_seqlens_i32 = None
+        if (ssm_state_indices < 0).any():
+            raise ValueError(
+                "ssm_state_indices must be non-negative for standard decode "
+                "(no cu_seqlens); -1 is reserved for padded packed rows"
+            )
🤖 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_kernels/recurrent_kda.py` around lines 1409 - 1449, Validate
standard-decode ssm_state_indices before allocating or using state/output
buffers: reject any negative entry because this [B]-shaped path does not permit
the reserved -1 sentinel. Add the guard in the non-cu_seqlens branch alongside
the existing shape validation, using the existing ssm_state_indices and B
symbols, and raise a clear ValueError before kernel execution.
🧹 Nitpick comments (1)
flashinfer/kda_kernels/recurrent_kda.py (1)

1240-1244: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Docstring calls the returned indexed state a "view"; it's actually a gather copy.

state[ssi] (fancy/advanced indexing, see line 1643) allocates a new tensor rather than aliasing state's storage. Calling it a "view" here could mislead callers into assuming in-place edits to the returned tensor propagate back into the caller-owned pool, which they do not.

📝 Suggested wording fix
-            - state: Updated state if ``output_final_state=True``, else
-              ``None``. Indexed standard decode returns the compact
-              ``[B, HV, V, K]`` view while updating the full caller-owned pool
-              in place. For batched spec decode without ``cu_seqlens``, this is
-              the packed checkpoint state pool used by the shim.
+            - state: Updated state if ``output_final_state=True``, else
+              ``None``. Indexed standard decode returns a compact
+              ``[B, HV, V, K]`` gather (a new tensor, not a view) while the
+              full caller-owned pool is updated in place. For batched spec
+              decode without ``cu_seqlens``, this is the packed checkpoint
+              state pool used by the shim.
🤖 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_kernels/recurrent_kda.py` around lines 1240 - 1244, Update the
return-state documentation for the indexed standard decode in the relevant
docstring to describe the compact [B, HV, V, K] result as a gathered copy rather
than a view, and clarify that edits to it do not modify the caller-owned state
pool. Leave the behavior and other state descriptions unchanged.
🤖 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.

Outside diff comments:
In `@flashinfer/kda_kernels/recurrent_kda.py`:
- Around line 1409-1449: Validate standard-decode ssm_state_indices before
allocating or using state/output buffers: reject any negative entry because this
[B]-shaped path does not permit the reserved -1 sentinel. Add the guard in the
non-cu_seqlens branch alongside the existing shape validation, using the
existing ssm_state_indices and B symbols, and raise a clear ValueError before
kernel execution.

---

Nitpick comments:
In `@flashinfer/kda_kernels/recurrent_kda.py`:
- Around line 1240-1244: Update the return-state documentation for the indexed
standard decode in the relevant docstring to describe the compact [B, HV, V, K]
result as a gathered copy rather than a view, and clarify that edits to it do
not modify the caller-owned state pool. Leave the behavior and other state
descriptions unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 17137426-641e-40d1-9dc5-8a75987642f9

📥 Commits

Reviewing files that changed from the base of the PR and between 72f159e and ca5a8e2.

📒 Files selected for processing (6)
  • flashinfer/kda_decode.py
  • flashinfer/kda_kernels/recurrent_kda.py
  • flashinfer/trace/templates/kda.py
  • tests/kda/test_recurrent_kda.py
  • tests/trace/example.py
  • tests/trace/test_fi_trace.py

@djmmoss

djmmoss commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator Author

/bot run tests/kda

@flashinfer-bot

Copy link
Copy Markdown
Collaborator

GitLab MR !1063 has been updated with latest changes, and the CI pipeline #60413256 is currently running. I'll report back once the pipeline job completes.

@flashinfer-bot

Copy link
Copy Markdown
Collaborator

[SUCCESS] Pipeline #60413256: 18/18 executed test jobs passed

@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_kernels/recurrent_kda.py`:
- Around line 31-33: Update the dispatch documentation near _use_one_warp to
state that multi-token workloads generally use grouped-CTA, except SM100
workloads where num_tokens == 3 with gating or num_tokens == 4 without gating
and without GQA use the one-warp kernel.
🪄 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: 66165540-5493-4921-b175-4fd02839b7ad

📥 Commits

Reviewing files that changed from the base of the PR and between 4d4493d and 6039ade.

📒 Files selected for processing (5)
  • flashinfer/kda_decode.py
  • flashinfer/kda_kernels/recurrent_kda.py
  • flashinfer/trace/templates/kda.py
  • tests/kda/test_recurrent_kda.py
  • tests/trace/example.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • tests/trace/example.py
  • flashinfer/trace/templates/kda.py

Comment thread flashinfer/kda_kernels/recurrent_kda.py Outdated
@djmmoss

djmmoss commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator Author

/bot run tests/kda

@flashinfer-bot

Copy link
Copy Markdown
Collaborator

GitLab MR !1063 has been updated with latest changes, and the CI pipeline #60944357 is currently running. I'll report back once the pipeline job completes.

@flashinfer-bot

Copy link
Copy Markdown
Collaborator

[SUCCESS] Pipeline #60944357: 18/18 executed test jobs passed

Comment thread flashinfer/kda_kernels/recurrent_kda.py Outdated
elif ssm_state_indices is not None:
state = initial_state[ssm_state_indices].contiguous()
copy_back_indices = ssm_state_indices
max_idx = int(ssi.max().item()) + 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.

This will leave a D2H sync everytime .item() is called + can break cuda graph capture. You can add

if ssm_state_indices is None: 
   state = torch.zeros(B, HV, V, K, device=device, dtype=torch.bfloat16)
else:
   max_idx = ...

) -> bool:
"""Select the measured kernel architecture for the active GPU."""
if compute_capability not in TUNED_DISPATCH_COMPUTE_CAPABILITIES:
return num_tokens == 1 and sequence_heads < GROUPED_MIN_SEQUENCE_HEADS[head_dim]

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.

I believe with this change it is now:

sequence_heads (D=128, T=1) pre-PR PR fallback
< 128 grouped one-warp
128 … 1919 one-warp one-warp
>= 1920 one-warp grouped

Is this expected?

Signed-off-by: Duncan Moss <djm.moss@gmail.com>
Signed-off-by: Duncan Moss <djm.moss@gmail.com>
@djmmoss
djmmoss requested a review from Anerudhan as a code owner September 10, 2026 02:31
@flashinfer-bot

Copy link
Copy Markdown
Collaborator

[SUCCESS] Pipeline #60944357: 18/18 executed test jobs passed

@djmmoss djmmoss closed this Sep 14, 2026
auto-merge was automatically disabled September 14, 2026 15:37

Pull request was closed

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.

4 participants