Skip to content

feat(cake_kda): add optimized B200 recurrent prefill backend - #1

Closed
yyihuang wants to merge 6 commits into
mainfrom
codex/flashkda-prefill-export
Closed

yyihuang wants to merge 6 commits into
mainfrom
codex/flashkda-prefill-export

Conversation

@yyihuang

@yyihuang yyihuang commented Jul 29, 2026 •

Copy link
Copy Markdown
Owner

📌 Description

This PR adds an optimized SM100a BF16 recurrent-KDA prefill backend and
dispatches to it through the existing recurrent_kda API.

The new backend coexists with the current CuTe-DSL implementation. Decode
(T=1), speculative decode, GQA, state-pool/checkpoint features, unsupported
layouts and dtypes, and non-B200 devices continue to use the existing backend
unchanged.

Dispatch contract

The optimized path is selected only when all of the following hold:

  • the device is exactly compute capability 10.0 (B200 / SM100a);
  • the call is ordinary multi-token prefill: fixed T > 1, or packed input
    with more tokens than sequences;
  • Q, K, V, and G are contiguous BF16 [B, T, H, 128] tensors with a shared
    head count, and beta is contiguous BF16 [B, T, H];
  • A_log is contiguous FP32 [H], while dt_bias is contiguous FP32
    [H, 128] or [H * 128];
  • use_qk_l2norm_in_kernel=True, use_gate_in_kernel=True,
    beta_is_logit=True, and lower_bound is finite and negative;
  • speculative decode, GQA, state indices, committed-state sources, and
    accepted-token/checkpoint features are disabled.

Packed input uses B=1 and contiguous CUDA int32/int64 cu_seqlens. The
binding consumes int64 offsets; CUDA graph capture therefore requires callers
to supply int64 offsets directly. An optional contiguous CUDA int32
seq_order can order packed sequences for better tail utilization.

Calls that do not exactly match this contract fall back to the existing
CuTe-DSL path.

Physical schedules and true in-place state update

  • M64 is used only for fixed-layout B=1, H=64.
  • M128 handles every other eligible fixed-layout call and all eligible packed
    calls.
  • State layout remains BF16 [N, H, V, K] with V=K=128.
  • A supplied initial_state is passed as both the initial- and final-state
    kernel pointer. The kernel updates that allocation directly; there is no
    state scratch allocation and no post-kernel copy_/memcpy.
  • M128 assigns one CTA to each (sequence, head). M64 assigns two CTAs whose
    64-row value partitions are disjoint. Each CTA loads all initial-state rows
    it owns before storing those same final-state rows, making exact pointer
    aliasing safe.
  • The binding accepts either disjoint state buffers or an exact full-range
    alias and rejects partial overlap.
  • When initial_state=None, final-state storage is created only if
    output_final_state=True.

The generated source emits __restrict__ for every pointer and has no
per-argument alias escape hatch, so the removal of __restrict__ from only the
two state parameters is an explicitly delimited FlashInfer ABI integration
patch. The frozen-source test restores those two qualifiers, removes the
existing tensor-map acquire integration prologue, and verifies the remaining
generated source against its normalized SHA256.

CUDA graph and tensor-map safety

RecurrentKDAPrefillWorkspace owns optional final-state scratch for calls
without an initial state, beta padding, and separate 768-byte M64/M128
tensor-map descriptor blocks. It binds to one stream, is warmed eagerly
against the exact tensor signature, and is used by one captured
recurrent_kda invocation. A workspace that has participated in capture is
rejected by later Python calls; graph replay remains valid because it does not
re-enter Python.

Tensor maps are prepared outside capture and published to stable global
storage on the caller stream. Every consumer CTA executes
fence.proxy.tensormap::generic.acquire.gpu for all six maps, followed by a
CTA barrier, before any TMA instruction can consume them.

