flashinfer.kda_prefill¶
Optimized recurrent Kimi Delta Attention (KDA) prefill support. The
flashinfer.kda.recurrent_kda() facade exposes frozen Cake and source-level
CuTe DSL implementations for a strict ordinary multi-token prefill subset.
|
Caller-owned storage required for recurrent-KDA CUDA graph capture. |
|
Plan-and-run wrapper for packed recurrent-KDA prefill. |
Backend selection¶
backend="auto" selects the source-level CuTe DSL backend for eligible
ordinary multi-token prefill and falls back to the frozen Cake backend for
unsupported contracts. Decode retains the existing KDA decode routing.
backend="cake" and backend="cute-dsl" select a backend strictly and
raise when its contract is unsupported.
For multi-token prefill, backend="cute-dsl" selects a BT=16 CuTe DSL kernel.
It supports contiguous BF16 Q, K, V, G, and beta with one shared head count and
head dimension 128, the in-kernel lower-bound gate, fixed or packed-varlen
layout, BF16 recurrent state, explicit seq_order, and the same checkpoint
contract as Cake. checkpoint_cu_starts must always be int64. Packed
cu_seqlens must be int64 during CUDA graph capture. The CuTe DSL schedule
is non-persistent.
Optimized Blackwell prefill subset¶
The strict Cake backend is available only when every condition below holds:
the device has compute capability 10.0 (SM100a; B200/GB200) or 10.3 (SM103a; B300/GB300);
input is ordinary multi-token prefill: fixed
T > 1, or packed input whose total token count is greater than its number of sequences;Q, K, V, and G are contiguous BF16
[B,T,H,128]tensors with one shared head count; beta is BF16[B,T,H]with unit head stride and non-overlapping token rows (an aligned slice of a fused projection is accepted directly);A_logis contiguous FP32[H]anddt_biasis contiguous FP32[H,128]or flattened[H*128];use_qk_l2norm_in_kernel=True,use_gate_in_kernel=True,beta_is_logit=True, andlower_boundis a finite negative value;speculative decode, GQA, committed-state sources, and accepted-token features are not enabled. Plain int32
ssm_state_indicesand native prefill checkpoints are supported by direct M128.
T=1 decode and speculative decode are not handled by either prefill backend.
CUDA 12.8 predates the family target, so CC 10.0 uses legacy exact
sm_100a modules. With CUDA 12.9 or newer, JIT and AOT compile one
sm_100f module per schedule for both CC 10.0 and CC 10.3. Cache keys also
include the frozen module identity so an older schedule cannot satisfy a
refreshed request. Runtime routing remains device-specific: persistent M128
is restricted to measured 148/152-SM CC 10.0 devices, while CC 10.3 uses the
direct schedules. On either capability, fixed-layout calls with at most eight
total sequence/head tasks, at most eight heads, and at least 2,048 tokens per
sequence use the small-BH owner/helper schedule when all eight CTAs per task
can reside concurrently. Calls outside that measured region continue through
the existing direct or fallback route; it is not a public-input allowlist.
The frozen H12 N16 schedule’s residual recurrence rounds four intermediates through BF16: the state/K prediction, the V-minus-prediction delta, sigmoid beta, and the post-beta update carrier. The final-state contraction starts from a zero accumulator; the old BF16 state is multiplied by the total decay and explicitly added to that product in FP32 before the 16-token chunk boundary rounds back to BF16. The N16 prepare carrier also matches the source-visible BF16 arithmetic graph: normalized Q/K, positive and inverse prefix decay, and every chained Qd/Kd/Ki/Kr multiplication round through BF16 at their respective boundaries. The N32 schedule comes from the same generated export.
Fixed input omits cu_seqlens. Packed input has B=1 and accepts a
contiguous CUDA int32 or int64 cu_seqlens. The frozen binding consumes
int64 offsets; pass int64 directly for CUDA graph capture to avoid an
in-capture conversion allocation. Offset values are a caller contract:
cu_seqlens[0] == 0, entries are non-decreasing, and
cu_seqlens[-1] == total_tokens. CuTe DSL accepts equal adjacent offsets for
zero-length sequences; Cake requires every sequence to be non-empty.
FlashInfer does not synchronize the device to inspect these values; invalid
offsets may cause out-of-bounds device access.
Packed scheduling¶
Packed prefill optionally accepts seq_order, a contiguous CUDA int32
tensor with one entry per sequence. It is a caller contract that this tensor
is a permutation of [0, N). Ordering sequences by decreasing length
reduces the final partial wave. FlashInfer validates dtype, device, rank, and
size without synchronizing the device to inspect permutation values.
For Cake, omitting seq_order uses its cached eager scheduling metadata. H12
selects the dedicated M128 schedule with a 16-token recurrence chunk for both
fixed and packed layouts. Fixed B=1,H=64 selects the two-CTA M64
value-split kernel; the fixed small-BH region described above selects its
eight-CTA owner/helper schedule; all remaining eligible inputs select the
general 32-token M128 schedule.
For eager packed CuTe DSL engine calls, omitting seq_order builds and
caches a stable decreasing-length order on the host. CuTe DSL decomp retains
the original sequence order because its CTA grid fits in one wave.
flashinfer.RecurrentKDAPrefillWrapper provides the explicit planned path
needed for packed engine CUDA Graph capture: plan builds the order and the
decomp cu_chunks prefix, then run consumes fixed-address buffers. The
decomp prep kernel binary-searches this compact prefix instead of carrying a
dense chunk-to-sequence tensor. The number of sequences, total tokens, and
total BT=16 chunks are fixed by the first plan so the metadata and launch
geometry remain valid across CUDA Graph replays.
State and graph semantics¶
The BF16 state layout remains [N,H,V,K] and an explicitly supplied
initial_state is still updated in place, even when
output_final_state=False. The frozen kernels load each CTA’s disjoint
state rows before writing the final rows back to the same storage, so no
separate state scratch or copy-back is required. If no initial state is
supplied, a final state is allocated only when output_final_state=True.
With ssm_state_indices, initial_state is a caller-owned pool
[N_pool,H,V,K]. Sequence i loads and updates the named pool row in
place. The pool may have padding between first-dimension slots, but each
[H,V,K] slot must be contiguous and both the pool base and slot pitch must
be 16-byte aligned. Slot ids must be unique and in range; this is a caller
contract so the launch path does not synchronize to inspect them.
Native checkpoints use a preallocated BF16
state_checkpoints[C,H,V,K], int64 checkpoint_cu_starts[N+1], and a
positive checkpoint_every_n_tokens divisible by 16. KDA checkpoints are
states before each interval: every non-empty sequence contributes its
initial state as row zero, followed by states after one, two, … intervals
that strictly precede its end. Consequently each sequence contributes
ceil(seq_len / interval) rows. The call returns
(output, final_state, state_checkpoints) when enabled. Intervals and
cumulative counts are caller-provided device metadata and are not value-scanned
at launch.
An aligned beta base address and a token pitch divisible by 16 bytes are sent
directly to TMA. Other eligible row-strided beta views remain valid API inputs,
but the binding refreshes reusable padded workspace internally; callers never
need to materialize beta.contiguous().
The frozen kernel uses restricted output and auxiliary storage. A preallocated
output and checkpoint buffers must not overlap Q, K, V, G, beta, state,
metadata, or descriptor storage.
Eager calls without prefill_workspace use an internal serialized workspace
for the current CUDA stream. This default workspace is eager-only and cannot
be used during CUDA graph capture.
CUDA graph capture requires a caller-owned
RecurrentKDAPrefillWorkspace(device) and a preallocated output. The
workspace owns optional final-state scratch for calls without an initial
state, beta padding, separate TMA descriptor blocks, and the small-BH compact
packet ring with its generation counters. It binds to the device and CUDA
stream of its first recurrent_kda call.
Warm it eagerly on the intended capture stream with the exact Q, K, V, G,
beta, and output tensors, then synchronize that stream before capture. Packed
graphs must also pass preallocated int64 cu_seqlens and int32
seq_order. The warm call prepares descriptors; capture accepts only the
exact warmed pointer, shape, stride, and dtype signature and performs no
descriptor preparation. Warm the largest intended small-BH shape before
capturing so its packet-ring storage is already allocated.
The workspace must outlive its graph and every replay. Use one distinct
workspace for each captured recurrent_kda invocation, including two KDA
invocations in the same graph. Once a workspace participates in capture, any
later Python use through recurrent_kda—eager or another capture—is
rejected. graph.replay() does not re-enter Python and remains valid.
Sequential replay launches may be issued while a different PyTorch stream is
current, but the caller must provide normal stream ordering. The Python stream
binding applies to eager warmup and capture calls, which must use the same
stream.
When an explicit workspace is used with initial_state=None and
output_final_state=True, the returned final state is workspace-owned
stable scratch. Otherwise an explicitly supplied initial_state is updated
directly in place by the frozen kernel. Head counts that are not divisible by
eight capture the beta copy into workspace-owned storage padded to the next
eight-head boundary before the frozen launch. The public beta and state shapes
keep the caller’s original head count.
SM120a prefill subset¶
Compute capability 12.0 devices select their own prefill backend,
flashinfer.kda_kernels.sm120_prefill. It is CuTe DSL like the BT=16 backend
above and shares no code and no device with it, nor with Cake: those two are
CC 10.0 and 10.3 and this one is CC 12.0, so at most one of the three can be
eligible for any call, and adding this one cannot change which kernel an SM100
or SM103 call receives. Nothing about the public API changes — the entry point
is still flashinfer.kda.recurrent_kda(), and no argument names the
architecture.
backend selects an implementation family, not an architecture name. On a
CC 12.0 device both "auto" and "cute-dsl" may reach this backend; the
dispatcher tries it before the SM100-family CuTe DSL prefill path. An explicit
"cake" request never probes or runs SM120. If the Cake prefill predicate
does not support that ordinary multi-token prefill call, the request is
refused rather than silently executed by another backend.
recurrent_kda uses it only when every condition below holds:
the device has compute capability 12.0, and the installed CuTe DSL and CUDA toolkit can natively target
sm_120a. A family-conditional fallback target is refused rather than accepted, because the kernels are written against architecture-specific instructions;input is ordinary multi-token prefill: fixed
T > 1, or packed input whose total token count exceeds its number of sequences;Q, K, V and G are contiguous BF16
[B,T,H,128]tensors sharing one head count, and beta is contiguous BF16[B,T,H]. GQA andV != Kare not supported;the output fits an INT32 extent:
T_total * H * 128 <= 2**31 - 1, which is 16383 tokens at H=1024 and no constraint at ordinary head counts. Larger is refused with the backend’s own error, because the two things that stop there — a device index built in INT32, and the DSL packing a memref extent as one — otherwise fail as a silent negative offset and as a compile-time overflow naming no tensor;A_logis contiguous FP32[H], anddt_biasis contiguous FP32[H,128]or flattened[H*128];use_qk_l2norm_in_kernel=True,use_gate_in_kernel=True,beta_is_logit=True, andlower_boundis in[-5.0, 0.0). The bound exists because the safe gate’s worst-case chunk prefix reaches a reciprocal approximation’s cliff at about-5.4585;initial_state, if given, is a contiguous BF16[N,H,128,128]tensor. A state pool withssm_state_indicesis not supported;output, if given, is contiguous BF16 with V’s shape and does not overlap any input in GMEM;speculative decode,
seq_order, prefill checkpoints, committed-state sources and FP32 gate or state are not enabled.
Under backend="auto", calls outside that subset continue through the
existing dispatcher. An explicit backend="cute-dsl" ordinary multi-token
prefill request is refused when neither CuTe DSL prefill implementation is
eligible. T=1 decode and speculative decode are not rerouted by this backend.
Two variants, chosen per shape¶
The backend has two implementations of the same contract. decomp runs a
chunk-parallel prepare and a serial recurrence, issued through one compiled
host entry; fused does both in a single kernel. They agree numerically, so
the choice between them is a performance one and is made from a measured
table keyed on the device’s SM count — not on its name, which is not a stable
unique selector.
Thresholds exist for the 110-SM, 156-SM and 188-SM parts, measured on each. They do not agree: the 110-SM part switches to the fused kernel at CTA 128 and the other two at 144, because at CTA 128 with short sequences the larger parts still prefer the decomposed kernel and the smallest one does not. Any other CC 12.0 device uses the 156-SM thresholds as a labelled fallback. A benchmark run reports which case applies, so a number taken on an unprofiled card cannot be read as tuned.
State and graph semantics¶
State semantics match the SM100-family path: a supplied initial_state is
updated in place whether or not output_final_state is set, and the second
return value is None when it is not set. Without an initial state, a BF16
final state is allocated only when output_final_state=True.
CUDA graph capture requires a caller-owned RecurrentKDAPrefillWorkspace and
a preallocated output, warmed eagerly on the capture stream with the exact
tensors and then synchronized before capture. The warm call is where every
compile, descriptor build, metadata table and allocation happens; capture
performs none of them and a cold capture is refused rather than silently
degraded. Both offsets dtypes are accepted for packed capture; eager warmup
populates a workspace-owned canonical int32 buffer. The offset values must
stay fixed for the graph’s lifetime. Changing them requires a fresh eager
warmup and capture. Q, K, V, G, beta and state contents may change freely at
unchanged addresses.
The offsets contract is not only a capture one. Validating cu_seqlens
needs a device-to-host read, so what is derived from it — the sequence
lengths, the canonical int32 copy, and the decomposed variant’s chunk tables —
is cached against the tensor’s address and version counter rather than read
again on every call. Under torch.inference_mode a tensor has no version
counter, so refilling an offsets buffer in place with a different segmentation
is not detectable and the stale tables are reused, silently computing against
the previous sequence boundaries. Use a different tensor for a different
segmentation, or call
flashinfer.kda_kernels.sm120_prefill.clear_kda_prefill_sm120_caches()
after refilling one in place. This applies to eager calls as much as to
captured ones.
A workspace binds to one variant, one stream and one call signature on first use, and once it has participated in a capture it cannot be used again.
What the caches hold¶
A warm call is a memo lookup, and the memo addresses the caller’s buffers: the descriptors carry their base addresses and the flat views wrap them. Those buffers therefore stay allocated for as long as the entry lives, which is what makes reusing the entry safe — an allocator that had recycled the address would otherwise hand the kernel someone else’s memory.
The retention scales with the number of distinct buffer sets a process
rotates through, not with the number of calls. On a 110-SM part at
[1, 1024, 8, 128] one set holds about 14.5 MiB, and eight rotating sets
about 73 MiB. Reuse one set and it stays at one set’s worth forever.
The entry ceilings are not a memory budget and lowering them does not trade
speed for memory: below the ceiling the retention is the same whatever the
ceiling is, and above it every call rebuilds its plan — about 7.3 ms against a
100 microsecond hit on that part. A deployment that needs the memory back
should rotate fewer buffer sets, or call
flashinfer.kda_kernels.sm120_prefill.clear_kda_prefill_sm120_caches(),
which releases all of it.