Look at all those SMs: Making FlashKDA 2.5x Faster


FlashKDA is Moonshot’s implementation of the Kimi Delta Attention (KDA) prefill: two CuteDSL kernels that are faster when there are many tokens to process. It does this by parallelising over the tokens and the attention heads used in KDA. However at high tensor parallelism the number of heads becomes quite small. This gives you fewer opportunities for parallelism and hence you risk idle GPU resources. This becomes apparent with some simple benchmarking. At 8,192 tokens on a B200 it takes about 1,030 µs with 96 heads and still about 730 µs with 6, even though it is doing sixteen times less work. That flat line points to a GPU that is mostly idle, and this post is about filling it.

The Kimi K3 model, which uses KDA, uses 96 heads in its KDA implementation. If you run it in TP8 (for example on an 8xB300 node) or in TP16 (which you might do on an NVL72) then suddenly you find yourself with 6-12 heads per node, and you are severely underutilizing your GPU resources.

That flat line points to a GPU that is mostly idle, and this post is about filling it. We changed FlashKDA’s recurrence kernel so that one head’s state is spread across several SMs instead of one, and kept in registers instead of shared memory. The recurrence gets 2.6–2.7× faster for 24 heads or fewer, about 1.95× at 48 heads and 1.2× at 96. Both kernels together get up to 2.58× faster. Every output byte and every bit of the final state is identical to the original kernel’s.

Three steps from left to right: FlashKDA runs each head's recurrence on one SM; splitting the state by value columns spreads a head over up to four SMs; keeping each warp's state slice in registers removes the shared-memory round trip and the per-chunk barrier.

Figure 1: The two changes we made to FlashKDA's recurrence kernel.

1. FlashKDA in two kernels

For some background on KDA, read You Could Have Come Up With Kimi Delta Attention.

FlashKDA computes the chunkwise KDA forward pass over 16-token chunks in two kernels:

Kernel Grid Threads per CTA What it does
1Prepare chunks × heads 256
  • normalize q and k
  • activate and prefix-sum the gates
  • decay each key and query from the chunk start to its own token
  • decay each key from its own token to the chunk end
  • invert the 16×16 lower-triangular I + Rc
  • form Aqkc
2Recurrence sequences × heads 192
  • walk the chunks in order, carrying the state
  • write the output

The first kernel is embarrassingly parallel. Every CTA handles one 16-token chunk of one head, so a long prompt launches thousands of CTAs. It writes a per-chunk workspace for the second kernel. Number the chunk’s tokens i=0,…,15i=0,\dots,15, let Di=Diag⁡(αi)D_i=\operatorname{Diag}(\alpha_i) be token ii‘s per-channel decay, and write D0:i=D0D1⋯DiD_{0:i}=D_0D_1\cdots D_i for the decay from the start of the chunk to token ii. The workspace holds:

  • KcstartK_c^{\mathrm{start}} and QcstartQ_c^{\mathrm{start}}: each key and query decayed from the start of the chunk to its own token. Column ii is D0:i∣ki⟩D_{0:i}\lvert k_i\rangle, or D0:i∣qi⟩D_{0:i}\lvert q_i\rangle.
  • KcendK_c^{\mathrm{end}}: each key decayed from its own token to the end of the chunk. Row ii is ⟨ki∣Di+1:15\langle k_i\rvert D_{i+1:15}.
  • D0:15D_{0:15}: the chunk’s total decay.
  • Ackk=(I+Rc)−1A^{kk}_c=(I+R_c)^{-1}: the inverse of a 16×16 lower-triangular matrix, where (Rc)ij=βi⟨kj∣Dj+1:i∣ki⟩(R_c)_{ij}=\beta_i\langle k_j\rvert D_{j+1:i}\lvert k_i\rangle for j<ij<i measures how much token jj‘s write disturbs token ii‘s prediction.
  • AcqkA^{qk}_c: the causal query-key scores, (Acqk)ij=s⟨kj∣Dj+1:i∣qi⟩(A^{qk}_c)_{ij}=s\langle k_j\rvert D_{j+1:i}\lvert q_i\rangle for j≤ij\le i.

The second kernel carries the recurrent state, which makes it the serial one. Each CTA loops over the entire set of tokens for a given Head and Batch pair, in chunks of 16 tokens. Your parallelism doesn’t grow with the number of chunks, so your only options for exposing parallelism is over heads and batches.

FlashKDA keeps the state ScS_c entering chunk cc as a 128×128 BF16 matrix in shared memory, stored transposed as ScTS_c^\mathsf T (keys by values). With the chunk’s values as the columns of VcV_c, its outputs as the columns of OcO_c and Bc=Diag⁡(β0,…,β15)B_c=\operatorname{Diag}(\beta_0,\dots,\beta_{15}), for every chunk it computes:

Uc=(Vc−ScKcstart)Bc(Ackk)TU_c = \left(V_c - S_cK_c^{\mathrm{start}}\right)B_c\left(A^{kk}_c\right)^\mathsf T Oc=sScQcstart+Uc(Acqk)TO_c = sS_cQ_c^{\mathrm{start}} + U_c\left(A^{qk}_c\right)^\mathsf T Sc+1=ScD0:15+UcKcendS_{c+1} = S_cD_{0:15} + U_cK_c^{\mathrm{end}}

Column ii of UcU_c is token ii‘s delta-rule error: what it writes into the state. These are the chunkwise equations derived in section 6 of the KDA post.

Each CTA has four compute warps, one warp issuing TMA loads into a three-stage ring, and one warp storing output tiles.

Left: kernel 1, preparation, launches one CTA per 16-token chunk per head, all independent, and writes a per-chunk workspace. Right: kernel 2, the recurrence, launches one CTA per sequence and head; each walks all chunks in order while holding its 128 by 128 BF16 state in shared memory.

Figure 2: FlashKDA's two kernels. Preparation is thousands of independent CTAs. The recurrence is one CTA per head, each running a serial chain over chunks.

2. Where the time goes

Timing the two kernels separately shows the problem straight away:

Horizontal stacked bars for 96, 48, 24, 12 and 6 heads at 8,192 tokens on B200. Preparation takes 301, 153, 77, 41 and 22 microseconds, falling with the head count. The recurrence takes 731, 726, 723, 721 and 710 microseconds, almost unchanged.

Figure 3: FlashKDA's two kernels at 8,192 tokens. Preparation scales with the head count, as it should. The recurrence costs about 720 µs whatever the head count.

Preparation does sixteen times less work at 6 heads than at 96 and runs about fourteen times faster. The recurrence does sixteen times less work and runs at the same speed. At 6 heads it is 97% of the total.

The reason is the shape of its grid. One CTA per head means one SM per head. Each CTA walks 512 chunks one after another, and each chunk’s state update depends on the last. The run time is set by how long one head’s chain takes, not by how many heads there are. A B200 has 148 SMs:

Three panels of 148 squares, one per SM. At 6 heads FlashKDA uses 6 SMs and the column split uses 24. At 24 heads FlashKDA uses 24 and the column split 96. At 96 heads both use 96 SMs.

Figure 4: SMs with recurrence work at batch 1. With 6 heads FlashKDA keeps 4% of the GPU busy. The column split (section 3) gives each head up to four SMs.

At 6 heads, 142 of the 148 SMs sit idle for the whole recurrence. If one head could be split across several SMs that idle hardware would be put to use.

3. Splitting the state

3.1 Every value column is independent

FlashKDA works in a transposed layout. It stores the state as ScTS_c^\mathsf T, and the chunk’s values, errors and outputs as VcTV_c^\mathsf T, UcTU_c^\mathsf T and OcTO_c^\mathsf T: one row per token, one column per value channel. Transposing the three per-chunk equations into that layout puts every per-chunk input on the left:

UcT=AckkBc(VcT−(Kcstart)TScT)U_c^\mathsf T = A^{kk}_cB_c\left(V_c^\mathsf T-\left(K_c^{\mathrm{start}}\right)^\mathsf TS_c^\mathsf T\right) OcT=s(Qcstart)TScT+AcqkUcTO_c^\mathsf T = s\left(Q_c^{\mathrm{start}}\right)^\mathsf TS_c^\mathsf T + A^{qk}_cU_c^\mathsf T Sc+1T=D0:15ScT+(Kcend)TUcTS_{c+1}^\mathsf T = D_{0:15}S_c^\mathsf T + \left(K_c^{\mathrm{end}}\right)^\mathsf TU_c^\mathsf T

One chunk of the FlashKDA recurrence in its transposed layout, drawn as three matrix products with the left operand to the left and the right operand above the product. Step 1: the transposed start-decayed keys times the transposed state give the prediction. Step 2: A kk times B times the values minus the prediction give the transposed errors U. Step 3: the transposed end-decayed keys times U, plus the total decay times the old state, give the next state. Two value columns, n and m, are highlighted in every value matrix and never meet.

Figure 5: One chunk of the recurrence as matrix products. Every per-chunk input multiplies from the left, so each column of a result needs only the same column of the matrix above it.

Follow value column nn (blue) through the chunk. Its column of ScTS_c^\mathsf T gives its column of the prediction, which gives its column of UcTU_c^\mathsf T, which gives its column of Sc+1TS_{c+1}^\mathsf T. Column mm (orange) takes the same path through the same shared inputs and never touches column nn. The output OcTO_c^\mathsf T is built the same way as steps 1 and 2. So the value dimension splits into 128 independent recurrences that happen to share their inputs.

