Skip to content

[KDA] Add fused BT=16 inference kernels for KDA prefill#915

Closed
kuoihao wants to merge 0 commit into
fla-org:mainfrom
kuoihao:main
Closed

[KDA] Add fused BT=16 inference kernels for KDA prefill#915
kuoihao wants to merge 0 commit into
fla-org:mainfrom
kuoihao:main

Conversation

@kuoihao

@kuoihao kuoihao commented May 22, 2026

Copy link
Copy Markdown

[Ops] Add fused BT=16 inference kernels for KDA prefill

Fuse L2 norm, beta sigmoid, gate activation, intra-chunk attention,
state propagation, and output into two Triton kernels to reduce
intermediate tensor global memory round-trips during inference.

The fused path activates automatically when grad is disabled and
use_gate_in_kernel is set, with no change to the training path.

Testing

Environment: NVIDIA H800 | CUDA 12.9 | PyTorch 2.10.0+cu129

Performance

Fused BT=16 inference kernels vs baseline (multi-kernel path):

B T H D Fused (ms) Baseline (ms) Speedup
1 8192 96 128 1.986 3.176 1.60x
2 16384 16 128 1.747 2.160 1.24x
4 2048 16 128 0.378 0.562 1.49x
4 4096 64 128 2.404 4.002 1.67x
8 1024 8 64 0.116 0.493 4.25x
8 2048 32 256 3.008 4.752 1.58x

Average speedup: ~1.6x, up to 4.25x on small shapes where kernel launch overhead dominates.

python -m benchmarks.ops.run --op chunk_kda_infer           # fused (default)
FLA_FUSED_INFER=0 python -m benchmarks.ops.run --op chunk_kda_infer  # baseline

Correctness

All tests pass with max output diff < 0.001 and ratio < 0.004:

pytest tests/ops/test_kda.py::test_chunk_fwd_inference -xvs         # 5/5 passed
pytest tests/ops/test_kda.py::test_chunk_fwd_inference_varlen -xvs  # 3/3 passed
Test o diff o ratio ht diff ht ratio
B4-T1024-H4-HV4-D128 (gate+bias) 0.000728 0.002588 0.002226 0.001217
B2-T2048-H4-HV8-D64 (gate+bias) 0.000766 0.002528 0.004225 0.000988
B1-T8192-H4-HV4-D128 (gate, no bias) 0.000795 0.003077 0.000762 0.002426
B4-T1024-H4-HV4-D128 (no gate) 0.000812 0.003123 0.001054 0.002839
B2-T2048-H4-HV8-D64 (fp16, no gate) 0.000178 0.000960 0.000165 0.000368
varlen [0,256,500,1000] 0.000652 0.003427 0.006081 0.003310
varlen [0,100,300,1200,2000] 0.000148 0.000850 0.000900 0.000409
varlen [0,101,303,1205,3007,4096] 0.000800 0.003458 0.005519 0.003007
example FlashKDA (CUDA) Triton FusedInfer TLE TLE vs FlashKDA TLE vs Triton
fixed [8192, 96, 128] 1.49 ms 2.00 ms 1.33 ms 1.12x 1.50x
varlen [1300,547,2048,963,271,3063] 1.53 ms 1.96 ms 1.29 ms 1.19x 1.52x
varlen [1024]*8 1.36 ms 1.88 ms 1.26 ms 1.08x 1.49x

data:

tle speed

(tle-0.5.1) ╭─root at p-lenovo-h800-01 in ~/workspace/fla-4 on fla-4✘✘✘ 26-06-11 - 10:19:54
╰─(tle-0.5.1) ⠠⠵ FLA_FLASH_KDA=0 FLA_FUSED_INFER=0 /root/workspace/bench_fwd.sh fla-4
shape=[8192,96,128] warmup=30 iters=200 repeats=5
flash_kda (bf16 state) : mean=1.4954 ms, min=1.4884 ms, max=1.5457 ms
flash_kda (no state) : mean=1.5099 ms, min=1.5032 ms, max=1.6497 ms
flash_kda (fp32 state) : mean=1.5123 ms, min=1.5070 ms, max=1.6644 ms
chunk_kda : mean=3.4993 ms, min=3.4796 ms, max=4.2836 ms
chunk_kda (tle) : mean=1.3300 ms, min=1.3122 ms, max=1.4548 ms
chunk_gated_delta_rule : mean=1.8883 ms, min=1.8765 ms, max=2.1909 ms
varlen shape=[8192,96,128] seq_lens=[1300, 547, 2048, 963, 271, 3063] warmup=30 iters=200 repeats=5
flash_kda (bf16 state) : mean=1.5303 ms, min=1.4744 ms, max=1.7063 ms
flash_kda (no state) : mean=1.5097 ms, min=1.4402 ms, max=1.5537 ms
flash_kda (fp32 state) : mean=1.5615 ms, min=1.5022 ms, max=1.7084 ms
chunk_kda : mean=3.5166 ms, min=3.4974 ms, max=3.8342 ms
chunk_kda (tle) : mean=1.2946 ms, min=1.2893 ms, max=1.4155 ms
chunk_gated_delta_rule : mean=1.8844 ms, min=1.8757 ms, max=2.0089 ms
varlen shape=[8192,96,128] seq_lens=[1024, 1024, 1024, 1024, 1024, 1024, 1024, 1024] warmup=30 iters=200 repeats=5
flash_kda (bf16 state) : mean=1.3645 ms, min=1.3537 ms, max=1.3924 ms
flash_kda (no state) : mean=1.3343 ms, min=1.3247 ms, max=1.4495 ms
flash_kda (fp32 state) : mean=1.3804 ms, min=1.3710 ms, max=1.4020 ms
chunk_kda : mean=3.4866 ms, min=3.4748 ms, max=3.7514 ms
chunk_kda (tle) : mean=1.2626 ms, min=1.2533 ms, max=1.3676 ms
chunk_gated_delta_rule : mean=1.8723 ms, min=1.8648 ms, max=2.0289 ms