Other integration

  • Adds JIT and AOT registration for separate M64 and M128 translation units.
  • Updates tracing and API documentation for seq_order and
    prefill_workspace.
  • Adds correctness, routing/fallback, ABI, stream, overlap, workspace, and
    CUDA graph tests.
  • Adds a CUPTI cold-L2 benchmark for the six fixed/packed contract shapes,
    including a commit- and binary-verified MoonshotAI/FlashKDA peer.

🧪 Validation

Final validation was run on pushed head
f6c3b0c486787e64f76b2be91ddb239ea3ba66ce, on an NVIDIA B200
(compute capability 10.0), PyTorch 2.13.0.dev20260503+cu132, CUDA 13.2,
and Python 3.13.13.

Gate Result
Frozen-source/JIT specification tests 8 passed
Full targeted recurrent-KDA prefill suite, including fixed/packed correctness, M64/M128, non-default streams, and CUDA graph capture/replay 28 passed
Focused M64/M128 compute-sanitizer --tool memcheck 5 passed; ERROR SUMMARY: 0 errors
Focused M64/M128 compute-sanitizer --tool synccheck 5 passed; ERROR SUMMARY: 0 errors
Pre-commit hooks on the change Passed

The full repository test suite was not run.

Fresh B200 CUPTI benchmark

The candidate is invoked only through the public
flashinfer.kda_decode.recurrent_kda entry point and performs its state update
in place inside the kernel. Its timed region contains no state memcpy.

The comparison uses six fixed/packed shapes with deterministic seeds, matching
tensor distributions, BF16 state, and scale=1/sqrt(128). Preinitialized
rotating state buffers ensure each timed candidate invocation receives the
same initial state. Allocation, metadata creation, sequence ordering,
state-pool reset, and JIT/cache warmup are outside the measured region.

The peer is MoonshotAI/FlashKDA._fwd_raw, from source commit
d2ff19a6
and CUTLASS
5c149f52.
The loaded extension SHA256 is
997c3a1d1338f8bf9dba3c1a01386b1b74448214c294d64409454cc11141c04c.
The benchmark records the source revision and independently computed extension
digest.

Timing uses CUPTI activity tracing, cold-L2 flushing, no CUDA graph, 20 ms
warmup, and 100 ms measurement per block. Each value is the median of two
independent block medians; the reported scopes occupy symmetric
PR/raw/.../raw/PR positions. Speedup is FlashKDA / this PR.

Case Layout / sequence lengths Variant This PR public in-place API (ms) FlashKDA raw peer (ms) Speedup
H=96 fixed [8192] M128 0.506095 1.085277 2.1444x
H=96 mixed [1300, 547, 2048, 963, 271, 3063] M128 0.391135 0.894702 2.2875x
H=96 uniform [1024] × 8 M128 0.434992 0.735535 1.6909x
H=64 fixed [8192] M64 0.469711 0.992638 2.1133x
H=64 mixed [1300, 547, 2048, 963, 271, 3063] M128 0.269903 0.676574 2.5067x
H=64 uniform [1024] × 8 M128 0.292567 0.495871 1.6949x
Geometric mean 2.0512x

All six output/state comparisons against FlashKDA passed at
atol=rtol=1e-2; the maximum observed absolute errors were 0.0009765625
for output and 0.015625 for state. The benchmark JSON SHA256 is
c34b27f9e8fb4e4dfbc500e6976e63668fcd0bff2bb00086cd516de59491f259.

Upstream recurrent-KDA comparison

Pinned upstream main does not implement ordinary multi-token recurrent-KDA
prefill through its public API: fixed T != 1 is rejected, and packed standard
decode processes one token per sequence. Speculative decode has different
checkpoint/state semantics and is not an equivalent workload. The upstream
prefill comparison is therefore unsupported / N/A, rather than timing a
different operation.

🚀 Pull Request Checklist

  • Tests were added or updated.
  • Targeted tests and pre-commit hooks pass.
  • Full repository test suite was not run.

Reviewer Notes

The main review surfaces are:

  1. strict eligibility and unchanged fallback behavior;
  2. exact-alias state ABI and direct in-kernel update;
  3. workspace lifetime, stream binding, and CUDA graph rules;
  4. tensor-map publication/acquire ordering;
  5. the two explicitly delimited frozen-CUDA integration patches and their
    integrity normalization.