FlashKDA already uses this inside the CTA: each of its four compute warps owns 32 of the 128 columns. We apply the same split across SMs. A task becomes (sequence, head, slice of BV columns), and the grid becomes N⋅H⋅128/BVN \cdot H \cdot 128 / \mathrm{BV} CTAs:

HeadsBVCTAsSMs per headColumns per warp
≤ 24324H48
486496216
9612896132 (as in FlashKDA)

We pick the smallest BV that keeps the grid at or below 148, so the kernel never launches more than one CTA per SM. Each CTA still has four compute warps, one per SM sub-partition, and a TMA loader warp. The preparation kernel and its workspace are unchanged.

Left: in FlashKDA one head runs on one SM whose four compute warps each own 32 value columns of a state kept in shared memory. Right: the column split runs the same head on four SMs, each CTA owning 32 columns split as 8 per warp, with each warp&#x27;s state slice held in registers. All columns read the same per-chunk inputs.

Figure 6: The same head before and after the split. The slices never communicate. They only read the same per-chunk inputs.

The slices share nothing they write, so the split needs no synchronization between CTAs at all: no flags, no atomics, no spinning. The only cost is that every slice CTA loads its own copy of the shared per-chunk inputs, up to four times as much input traffic. Those loads are small, served mostly from L2, and hidden behind the TMA pipeline.

3.2 Why more SMs is faster

Splitting doesn’t shorten the chain. Every chunk still has to wait for the one before it. What it changes is how many tensor cores work on each link of that chain at once.

In FlashKDA, one head’s whole state update runs on one SM. Each of its four compute warps owns 32 value columns, which is four 8-column MMA tiles, so every matrix multiply in a chunk is four HMMAs issued back to back on that SM’s tensor cores. Meanwhile, with 6 heads, the other 142 SMs and their tensor cores sit idle. They have nothing to do because the only remaining work is the next link of a chain that is still running somewhere else.

The column split puts those idle tensor cores to work. A head’s columns are spread over four SMs, and each warp owns just 8 columns, which is a single HMMA per matrix multiply. Sixteen warps on four SMs now share each chunk’s update instead of four warps on one, so four SMs’ worth of tensor cores work on the same link in parallel and the link finishes sooner.

This works because the recurrence is bound by latency, not throughput. In Nsight Compute the column-split kernel at 6 heads keeps the issue slots busy 5% of the time, with 0.35 eligible warps per scheduler. No single SM’s tensor cores are close to full. The limit is how long each link of the chain takes, so spreading a link over more tensor cores translates almost directly into wall-clock time:

Horizontal bars at 6 heads and 2,048 tokens. FlashKDA takes 180.3 microseconds. The column split with 128, 64 and 32 columns per CTA, four warps each, takes 151.6, 90.9 and 66.8 microseconds as a head spreads over 6, 12 and 24 SMs. Keeping 6 SMs and adding warps instead is slower: 175.3 microseconds with 8 warps and 232.4 with 16.

Figure 7: Recurrence time at 6 heads and 2,048 tokens as a head's columns are spread differently. More SMs per head helps. More warps on the same SM does not.

The bottom two bars are the control experiment. They give each warp the same 16 or 8 columns but keep all the warps for a head on one SM. That is barely better than FlashKDA, or worse. Every extra warp has to load the shared KcstartK_c^{\mathrm{start}} and QcstartQ_c^{\mathrm{start}} operands from shared memory itself, and a single SM has a fixed number of tensor cores, schedulers and load paths to share between them. Spreading over SMs adds those resources; packing onto one SM only divides them.

3.3 Keeping the state in registers

The split has a second effect that matters even when it adds no SMs. Once a warp owns its columns outright, its state slice no longer needs to be in shared memory.

In FlashKDA the four warps share one ScTS_c^\mathsf T buffer in shared memory. Each chunk, a warp loads its columns as MMA operands, computes the update and writes it back. Then all warps meet at a barrier before anyone reads the next chunk’s state. In the column-split kernel each warp keeps its ScT[128,8]S_c^\mathsf T[128, 8] slice in registers for the entire sequence. That is eight 16-row blocks in the MMA accumulator layout.

The one wrinkle is layout. The update produces the state in the MMA accumulator layout, but the next chunk needs it as the MMA B-operand. PTX’s movmatrix instruction transposes 8×8 tiles between the two inside a warp’s registers, so the conversion never touches shared memory. Output tiles also skip the staging buffer and store warp, and go straight from registers to global memory.

Top: in FlashKDA a compute warp loads its 32 state columns from shared memory, runs the matrix multiplies that read the state over four n-tiles, the BF16 β step, the triangular-solve multiply, writes output to shared memory for a store warp, updates the state over four n-tiles, stores it back to shared memory and waits at a barrier. Bottom: in the column split a warp transposes its register-held state with movmatrix, runs the same steps on one n-tile of 8 columns, writes output straight to global memory and loops without any barrier.