triton speed

(tle-0.5.1) ╭─root at p-lenovo-h800-01 in ~/workspace/fla-4 on fla-4✘✘✘ 26-06-11 - 10:23:17
╰─(tle-0.5.1) ⠠⠵ FLA_FLASH_KDA=0 FLA_TLE=0 /root/workspace/bench_fwd.sh fla-4
shape=[8192,96,128] warmup=30 iters=200 repeats=5
flash_kda (bf16 state) : mean=1.4959 ms, min=1.4882 ms, max=1.5461 ms
flash_kda (no state) : mean=1.5103 ms, min=1.5043 ms, max=1.5353 ms
flash_kda (fp32 state) : mean=1.5133 ms, min=1.5073 ms, max=1.5482 ms
chunk_kda : mean=3.4994 ms, min=3.4802 ms, max=3.6512 ms
chunk_kda (tle) : mean=1.9965 ms, min=1.9694 ms, max=2.1630 ms
chunk_gated_delta_rule : mean=1.8866 ms, min=1.8741 ms, max=2.0010 ms
varlen shape=[8192,96,128] seq_lens=[1300, 547, 2048, 963, 271, 3063] warmup=30 iters=200 repeats=5
flash_kda (bf16 state) : mean=1.5310 ms, min=1.4749 ms, max=1.5844 ms
flash_kda (no state) : mean=1.5095 ms, min=1.4454 ms, max=1.5972 ms
flash_kda (fp32 state) : mean=1.5613 ms, min=1.4979 ms, max=1.6116 ms
chunk_kda : mean=3.5100 ms, min=3.4975 ms, max=3.6572 ms
chunk_kda (tle) : mean=1.9560 ms, min=1.9217 ms, max=2.2188 ms
chunk_gated_delta_rule : mean=1.8811 ms, min=1.8726 ms, max=2.0651 ms
varlen shape=[8192,96,128] seq_lens=[1024, 1024, 1024, 1024, 1024, 1024, 1024, 1024] warmup=30 iters=200 repeats=5
flash_kda (bf16 state) : mean=1.3645 ms, min=1.3551 ms, max=1.4614 ms
flash_kda (no state) : mean=1.3341 ms, min=1.3247 ms, max=1.4173 ms
flash_kda (fp32 state) : mean=1.3820 ms, min=1.3726 ms, max=1.4370 ms
chunk_kda : mean=3.4890 ms, min=3.4809 ms, max=3.7547 ms
chunk_kda (tle) : mean=1.8754 ms, min=1.8514 ms, max=2.1503 ms
chunk_gated_delta_rule : mean=1.8708 ms, min=1.8628 ms, max=2.0563 ms

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request introduces a fused inference fast path for KDA prefill by implementing two specialized Triton kernels that combine gate activation, L2 normalization, and state propagation to reduce memory overhead. Review feedback highlights the need to enforce a chunk size of 16 throughout the fast path to match kernel expectations. Other suggestions include improving numerical stability by using higher precision for matrix inversions and adjusting normalization epsilons, as well as fixing a redundant memory allocation issue for intermediate states in variable-length sequences.