averyhNV added 2 commits July 29, 2026 15:28
Dispatch the exact BF16 multi-token recurrent KDA contract to exported SM100a M64/M128 kernels while retaining the existing CuTe backend for decode, speculative decode, GQA, and unsupported shapes.

Add graph-safe caller workspaces, JIT/AOT integration, frozen-source integrity checks, correctness coverage, trace updates, documentation, and the six-shape CUPTI benchmark.

Signed-off-by: Yingyi Huang <averyh@nvidia.com>
Signed-off-by: Yingyi Huang <averyh@nvidia.com>
@yyihuang yyihuang changed the title feat(kda): add optimized B200 recurrent prefill backend feat(cake_kda): add optimized B200 recurrent prefill backend Jul 29, 2026
@yyihuang
yyihuang requested a review from yzh119 as a code owner July 30, 2026 02:36
@yyihuang yyihuang closed this Jul 30, 2026
yyihuang pushed a commit that referenced this pull request Oct 19, 2026
<!-- .github/pull_request_template.md -->

## 📌 Description

<!-- What does this PR do? Briefly describe the changes and why they’re
needed. -->

This achieved about 10x less MoE time per decode step than FlashInfer
main for dsv4.1-flash workload.

## Commits

| # | commit | what it does | gate |
|---|---|---|---|
| 1 | fix(moe_ep/sm107): phase-interleave FC12 scheduler keeps
cluster-uniform state (EP deadlock) | Stops two CTAs of one cluster from
disagreeing on `fc1_exhausted`. Without it, the shared-expert overlap
with the phase-interleave schedule hung 3 of 3 vLLM boots during
adaptive-verification profiling. | always (phase-interleave schedule) |
| 2 | perf(moe_ep/sm107): an SM budget launches the MegaMoE as a uniform
cluster grid | `max_sm_count` now builds a workspace instead of raising,
so a side-stream shared-expert MLP can run next to the megakernel. |
only when `max_sm_count` is set |
| 5 | feat(quantization): nvfp4_quantize_cute_dsl can write into
caller-provided buffers (out=) | Adds an `out=` argument; the quantizer
itself is unchanged. | caller opt-in |
| 7 | perf(moe_ep/sm107): bind the per-call NVFP4 epilogue scalars as
launch arguments | Binds fc1/fc2 alpha and the fc1 norm constant as
launch arguments, removing 3 device copies per layer. | always (NVFP4) |
| 8 | perf(moe_ep/sm107): fold the caller's padding mask into the
one-launch route staging | Adds `MoEEpTensors.route_padding_mask`, so
callers drop their own masking kernel. | only when the caller sets the
field |

14 files, +726 / -56, on `main` 2895577.

The steps are cumulative from `main` (top) to this series plus the
companion vLLM change (bottom). Rows marked
*[vLLM]* belong to the companion vLLM change and are shown because the
steps were measured together. Columns are
tokens per rank @ workspace capacity.


| step | 1260 @ 3072 (µs) | 2046 @ 2048 (µs) | 2304 @ 3072 (µs) | 4608 @
6144 (µs) | MoE per step (ms) | output vs previous step |
|---|---|---|---|---|---|---|
| `main` (start) | 1892.8 | 2782.5 | 3098.0 | 5992.8 | 94.40 | – |
| #1 scheduler fix + #2 SM-budget launch (+ *[vLLM]* SM107 enablement) |
+0.2 | -0.2 | +1.0 | +0.1 | -0.001 | bit-identical |
| #7 bound epilogue scalars | -6.7 | -7.0 | -8.7 | -4.2 | -0.274 |
bit-identical |
| #8 padding-mask fold (+ *[vLLM]* hand-off), no padding rows | -0.2 |
-0.2 | 0.0 | -0.6 | -0.007 | bit-identical |
| **this series + vLLM change (end)** | **232.5** | **257.2** |
**265.8** | **438.4** | **9.82** | |