Figure 8: One chunk for one warp. The column split removes the shared-memory round trip for the state, the output staging and the per-chunk barrier, and each tensor-core step is a quarter as wide.

This is where the 1.2× at 96 heads comes from. There the column split uses BV = 128, the same 96 CTAs and 32 columns per warp as FlashKDA, so it gets no extra SMs. All of the gain comes from Figure 8: no state round trip, no barrier and a simpler output path.

3.4 Why the bits don’t change

FlashKDA is built with --use_fast_math, and its exact results depend on the precise instruction behind every operation. We read those off the SASS of the original build and kept every one of them in the modified kernel:

OperationInstruction in FlashKDA’s SASS
FP32 add, multiply, FMAFADD.FTZ, FMUL.FTZ, FFMA.FTZ (denormals flushed)
BF16 arithmeticHADD2.BF16, HMUL2.BF16, rounded once
exp2, tanhMUFU.EX2, MUFU.TANH
float → BF16F2FP, round to nearest
every matrix multiplyHMMA.16816.F32.BF16, in a fixed k-block order

Per column, each warp runs the same m16n8k16 HMMA on the same k-blocks in the same order. It applies the same FTZ FMA for the state update, the same BF16 subtract and multiply for (Vc−ScKcstart)Bc\left(V_c-S_cK_c^{\mathrm{start}}\right)B_c, the same tanh-based sigmoid and BF16 rounding at the same points. Splitting by columns changes which warp computes a column, never the order of any sum inside it. Every configuration we tried (BV of 32, 64 and 128 with 4, 8 or 16 warps) is bitwise identical to FlashKDA.

3.5 What didn’t help

  • Deeper pipelines. Three to eight input stages make no measurable difference; the loads are already hidden. (Going beyond four stages exposed a bug: a four-entry ring of release barriers aliased mbarrier phases and hung. The ring is now twice the stage count.)
  • Offloading output work. A debug build that removed the entire output path saved only 7%, so dedicated output warps weren’t worth building.
  • Overlapping the two kernels. The recurrence for chunk cc only needs the preparation output for chunk cc, so we tried running both kernels at once, synchronized by per-chunk ready flags. It was about 8% faster at 24 and 48 heads, but slower at 96 heads and at 12 or fewer, so we left it out.

What is left is the chain itself: 8 dependent HMMAs for ScKcstartS_cK_c^{\mathrm{start}}, the BF16 β\beta step, the (Ackk)T(A^{kk}_c)^\mathsf T multiply, 8 HMMAs for the update, the FMA and the movmatrix transpose, chunk after chunk. Reordering any of it would change rounding, and with it the bits.

4. Results

All numbers are medians of 11 to 15 CUDA-graph samples per variant. Variants were sampled in rotating order, one exclusive B200 per head count, with compilation excluded. Both recurrence kernels read the same workspace written by FlashKDA’s preparation kernel. Before timing, the modified kernel’s output and final state were checked bitwise against FlashKDA’s in each of the 35 shapes.

The right-hand numbers add the preparation kernel’s time to each recurrence. Preparation is unchanged, so it costs the same in both columns.

Grouped bars for 96, 48, 24, 12 and 6 heads at 8,192 tokens. Left, recurrence only: FlashKDA takes about 710 to 731 microseconds at every head count, while the column split takes 613, 374, 276, 272 and 262 microseconds. Right, preparation plus recurrence: FlashKDA 1033, 879, 800, 763 and 732 microseconds, with the column split 915, 527, 354, 313 and 283 microseconds.

Figure 9: Latency at 8,192 tokens. The column split is what makes the recurrence's cost follow the head count.
HeadsRecurrence, FlashKDA (µs)Recurrence, column splitSpeedupBoth kernels, FlashKDABoth kernels, column splitSpeedup
96731.3613.41.19×1032.6914.71.13×
48726.1374.21.94×878.8526.91.67×
24722.5276.52.61×799.5353.52.26×
12721.2271.82.65×762.5313.12.44×
6710.0261.72.71×731.7283.42.58×

The pattern holds at every sequence length we measured:

Two heatmaps of speedup over unmodified FlashKDA by head count and sequence length. Recurrence only: 1.19 to 1.34 at 96 heads, about 1.95 at 48 heads and 2.6 to 2.7 at 24 heads and below. Preparation plus recurrence: 1.13 to 1.21 at 96 heads, 1.59 to 1.67 at 48 heads and 1.91 to 2.58 at 24 heads and below.

Figure 10: Speedup over unmodified FlashKDA for all 35 shapes.