Comment thread fla/ops/kda/chunk.py Outdated
if cu_seqlens is not None:
chunk_indices = prepare_chunk_indices(
cu_seqlens,
chunk_size,

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.

critical

The inference fast path is designed for fused kernels that are hardcoded for a chunk size of 16 (BT=16). However, chunk_indices are currently prepared using the chunk_size argument passed to the function, which defaults to 64. This mismatch will cause the kernels to process the sequence incorrectly. You should force the chunk size to 16 when preparing indices for this path.

Suggested change
chunk_size,
16,

Comment thread fla/ops/kda/chunk.py Outdated
use_gate_in_kernel=use_gate_in_kernel,
A_log=A_log,
dt_bias=dt_bias,
chunk_size=chunk_size,

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.

critical

As the fused inference kernels require a chunk size of 16, the chunk_size argument passed to chunk_kda_fwd should be explicitly set to 16 here to ensure consistency with the prepared indices and kernel expectations.

Suggested change
chunk_size=chunk_size,
chunk_size=16,

Comment thread fla/ops/kda/chunk_fwd_infer.py Outdated
Comment on lines +165 to +172
b_L = b_Akk.to(tl.float16)
b_Ai = m_I.to(tl.float16) - b_L
b_L2 = tl.dot(b_L, b_L, out_dtype=tl.float16)
b_Ai = b_Ai + tl.dot(b_Ai, b_L2, out_dtype=tl.float16)
b_L4 = tl.dot(b_L2, b_L2, out_dtype=tl.float16)
b_Ai = b_Ai + tl.dot(b_Ai, b_L4, out_dtype=tl.float16)
b_L8 = tl.dot(b_L4, b_L4, out_dtype=tl.float16)
b_Ai = b_Ai + tl.dot(b_Ai, b_L8, out_dtype=tl.float16)

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.

high

The parallel prefix expansion for solving $(I+L)^{-1}$ is performed using tl.float16. Since this involves multiple matrix multiplications ($L^2, L^4, L^8$), precision errors can accumulate significantly and lead to numerical instability in the attention mechanism. It is highly recommended to perform these operations in float32 to maintain accuracy, especially since 16x16 matrix operations are very efficient on modern GPUs.

Suggested change
b_L = b_Akk.to(tl.float16)
b_Ai = m_I.to(tl.float16) - b_L
b_L2 = tl.dot(b_L, b_L, out_dtype=tl.float16)
b_Ai = b_Ai + tl.dot(b_Ai, b_L2, out_dtype=tl.float16)
b_L4 = tl.dot(b_L2, b_L2, out_dtype=tl.float16)
b_Ai = b_Ai + tl.dot(b_Ai, b_L4, out_dtype=tl.float16)
b_L8 = tl.dot(b_L4, b_L4, out_dtype=tl.float16)
b_Ai = b_Ai + tl.dot(b_Ai, b_L8, out_dtype=tl.float16)
b_L = b_Akk.to(tl.float32)
b_Ai = m_I.to(tl.float32) - b_L
b_L2 = tl.dot(b_L, b_L)
b_Ai = b_Ai + tl.dot(b_Ai, b_L2)
b_L4 = tl.dot(b_L2, b_L2)
b_Ai = b_Ai + tl.dot(b_Ai, b_L4)
b_L8 = tl.dot(b_L4, b_L4)
b_Ai = b_Ai + tl.dot(b_Ai, b_L8)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Inversion can be done using fp16, provided that qk must l2norm

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

The use_qk_l2norm flag controls whether the kernel does norm internally. When False, it assumes q/k are already L2-normalized externally — the fp16 inversion is still safe since the spectral radius is bounded either way.

Comment thread fla/ops/kda/chunk_fwd_infer.py Outdated
Comment thread fla/ops/kda/chunk_fwd_infer.py Outdated
Comment on lines +590 to +592
h = kg.new_zeros(NT * N, HV, V, K, dtype=kg.dtype)
else:
h = kg.new_zeros(NT * N, HV, K, V, dtype=kg.dtype)

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.

medium

The memory allocation for intermediate states h is incorrect for variable-length sequences. When cu_seqlens is provided, NT (calculated as len(chunk_indices)) already represents the total number of chunks across all sequences in the batch. Multiplying by N results in redundant memory allocation.

Suggested change
h = kg.new_zeros(NT * N, HV, V, K, dtype=kg.dtype)
else:
h = kg.new_zeros(NT * N, HV, K, V, dtype=kg.dtype)
if state_v_first:
h = kg.new_zeros(NT if cu_seqlens is not None else NT * N, HV, V, K, dtype=kg.dtype)
else:
h = kg.new_zeros(NT if cu_seqlens is not None else NT * N, HV, K, V, dtype=kg.dtype)

@kuoihao
kuoihao marked this pull request as ready for review May 24, 2026 14:14

@zhiyuan1i zhiyuan1i left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thank you for your contribution. Looks similar to FLASHKDA. I am very curious about the complete performance, I am curious about the performance of your contributed kernel for D=128, different varlen lengths, and shorter sequence lengths. Also: I hope you can use the dispatch backend and set the default to False, or set the priority to a lower level. You can check the class signatures and comments related to dispatch.

Comment thread fla/ops/kda/chunk_fwd_infer.py Outdated
Comment on lines +165 to +172
b_L = b_Akk.to(tl.float16)
b_Ai = m_I.to(tl.float16) - b_L
b_L2 = tl.dot(b_L, b_L, out_dtype=tl.float16)
b_Ai = b_Ai + tl.dot(b_Ai, b_L2, out_dtype=tl.float16)
b_L4 = tl.dot(b_L2, b_L2, out_dtype=tl.float16)
b_Ai = b_Ai + tl.dot(b_Ai, b_L4, out_dtype=tl.float16)
b_L8 = tl.dot(b_L4, b_L4, out_dtype=tl.float16)
b_Ai = b_Ai + tl.dot(b_Ai, b_L8, out_dtype=tl.float16)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Inversion can be done using fp16, provided that qk must l2norm

Comment thread fla/ops/kda/chunk_fwd_infer.py Outdated
Comment thread fla/ops/kda/chunk_fwd.py Outdated
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.

2 participants