## 🔍 Related Issues

<!-- Link any related issues here -->

## 🚀 Pull Request Checklist

Thank you for contributing to FlashInfer! Before we review your pull
request, please make sure the following items are complete.

### ✅ Pre-commit Checks

- [ ] I have installed `pre-commit` by running `pip install pre-commit`
(or used your preferred method).
- [ ] I have installed the hooks with `pre-commit install`.
- [ ] I have run the hooks manually with `pre-commit run --all-files`
and fixed any reported issues.

> If you are unsure about how to set up `pre-commit`, see [the
pre-commit documentation](https://pre-commit.com/).

## 🧪 Tests

- [ ] Tests have been added or updated as needed.
- [ ] All tests are passing (`unittest`, etc.).

## 🔬 Experimental Track

<!-- Only for PRs submitted under the experimental policy
(CONTRIBUTING.md → "Experimental APIs and Backends").
     Leave this section untouched for normal PRs. -->

- [ ] This PR is **experimental**: it adds or changes code under
`flashinfer/experimental/` and/or an `@flashinfer_experimental_api`.
Tracking issue: #
- [ ] The tracking issue names an owner, the reason for the experimental
path, and a graduation plan with a target release.
- [ ] Core changes are limited to a thin entry point (signature, shared
validation, feature-gate check, backend selection, handoff).
- [ ] Tests live in `tests/experimental/` and were validated on the
intended hardware; a runnable example is included.
- [ ] Nothing is registered in `flashinfer/aot.py`, and no experimental
backend is reachable from `backend="auto"` without
`FLASHINFER_ALLOW_EXPERIMENTAL_AUTO_BACKENDS=1`. (Calling an
`@flashinfer_experimental_api` or naming a backend explicitly is itself
the opt-in and needs no environment variable.)
- [ ] **Test scope declared below.** The experimental CI lane runs
exactly these targets, so keep them as narrow as the change allows.

<!-- Required for experimental PRs. Replace the commented lines below
with your targets.
Do not delete the fence or change its `experimental-tests` tag — the
experimental-track
watcher reads it verbatim to decide which targets to ask CI for. -->

```experimental-tests
# One target per line: a directory or a file. (A pytest ::selector is not
# supported -- the sharding runner cannot consume one.) Must be under
# tests/experimental/ and must exist. Delete these comment lines and add yours, e.g.
#
#   tests/experimental/test_my_backend.py
#   tests/experimental/my_backend/
#
# Declaring the whole tree (tests/experimental/) is allowed but means every
# experimental PR pays for every other feature's tests, in every matrix cell.
```

## Reviewer Notes

<!-- Optional: anything you'd like reviewers to focus on, concerns, etc.
-->


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

* **New Features**
* Added faster staging for supported SM107 MoE routes and NVFP4 inputs,
with fallback handling for unsupported inputs.
* Added an optimized BF16 inference reduction path that masks invalid
routes without first clearing their combine slots.
* NVFP4 quantization can now write into caller-provided output buffers.
* Per-call scalar tensors can be passed directly to supported launches.

* **Bug Fixes**
* Improved reliability of SM107 work scheduling to prevent CTAs from
disagreeing about which work phase is active.
  * Preserved route validation for inputs that use fallback staging.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
yyihuang pushed a commit that referenced this pull request Nov 5, 2026
…ed set

A forward that keeps no saved set (save_for_backward=False under
boundary_mode="saved") binds the arena r at #1 of the fused form when
the in-kernel Q / K norm is on, and binds c1_recv / stage_m from the
workspace instead of fresh tensors.  gdn_det_workspace_layout_kcp takes
save_for_backward (default True: the saved-set figures are unchanged)
and reports fwd.qk_rstd; False requires backward=False.  An overridden
P now sizes the staging planes from the plan of that P (the plan the
host allocates for), not from the default plan's segment count.  A test
checks the peak of a caller-held workspace against the layout at
tolerance 0 for both forms at P = 1 and P = 2.
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.

3 participants