[Perf] Integrate FlashInfer KDA kernels (#55364)

Signed-off-by: wzhao18 <wzhao18.sz@gmail.com>

Wei Zhao committed Sep 7, 2026 at 23:29 UTC a69402aaa8171851232fd6cdd19a33c2a49fd670
8 files changed +722 -155
tests/models/kimi_k3/test_kda.py
+348 -113
@@ -13,6 +13,7 @@ import torch
13 import torch.nn.functional as F
14
15 from vllm import _custom_ops as ops
16 +from vllm.compilation.breakable_cudagraph import BreakableCUDAGraphCapture
17 from vllm.model_executor.layers.mamba.ops.causal_conv1d import causal_conv1d_update
18 from vllm.model_executor.layers.mamba.ops.gather_initial_states import (
19 gather_initial_states,
@@ -22,8 +23,11 @@ from vllm.models.kimi_k3.amd.ops.third_party.kda import (
23 )
24 from vllm.models.kimi_k3.nvidia import kda as nvidia_kda
25 from vllm.models.kimi_k3.nvidia.kda import (
26 + _flashinfer_kda_prefill,
27 _flashkda_prefill,
28 _store_cache_checkpoints_kernel,
29 + is_flashinfer_fused_kda_decode_supported,
30 + is_flashinfer_recurrent_kda_prefill_supported,
31 is_flashkda_supported,
32 is_fused_kda_decode_supported,
33 )
@@ -43,6 +47,7 @@ from vllm.models.kimi_k3.nvidia.ops.third_party.kda import (
47 )
48 from vllm.platforms import current_platform
49 from vllm.third_party.flash_linear_attention.ops.l2norm import l2norm_fwd
50 +from vllm.utils.flashinfer import flashinfer_fused_kda_decode
51 from vllm.v1.attention.backends.utils import NULL_BLOCK_ID
52
53 DEVICE = current_platform.device_type
@@ -87,6 +92,7 @@ def test_kda_recoverssm_config_state_layout():
92 ),
93 cache_config=SimpleNamespace(
94 mamba_cache_dtype="auto",
95 + mamba_ssm_cache_dtype="auto",
96 use_kda_recoverssm=True,
97 ),
98 parallel_config=SimpleNamespace(tensor_parallel_size=1),
@@ -909,6 +915,7 @@ def test_kda_recoverssm_verify_and_group_commit(
915 (96, 1, -5.0, True, "DS"),
916 ],
917 )
918 +@pytest.mark.parametrize("decode_backend", ["native", "flashinfer"])
919 @torch.inference_mode()
920 def test_fused_kda_decode_correctness(
921 num_heads: int,
@@ -916,17 +923,36 @@ def test_fused_kda_decode_correctness(
923 lower_bound: float | None,
924 fuse_output_norm: bool,
925 conv_layout: str,
926 + decode_backend: str,
927 ):
928 D, W = 128, 4
921 - if not is_fused_kda_decode_supported(
922 - num_heads,
923 - D,
924 - W,
925 - num_spec=0,
926 - input_dtype=torch.bfloat16,
927 - conv_state_dtype=torch.bfloat16,
928 - ):
929 - pytest.skip("Fused KDA decode is not supported on this platform")
929 + state_dtype = torch.bfloat16 if decode_backend == "flashinfer" else torch.float32
930 + if decode_backend == "flashinfer":
931 + if conv_layout == "DS":
932 + pytest.skip("FlashInfer fused decode requires SD conv-state layout")
933 + if not fuse_output_norm:
934 + pytest.skip("FlashInfer's fused decode always applies output norm")
935 + supported = is_flashinfer_fused_kda_decode_supported(
936 + num_heads,
937 + D,
938 + W,
939 + num_spec=0,
940 + input_dtype=torch.bfloat16,
941 + conv_state_dtype=torch.bfloat16,
942 + recurrent_state_dtype=state_dtype,
943 + )
944 + else:
945 + supported = is_fused_kda_decode_supported(
946 + num_heads,
947 + D,
948 + W,
949 + num_spec=0,
950 + input_dtype=torch.bfloat16,
951 + conv_state_dtype=torch.bfloat16,
952 + recurrent_state_dtype=state_dtype,
953 + )
954 + if not supported:
955 + pytest.skip(f"{decode_backend} fused KDA decode is not supported")
956 torch.manual_seed(967 + num_heads + num_seqs + (conv_layout == "DS"))
957 dim = num_heads * D
958 slots = num_seqs + 2
@@ -995,7 +1021,7 @@ def test_fused_kda_decode_correctness(
1021 D,
1022 dtype=torch.float32,
1023 device=DEVICE,
998 - )
1024 + ).to(state_dtype)
1025
1026 conv_ref = conv_seed.clone()
1027 state_ref = state_seed.clone()
@@ -1030,7 +1056,7 @@ def test_fused_kda_decode_correctness(
1056 conv_slot_elements = 3 * dim * (W - 1)
1057 state_slot_elements = num_heads * D * D
1058 conv_slot_bytes = conv_slot_elements * torch.bfloat16.itemsize
1033 - page_bytes = conv_slot_bytes + state_slot_elements * torch.float32.itemsize
1059 + page_bytes = conv_slot_bytes + state_slot_elements * state_dtype.itemsize
1060 cache_storage = torch.empty(slots * page_bytes, dtype=torch.uint8, device=DEVICE)
1061 conv_actual = torch.as_strided(
1062 cache_storage.view(torch.bfloat16),
@@ -1042,30 +1068,57 @@ def test_fused_kda_decode_correctness(
1068 ),
1069 )
1070 state_actual = torch.as_strided(
1045 - cache_storage.view(torch.float32),
1071 + cache_storage.view(state_dtype),
1072 size=(slots, num_heads, D, D),
1047 - stride=(page_bytes // torch.float32.itemsize, D * D, D, 1),
1048 - storage_offset=conv_slot_bytes // torch.float32.itemsize,
1073 + stride=(page_bytes // state_dtype.itemsize, D * D, D, 1),
1074 + storage_offset=conv_slot_bytes // state_dtype.itemsize,
1075 )
1076 conv_actual.copy_(conv_seed)
1077 state_actual.copy_(state_seed)
1078 fused_weight = weight.reshape(3, dim, W).transpose(1, 2).contiguous()
1053 - actual = ops.fused_kda_decode(
1054 - x=packed_x,
1055 - weight=fused_weight,
1056 - bias=None,
1057 - conv_state=conv_actual,
1058 - raw_g=raw_g,
1059 - raw_beta=raw_beta,
1060 - A_log=A_log,
1061 - dt_bias=dt_bias,
1062 - state_indices=state_indices,
1063 - state=state_actual,
1064 - lower_bound=lower_bound,
1065 - output_gate=output_gate if fuse_output_norm else None,
1066 - norm_weight=norm_weight if fuse_output_norm else None,
1067 - norm_eps=norm_eps,
1068 - )
1079 + if decode_backend == "flashinfer":
1080 + output = torch.empty(
1081 + 1,
1082 + num_seqs,
1083 + num_heads,
1084 + D,
1085 + dtype=torch.bfloat16,
1086 + device=DEVICE,
1087 + )
1088 +
1089 + actual = flashinfer_fused_kda_decode(
1090 + x=packed_x,
1091 + weight=fused_weight,
1092 + conv_state=conv_actual,
1093 + raw_gate=raw_g,
1094 + raw_beta=raw_beta,
1095 + A_log=A_log,
1096 + dt_bias=dt_bias,
1097 + state_indices=state_indices,
1098 + state=state_actual,
1099 + output_gate=output_gate,
1100 + norm_weight=norm_weight,
1101 + lower_bound=lower_bound,
1102 + norm_eps=norm_eps,
1103 + output=output,
1104 + )
1105 + else:
1106 + actual = ops.fused_kda_decode(
1107 + x=packed_x,
1108 + weight=fused_weight,
1109 + bias=None,
1110 + conv_state=conv_actual,
1111 + raw_g=raw_g,
1112 + raw_beta=raw_beta,
1113 + A_log=A_log,
1114 + dt_bias=dt_bias,
1115 + state_indices=state_indices,
1116 + state=state_actual,
1117 + lower_bound=lower_bound,
1118 + output_gate=output_gate if fuse_output_norm else None,
1119 + norm_weight=norm_weight if fuse_output_norm else None,
1120 + norm_eps=norm_eps,
1121 + )
1122
1123 torch.testing.assert_close(actual, expected, atol=3e-2, rtol=3e-2)
1124 torch.testing.assert_close(conv_actual, conv_ref, atol=0, rtol=0)
@@ -1080,17 +1133,171 @@ def test_fused_kda_decode_rejects_speculative_conv_state():
1133 num_spec=2,
1134 input_dtype=torch.bfloat16,
1135 conv_state_dtype=torch.bfloat16,
1136 + recurrent_state_dtype=torch.float32,
1137 + )
1138 +
1139 +
1140 +def _make_kda_prefill_inputs(
1141 + state_dtype: torch.dtype,
1142 + *,
1143 + lower_bound: float,
1144 +) -> SimpleNamespace:
1145 + B, T, H, D = 1, 48, 2, 128
1146 + torch.manual_seed(11)
1147 + q, k, v, raw_g = [
1148 + torch.randn(B, T, H, D, dtype=torch.bfloat16, device=DEVICE) for _ in range(4)
1149 + ]
1150 + raw_beta = torch.randn(B, T, H, dtype=torch.bfloat16, device=DEVICE)
1151 + A_log = torch.randn(H, dtype=torch.float32, device=DEVICE) * 0.5
1152 + dt_bias = torch.randn(H, D, dtype=torch.float32, device=DEVICE) * 0.1
1153 + initial_state = torch.randn(2, H, D, D, dtype=torch.float32, device=DEVICE).to(
1154 + state_dtype
1155 + )
1156 + cu_seqlens = torch.tensor([0, 17, T], dtype=torch.int32, device=DEVICE)
1157 + return SimpleNamespace(
1158 + q=q,
1159 + k=k,
1160 + v=v,
1161 + raw_g=raw_g,
1162 + raw_beta=raw_beta,
1163 + A_log=A_log,
1164 + dt_bias=dt_bias,
1165 + initial_state=initial_state,
1166 + cu_seqlens=cu_seqlens,
1167 + lower_bound=lower_bound,
1168 + )
1169 +
1170 +
1171 +def _require_kda_prefill_backend(
1172 + backend: str,
1173 + state_dtype: torch.dtype,
1174 + lower_bound: float,
1175 +) -> None:
1176 + if backend == "flashinfer":
1177 + supported = is_flashinfer_recurrent_kda_prefill_supported(
1178 + 128, torch.bfloat16, state_dtype, lower_bound
1179 + )
1180 + else:
1181 + assert backend == "flashkda"
1182 + supported = is_flashkda_supported(128, torch.bfloat16, state_dtype, lower_bound)
1183 + if not supported:
1184 + pytest.skip(f"{backend} KDA prefill is not supported on this platform")
1185 +
1186 +
1187 +def _run_kda_prefill_backend(
1188 + backend: str,
1189 + *,
1190 + q: torch.Tensor,
1191 + k: torch.Tensor,
1192 + v: torch.Tensor,
1193 + raw_g: torch.Tensor,
1194 + raw_beta: torch.Tensor,
1195 + A_log: torch.Tensor,
1196 + dt_bias: torch.Tensor,
1197 + initial_state: torch.Tensor,
1198 + cu_seqlens: torch.Tensor,
1199 + lower_bound: float,
1200 +) -> tuple[torch.Tensor, torch.Tensor]:
1201 + initial_state = initial_state.clone()
1202 + output = torch.empty_like(v)
1203 + if backend == "flashinfer":
1204 + flashinfer_query_start_loc = cu_seqlens.to(torch.int64)
1205 + seq_order = None
1206 + if q.shape[1] > initial_state.shape[0]:
1207 + seq_order = torch.argsort(
1208 + flashinfer_query_start_loc.diff(), descending=True
1209 + ).to(torch.int32)
1210 + return _flashinfer_kda_prefill(
1211 + q=q,
1212 + k=k,
1213 + v=v,
1214 + raw_g=raw_g,
1215 + raw_beta=raw_beta,
1216 + A_log=A_log,
1217 + dt_bias=dt_bias,
1218 + lower_bound=lower_bound,
1219 + initial_state=initial_state,
1220 + cu_seqlens=flashinfer_query_start_loc,
1221 + out=output,
1222 + seq_order=seq_order,
1223 + )
1224 +
1225 + assert backend == "flashkda"
1226 + import vllm._flashkda_C # noqa: F401
1227 +
1228 + final_state = torch.empty_like(initial_state)
1229 + workspace = torch.empty(
1230 + torch.ops._flashkda_C.get_workspace_size(
1231 + q.shape[1], q.shape[2], cu_seqlens.numel() - 1
1232 + ),
1233 + dtype=torch.uint8,
1234 + device=q.device,
1235 + )
1236 + return _flashkda_prefill(
1237 + q=q,
1238 + k=k,
1239 + v=v,
1240 + g=raw_g,
1241 + beta=raw_beta,
1242 + A_log=A_log,
1243 + dt_bias=dt_bias,
1244 + lower_bound=lower_bound,
1245 + initial_state=initial_state,
1246 + cu_seqlens=cu_seqlens,
1247 + out=output,
1248 + final_state=final_state,
1249 + workspace=workspace,
1250 + )
1251 +
1252 +
1253 +def _kda_prefill_reference(
1254 + inputs: SimpleNamespace,
1255 +) -> tuple[torch.Tensor, torch.Tensor]:
1256 + gate = inputs.lower_bound * torch.sigmoid(
1257 + inputs.A_log.exp()[None, None, :, None]
1258 + * (inputs.raw_g.float() + inputs.dt_bias[None, None])
1259 + )
1260 + beta = inputs.raw_beta.float().sigmoid()
1261 + q_norm = l2norm_fwd(inputs.q.contiguous())
1262 + k_norm = l2norm_fwd(inputs.k.contiguous())
1263 + expected_outputs = []
1264 + expected_states = []
1265 + for i, (start, end) in enumerate(
1266 + zip(inputs.cu_seqlens[:-1].tolist(), inputs.cu_seqlens[1:].tolist())
1267 + ):
1268 + output, final_state = naive_recurrent_kda(
1269 + q_norm[:, start:end],
1270 + k_norm[:, start:end],
1271 + inputs.v[:, start:end],
1272 + gate[:, start:end],
1273 + beta[:, start:end],
1274 + initial_state=inputs.initial_state[i : i + 1].transpose(-1, -2),
1275 + output_final_state=True,
1276 + )
1277 + assert final_state is not None
1278 + expected_outputs.append(output)
1279 + expected_states.append(final_state)
1280 + return (
1281 + torch.cat(expected_outputs, dim=1),
1282 + torch.cat(expected_states).transpose(-1, -2).contiguous(),
1283 )
1284
1285
1286 +@pytest.mark.parametrize(
1287 + ("backend", "state_dtype"),
1288 + [
1289 + pytest.param("flashkda", torch.float32, id="flashkda"),
1290 + pytest.param("flashinfer", torch.bfloat16, id="flashinfer"),
1291 + ],
1292 +)
1293 @torch.inference_mode()
1087 -def test_flashkda_near_collinear_keys_remain_finite():
1294 +def test_kda_prefill_near_collinear_keys_remain_finite(
1295 + backend: str,
1296 + state_dtype: torch.dtype,
1297 +):
1298 """Guard against unstable inversion of near-collinear key blocks."""
1299 lower_bound = -5.0
1090 - if not is_flashkda_supported(128, torch.bfloat16, lower_bound):
1091 - pytest.skip("FlashKDA is not supported on this platform")
1092 -
1093 - import vllm._flashkda_C # noqa: F401
1300 + _require_kda_prefill_backend(backend, state_dtype, lower_bound)
1301
1302 T, H, D = 16384, 1, 128
1303 torch.manual_seed(0)
@@ -1102,81 +1309,131 @@ def test_flashkda_near_collinear_keys_remain_finite():
1309 raw_beta = torch.full((1, T, H), 8.0, dtype=qk.dtype, device=DEVICE)
1310 A_log = torch.zeros(H, dtype=torch.float32, device=DEVICE)
1311 dt_bias = torch.zeros(H, D, dtype=torch.float32, device=DEVICE)
1105 - initial_state = torch.zeros(1, H, D, D, dtype=torch.float32, device=DEVICE)
1106 - final_state = torch.empty_like(initial_state)
1107 - output = torch.empty_like(value)
1312 + initial_state = torch.zeros(1, H, D, D, dtype=state_dtype, device=DEVICE)
1313 cu_seqlens = torch.tensor([0, T], dtype=torch.int32, device=DEVICE)
1109 - workspace = torch.empty(
1110 - torch.ops._flashkda_C.get_workspace_size(T, H, 1),
1111 - dtype=torch.uint8,
1112 - device=DEVICE,
1113 - )
1114 -
1115 - torch.ops._flashkda_C.fwd(
1116 - qk,
1117 - qk,
1118 - value,
1119 - raw_gate,
1120 - raw_beta,
1121 - D**-0.5,
1122 - output,
1123 - workspace,
1124 - A_log,
1125 - dt_bias,
1126 - lower_bound,
1127 - initial_state,
1128 - final_state,
1129 - cu_seqlens,
1314 + output, final_state = _run_kda_prefill_backend(
1315 + backend,
1316 + q=qk,
1317 + k=qk,
1318 + v=value,
1319 + raw_g=raw_gate,
1320 + raw_beta=raw_beta,
1321 + A_log=A_log,
1322 + dt_bias=dt_bias,
1323 + initial_state=initial_state,
1324 + cu_seqlens=cu_seqlens,
1325 + lower_bound=lower_bound,
1326 )
1327
1328 assert torch.isfinite(output).all()
1329 assert torch.isfinite(final_state).all()
1330
1331
1332 +@pytest.mark.parametrize(
1333 + ("backend", "state_dtype", "tolerance"),
1334 + [
1335 + pytest.param("flashinfer", torch.bfloat16, 0.03, id="flashinfer-bf16"),
1336 + pytest.param("flashkda", torch.bfloat16, 0.03, id="flashkda-bf16"),
1337 + pytest.param("flashkda", torch.float32, 0.01, id="flashkda-fp32"),
1338 + ],
1339 +)
1340 @torch.inference_mode()
1137 -def test_flashkda_correctness():
1138 - if not is_flashkda_supported(128, torch.bfloat16, -3.0):
1139 - pytest.skip("FlashKDA is not supported on this platform")
1341 +def test_kda_prefill_correctness(
1342 + backend: str,
1343 + state_dtype: torch.dtype,
1344 + tolerance: float,
1345 +):
1346 + lower_bound = -5.0
1347 + _require_kda_prefill_backend(backend, state_dtype, lower_bound)
1348 + inputs = _make_kda_prefill_inputs(state_dtype, lower_bound=lower_bound)
1349 + expected_out, expected_state = _kda_prefill_reference(inputs)
1350 + actual_out, actual_state = _run_kda_prefill_backend(
1351 + backend,
1352 + **vars(inputs),
1353 + )
1354
1141 - import vllm._flashkda_C # noqa: F401
1355 + assert_close("o", expected_out, actual_out, tolerance)
1356 + assert_close("ht", expected_state, actual_state, tolerance)
1357
1143 - B, T, H, D = 1, 48, 2, 128
1144 - torch.manual_seed(11)
1358 +
1359 +@torch.inference_mode()
1360 +def test_flashinfer_kda_prefill_breakable_graph_cross_stream():
1361 + if not is_flashinfer_recurrent_kda_prefill_supported(
1362 + 128,
1363 + torch.bfloat16,
1364 + torch.bfloat16,
1365 + -5.0,
1366 + ):
1367 + pytest.skip("FlashInfer KDA prefill is not supported on this platform")
1368 +
1369 + B, T, H, D = 1, 8, 12, 128
1370 q, k, v, raw_g = [
1371 torch.randn(B, T, H, D, dtype=torch.bfloat16, device=DEVICE) for _ in range(4)
1372 ]
1148 - beta_logits = torch.randn(B, T, H, dtype=torch.bfloat16, device=DEVICE)
1149 - A_log = torch.randn(H, dtype=torch.float32, device=DEVICE) * 0.5
1150 - dt_bias = torch.randn(H, D, dtype=torch.float32, device=DEVICE) * 0.1
1151 - initial_state = torch.randn(2, H, D, D, dtype=torch.float32, device=DEVICE)
1152 - cu_seqlens = torch.tensor([0, 17, T], dtype=torch.int32, device=DEVICE)
1373 + raw_beta = torch.randn(B, T, H, dtype=torch.bfloat16, device=DEVICE)
1374 + initial_state = torch.randn(
1375 + B,
1376 + H,
1377 + D,
1378 + D,
1379 + dtype=torch.bfloat16,
1380 + device=DEVICE,
1381 + )
1382 + kwargs = {
1383 + "q": q,
1384 + "k": k,
1385 + "v": v,
1386 + "raw_g": raw_g,
1387 + "raw_beta": raw_beta,
1388 + "A_log": torch.randn(H, dtype=torch.float32, device=DEVICE),
1389 + "dt_bias": torch.randn(H, D, dtype=torch.float32, device=DEVICE),
1390 + "lower_bound": -5.0,
1391 + "initial_state": initial_state,
1392 + "cu_seqlens": torch.tensor([0, T], dtype=torch.int64, device=DEVICE),
1393 + "out": torch.empty_like(q),
1394 + "seq_order": torch.zeros(B, dtype=torch.int32, device=DEVICE),
1395 + }
1396 +
1397 + capture_stream = torch.Stream(device=DEVICE)
1398 + original_stream = torch.accelerator.current_stream()
1399 + torch.accelerator.set_stream(capture_stream)
1400 + try:
1401 + graph_value = torch.zeros(1, device=DEVICE)
1402 + capture = BreakableCUDAGraphCapture()
1403 + with capture:
1404 + graph_value.add_(1)
1405 + capture.add_eager(lambda: _flashinfer_kda_prefill(**kwargs))
1406 + graph_value.add_(1)
1407 + capture_stream.synchronize()
1408 + finally:
1409 + torch.accelerator.set_stream(original_stream)
1410 +
1411 + torch.testing.assert_close(graph_value, torch.zeros_like(graph_value))
1412 + capture.replay()
1413 + torch.accelerator.synchronize()
1414 + torch.testing.assert_close(graph_value, torch.full_like(graph_value, 2))
1415 +
1416 +
1417 +@torch.inference_mode()
1418 +def test_flashkda_checkpoint_correctness():
1419 lower_bound = -3.0
1420 + _require_kda_prefill_backend("flashkda", torch.float32, lower_bound)
1421 +
1422 + import vllm._flashkda_C # noqa: F401
1423
1424 + inputs = _make_kda_prefill_inputs(torch.float32, lower_bound=lower_bound)
1425 + q, k, v = inputs.q, inputs.k, inputs.v
1426 + raw_g, raw_beta = inputs.raw_g, inputs.raw_beta
1427 + A_log, dt_bias = inputs.A_log, inputs.dt_bias
1428 + initial_state, cu_seqlens = inputs.initial_state, inputs.cu_seqlens
1429 + _, T, H, D = q.shape
1430 + expected_out, expected_state = _kda_prefill_reference(inputs)
1431 gate = lower_bound * torch.sigmoid(
1432 A_log.exp()[None, None, :, None] * (raw_g.float() + dt_bias[None, None, :, :])
1433 )
1158 - beta = beta_logits.float().sigmoid()
1434 + beta = raw_beta.float().sigmoid()
1435 q_norm = l2norm_fwd(q.contiguous())
1436 k_norm = l2norm_fwd(k.contiguous())
1161 -
1162 - expected_outputs = []
1163 - expected_states = []
1164 - for i, (start, end) in enumerate(
1165 - zip(cu_seqlens[:-1].tolist(), cu_seqlens[1:].tolist())
1166 - ):
1167 - output, final_state = naive_recurrent_kda(
1168 - q_norm[:, start:end],
1169 - k_norm[:, start:end],
1170 - v[:, start:end],
1171 - gate[:, start:end],
1172 - beta[:, start:end],
1173 - initial_state=initial_state[i].transpose(-1, -2),
1174 - output_final_state=True,
1175 - )
1176 - expected_outputs.append(output)
1177 - expected_states.append(final_state)
1178 - expected_out = torch.cat(expected_outputs, dim=1)
1179 - expected_state = torch.cat(expected_states).transpose(-1, -2).contiguous()
1437 _, expected_checkpoint = naive_recurrent_kda(
1438 q_norm[:, :16],
1439 k_norm[:, :16],
@@ -1189,33 +1446,11 @@ def test_flashkda_correctness():
1446 assert expected_checkpoint is not None
1447 expected_checkpoint = expected_checkpoint.transpose(-1, -2).contiguous()
1448
1192 - actual_out = torch.empty_like(v)
1193 - actual_state = torch.empty_like(initial_state)
1449 workspace = torch.empty(
1450 torch.ops._flashkda_C.get_workspace_size(T, H, cu_seqlens.numel() - 1),
1451 dtype=torch.uint8,
1452 device=DEVICE,
1453 )
1199 - torch.ops._flashkda_C.fwd(
1200 - q,
1201 - k,
1202 - v,
1203 - raw_g,
1204 - beta_logits,
1205 - D**-0.5,
1206 - actual_out,
1207 - workspace,
1208 - A_log,
1209 - dt_bias,
1210 - lower_bound,
1211 - initial_state,
1212 - actual_state,
1213 - cu_seqlens,
1214 - )
1215 -
1216 - assert_close("o", expected_out, actual_out, 0.01)
1217 - assert_close("ht", expected_state, actual_state, 0.01)
1218 -
1454 checkpoint_out = torch.empty_like(v)
1455 checkpoint_final_state = torch.empty_like(initial_state)
1456 checkpoint_state = torch.empty_like(initial_state)
@@ -1225,7 +1460,7 @@ def test_flashkda_correctness():
1460 k=k,
1461 v=v,
1462 g=raw_g,
1228 - beta=beta_logits,
1463 + beta=raw_beta,
1464 A_log=A_log,
1465 dt_bias=dt_bias,
1466 lower_bound=lower_bound,
tests/models/kimi_k3/test_kda_metadata.py
+4 -1
@@ -49,6 +49,8 @@ PRUNED_METADATA_FIELDS = {
49 "prefill_state_indices",
50 "prefill_has_initial_state",
51 "spec_sequence_masks",
52 + "flashinfer_prefill_query_start_loc",
53 + "flashinfer_prefill_seq_order",
54 }
55
56
@@ -59,10 +61,10 @@ def _assert_matches_shared_gdn(
61 assert actual.recoverssm_context is None
62 for field in fields(GDNAttentionMetadata):
63 actual_value = getattr(actual, field.name)
62 - expected_value = getattr(reference, field.name)
64 if field.name in PRUNED_METADATA_FIELDS:
65 assert actual_value is None
66 continue
67 + expected_value = getattr(reference, field.name)
68 if (
69 field.name in {"spec_token_indx", "non_spec_token_indx"}
70 and actual.num_spec_decodes > 0
@@ -157,6 +159,7 @@ def test_kda_recoverssm_startup_metadata_flow_without_model(monkeypatch):
159 ),
160 cache_config=SimpleNamespace(
161 mamba_cache_dtype="auto",
162 + mamba_ssm_cache_dtype="auto",
163 use_kda_recoverssm=True,
164 ),
165 parallel_config=SimpleNamespace(tensor_parallel_size=1),
vllm/engine/arg_utils.py
+14 -2
@@ -772,7 +772,10 @@ class EngineArgs:
772
773 fail_on_environ_validation: bool = False
774 gdn_prefill_backend: Literal["flashinfer", "triton", "cutedsl"] | None = None
775 - kda_prefill_backend: Literal["auto", "triton", "flashkda"] | None = None
775 + kda_prefill_backend: Literal["auto", "triton", "flashkda", "flashinfer"] | None = (
776 + None
777 + )
778 + kda_decode_backend: Literal["auto", "native", "flashinfer", "triton"] | None = None
779
780 def __post_init__(self):
781 # support `EngineArgs(compilation_config={...})`
@@ -1747,10 +1750,17 @@ class EngineArgs:
1750 parser.add_argument(
1751 "--kda-prefill-backend",
1752 dest="kda_prefill_backend",
1750 - choices=["auto", "triton", "flashkda"],
1753 + choices=["auto", "triton", "flashkda", "flashinfer"],
1754 default=None,
1755 help="Select KDA prefill backend.",
1756 )
1757 + parser.add_argument(
1758 + "--kda-decode-backend",
1759 + dest="kda_decode_backend",
1760 + choices=["auto", "native", "flashinfer", "triton"],
1761 + default=None,
1762 + help="Select KDA decode backend.",
1763 + )
1764 return parser
1765
1766 @classmethod
@@ -2567,6 +2577,8 @@ class EngineArgs:
2577 self.additional_config["gdn_prefill_backend"] = self.gdn_prefill_backend
2578 if self.kda_prefill_backend is not None:
2579 self.additional_config["kda_prefill_backend"] = self.kda_prefill_backend
2580 + if self.kda_decode_backend is not None:
2581 + self.additional_config["kda_decode_backend"] = self.kda_decode_backend
2582
2583 config = VllmConfig(
2584 model_config=model_config,
vllm/model_executor/layers/mamba/mamba_utils.py
+12 -2
@@ -134,9 +134,19 @@ class MambaStateDtypeCalculator:
134 cls,
135 model_dtype: ModelDType | torch.dtype,
136 mamba_cache_dtype: MambaDType,
137 + mamba_ssm_cache_dtype: MambaDType = "auto",
138 ) -> tuple[torch.dtype, torch.dtype]:
138 - state_dtype = get_kv_cache_torch_dtype(mamba_cache_dtype, model_dtype)
139 - return (state_dtype, torch.float32)
139 + conv_state_dtype = get_kv_cache_torch_dtype(mamba_cache_dtype, model_dtype)
140 + if mamba_ssm_cache_dtype == "auto":
141 + recurrent_state_dtype = torch.float32
142 + elif mamba_ssm_cache_dtype in ("float32", "bfloat16"):
143 + recurrent_state_dtype = STR_DTYPE_TO_TORCH_DTYPE[mamba_ssm_cache_dtype]
144 + else:
145 + raise ValueError(
146 + "KDA recurrent state supports only auto, float32, and bfloat16; "
147 + f"got {mamba_ssm_cache_dtype}"
148 + )
149 + return (conv_state_dtype, recurrent_state_dtype)
150
151 @classmethod
152 def append_kda_recoverssm_record(
vllm/models/kimi_k3/nvidia/kda.py
+284 -36
@@ -50,6 +50,12 @@ from vllm.platforms import current_platform
50 from vllm.third_party.flash_linear_attention.ops.kda import FusedRMSNormGated
51 from vllm.transformers_utils.configs.kimi_linear import KimiLinearConfig
52 from vllm.triton_utils import tl, triton
53 +from vllm.utils.flashinfer import (
54 + flashinfer_fused_kda_decode,
55 + flashinfer_recurrent_kda,
56 + has_flashinfer_fused_kda_decode,
57 + has_flashinfer_recurrent_kda,
58 +)
59 from vllm.v1.attention.backend import AttentionBackend
60 from vllm.v1.attention.backends.utils import NULL_BLOCK_ID
61 from vllm.v1.kv_cache_interface import MambaSpec
@@ -155,6 +161,7 @@ def is_fused_kda_decode_supported(
161 num_spec: int,
162 input_dtype: torch.dtype,
163 conv_state_dtype: torch.dtype,
164 + recurrent_state_dtype: torch.dtype,
165 ) -> bool:
166 # The fused kernel handles both conv-state cache layouts (SD and DS); the
167 # inner strides are selected from the tensor at launch time.
@@ -165,6 +172,7 @@ def is_fused_kda_decode_supported(
172 or num_spec != 0
173 or input_dtype != torch.bfloat16
174 or conv_state_dtype != torch.bfloat16
175 + or recurrent_state_dtype != torch.float32
176 or not hasattr(torch.ops._C, "fused_kda_decode")
177 ):
178 return False
@@ -176,9 +184,93 @@ def is_fused_kda_decode_supported(
184 )
185
186
187 +def is_flashinfer_fused_kda_decode_supported(
188 + num_heads: int,
189 + head_dim: int,
190 + conv_width: int,
191 + num_spec: int,
192 + input_dtype: torch.dtype,
193 + conv_state_dtype: torch.dtype,
194 + recurrent_state_dtype: torch.dtype,
195 +) -> bool:
196 + if not has_flashinfer_fused_kda_decode():
197 + return False
198 + capability = current_platform.get_device_capability()
199 + if capability is None:
200 + return False
201 + compute_capability = (capability.major, capability.minor)
202 + return (
203 + compute_capability in ((10, 0), (10, 3))
204 + and num_heads in (12, 24, 32, 48, 96)
205 + and head_dim == 128
206 + and conv_width == 4
207 + and num_spec == 0
208 + and input_dtype == torch.bfloat16
209 + and conv_state_dtype == torch.bfloat16
210 + and recurrent_state_dtype in (torch.float32, torch.bfloat16)
211 + and not is_conv_state_dim_first()
212 + )
213 +
214 +
215 +def resolve_kda_decode_backend(
216 + backend: str,
217 + num_heads: int,
218 + head_dim: int,
219 + conv_width: int,
220 + num_spec: int,
221 + input_dtype: torch.dtype,
222 + conv_state_dtype: torch.dtype,
223 + recurrent_state_dtype: torch.dtype,
224 +) -> str:
225 + if backend not in ("auto", "native", "flashinfer", "triton"):
226 + raise ValueError(f"Unsupported KDA decode backend: {backend}")
227 + if backend == "triton":
228 + return "triton"
229 +
230 + native_supported = is_fused_kda_decode_supported(
231 + num_heads,
232 + head_dim,
233 + conv_width,
234 + num_spec,
235 + input_dtype,
236 + conv_state_dtype,
237 + recurrent_state_dtype,
238 + )
239 + if native_supported and backend in ("auto", "native"):
240 + logger.info_once("Using native fused KDA decode backend.")
241 + return "native"
242 + if backend == "native":
243 + raise RuntimeError(
244 + "Native fused KDA decode requires a supported CUDA architecture, "
245 + "bfloat16 activations and convolution state, float32 recurrent "
246 + "state, head_dim=128, convolution width 4, and no speculation."
247 + )
248 +
249 + flashinfer_supported = is_flashinfer_fused_kda_decode_supported(
250 + num_heads,
251 + head_dim,
252 + conv_width,
253 + num_spec,
254 + input_dtype,
255 + conv_state_dtype,
256 + recurrent_state_dtype,
257 + )
258 + if flashinfer_supported and backend == "flashinfer":
259 + logger.info_once("Using FlashInfer fused KDA decode backend.")
260 + return "flashinfer"
261 + if backend == "flashinfer":
262 + raise RuntimeError(
263 + "FlashInfer fused KDA decode requires CUDA SM100 or SM103, "
264 + "bfloat16 activations and convolution state, bfloat16 or float32 "
265 + "recurrent state, head_dim=128, convolution width 4, no speculation."
266 + )
267 + return "triton"
268 +
269 +
270 def is_flashkda_supported(
271 head_dim: int,
181 - dtype: torch.dtype,
272 + input_dtype: torch.dtype,
273 + recurrent_state_dtype: torch.dtype,
274 lower_bound: float | None,
275 ) -> bool:
276 if not current_platform.is_cuda():
@@ -188,7 +280,29 @@ def is_flashkda_supported(
280 capability is not None
281 and capability.major in (9, 10, 12)
282 and head_dim == 128
191 - and dtype == torch.bfloat16
283 + and input_dtype == torch.bfloat16
284 + and recurrent_state_dtype in (torch.bfloat16, torch.float32)
285 + and lower_bound is not None
286 + )
287 +
288 +
289 +def is_flashinfer_recurrent_kda_prefill_supported(
290 + head_dim: int,
291 + input_dtype: torch.dtype,
292 + recurrent_state_dtype: torch.dtype,
293 + lower_bound: float | None,
294 +) -> bool:
295 + if not current_platform.is_cuda() or not has_flashinfer_recurrent_kda():
296 + return False
297 + capability = current_platform.get_device_capability()
298 + if capability is None:
299 + return False
300 + compute_capability = (capability.major, capability.minor)
301 + return (
302 + compute_capability in ((10, 0), (10, 3))
303 + and head_dim == 128
304 + and input_dtype == torch.bfloat16
305 + and recurrent_state_dtype == torch.bfloat16
306 and lower_bound is not None
307 )
308
@@ -237,6 +351,44 @@ def _flashkda_prefill(
351 return out, final_state
352
353
354 +def _flashinfer_kda_prefill(
355 + q: torch.Tensor,
356 + k: torch.Tensor,
357 + v: torch.Tensor,
358 + raw_g: torch.Tensor,
359 + raw_beta: torch.Tensor,
360 + A_log: torch.Tensor,
361 + dt_bias: torch.Tensor,
362 + lower_bound: float,
363 + initial_state: torch.Tensor,
364 + cu_seqlens: torch.Tensor,
365 + out: torch.Tensor,
366 + seq_order: torch.Tensor | None = None,
367 + prefill_workspace: object | None = None,
368 +) -> tuple[torch.Tensor, torch.Tensor]:
369 + output, _ = flashinfer_recurrent_kda(
370 + q=q.contiguous(),
371 + k=k.contiguous(),
372 + v=v.contiguous(),
373 + g=raw_g.contiguous(),
374 + beta=raw_beta.contiguous(),
375 + A_log=A_log.contiguous(),
376 + dt_bias=dt_bias.contiguous(),
377 + scale=q.shape[-1] ** -0.5,
378 + initial_state=initial_state.contiguous(),
379 + output_final_state=False,
380 + use_qk_l2norm_in_kernel=True,
381 + use_gate_in_kernel=True,
382 + lower_bound=lower_bound,
383 + cu_seqlens=cu_seqlens.contiguous(),
384 + output=out,
385 + beta_is_logit=True,
386 + seq_order=seq_order,
387 + prefill_workspace=prefill_workspace,
388 + )
389 + return output, initial_state
390 +
391 +
392 @triton.jit
393 def _store_cache_checkpoints_kernel(
394 x_ptr,
@@ -303,18 +455,40 @@ def _store_cache_checkpoints_kernel(
455 def resolve_kda_prefill_backend(
456 backend: str,
457 head_dim: int,
306 - dtype: torch.dtype,
458 + input_dtype: torch.dtype,
459 + recurrent_state_dtype: torch.dtype,
460 lower_bound: float | None,
461 ) -> str:
309 - if backend not in ("auto", "triton", "flashkda"):
462 + if backend not in ("auto", "triton", "flashkda", "flashinfer"):
463 raise ValueError(f"Unsupported KDA prefill backend: {backend}")
311 - supported = is_flashkda_supported(head_dim, dtype, lower_bound)
312 - if backend == "flashkda" and not supported:
464 + flashinfer_supported = is_flashinfer_recurrent_kda_prefill_supported(
465 + head_dim,
466 + input_dtype,
467 + recurrent_state_dtype,
468 + lower_bound,
469 + )
470 + flashkda_supported = is_flashkda_supported(
471 + head_dim,
472 + input_dtype,
473 + recurrent_state_dtype,
474 + lower_bound,
475 + )
476 + if backend == "flashinfer" and not flashinfer_supported:
477 raise RuntimeError(
314 - "FlashKDA requires CUDA SM90/SM10x/SM12x, bfloat16, "
315 - "head_dim=128, and a bounded KDA gate."
478 + "FlashInfer KDA prefill requires CUDA SM100 or SM103, bfloat16 "
479 + "activations and recurrent state, head_dim=128, a bounded KDA "
480 + "gate, and flashinfer-python 0.6.18 or newer."
481 )
317 - if supported and backend != "triton":
482 + if backend == "flashkda" and not flashkda_supported:
483 + raise RuntimeError(
484 + "FlashKDA requires CUDA SM90/SM10x/SM12x, bfloat16 input, "
485 + "bfloat16 or float32 recurrent state, head_dim=128, and a bounded "
486 + "KDA gate."
487 + )
488 + if flashinfer_supported and backend == "flashinfer":
489 + logger.info_once("Using FlashInfer KDA prefill backend.")
490 + return "flashinfer"
491 + if flashkda_supported and backend in ("auto", "flashkda"):
492 logger.info_once("Using FlashKDA KDA prefill backend.")
493 return "flashkda"
494 return "triton"
@@ -369,7 +543,9 @@ class KimiK3DeltaAttention(GatedDeltaNetAttention):
543 if self.model_config is None or self.cache_config is None:
544 raise ValueError("model_config and cache_config must be set")
545 base_dtypes = MambaStateDtypeCalculator.kda_state_dtype(
372 - self.model_config.dtype, self.cache_config.mamba_cache_dtype
546 + self.model_config.dtype,
547 + self.cache_config.mamba_cache_dtype,
548 + self.cache_config.mamba_ssm_cache_dtype,
549 )
550 if self.cache_config.use_kda_recoverssm:
551 return MambaStateDtypeCalculator.append_kda_recoverssm_record(
@@ -486,17 +662,25 @@ class KimiK3DeltaAttention(GatedDeltaNetAttention):
662 self.conv1d.weight.data = self.conv1d.weight.data.unsqueeze(1)
663 # Keep a width-major copy for fused decode without changing the layout
664 # consumed by the prefill and fallback decode kernels.
489 - conv_state_dtype = self.get_state_dtype()[0]
490 - decode_conv1d_weight = None
491 - if is_fused_kda_decode_supported(
665 + conv_state_dtype, recurrent_state_dtype = self.get_state_dtype()[:2]
666 + additional_config = vllm_config.additional_config
667 + decode_backend = (
668 + additional_config.get("kda_decode_backend", "auto")
669 + if isinstance(additional_config, dict)
670 + else "auto"
671 + )
672 + self.kda_decode_backend = resolve_kda_decode_backend(
673 + decode_backend,
674 self.local_num_heads,
675 self.head_dim,
676 self.conv_size,
677 self.num_spec,
678 vllm_config.model_config.dtype,
679 conv_state_dtype,
498 - ):
499 - logger.info_once("Fused KDA decode kernel (conv+KDA+norm) is enabled.")
680 + recurrent_state_dtype,
681 + )
682 + decode_conv1d_weight = None
683 + if self.kda_decode_backend != "triton":
684 decode_conv1d_weight = torch.empty(
685 3,
686 self.conv_size,
@@ -533,7 +717,6 @@ class KimiK3DeltaAttention(GatedDeltaNetAttention):
717 f"Got {self.gate_lower_bound}."
718 )
719
536 - additional_config = vllm_config.additional_config
720 backend = (
721 additional_config.get("kda_prefill_backend", "auto")
722 if isinstance(additional_config, dict)
@@ -543,11 +726,15 @@ class KimiK3DeltaAttention(GatedDeltaNetAttention):
726 backend,
727 self.head_dim,
728 vllm_config.model_config.dtype,
729 + recurrent_state_dtype,
730 self.gate_lower_bound,
731 )
732 self._flashkda_buffer_specs: (
733 tuple[tuple[tuple[int, ...], torch.dtype], ...] | None
734 ) = None
735 + self._flashinfer_kda_output_spec: tuple[tuple[int, ...], torch.dtype] | None = (
736 + None
737 + )
738 if self.kda_prefill_backend == "flashkda":
739 T = vllm_config.scheduler_config.max_num_batched_tokens
740 N = vllm_config.scheduler_config.max_num_seqs
@@ -561,6 +748,13 @@ class KimiK3DeltaAttention(GatedDeltaNetAttention):
748 ((N, H, D, D), self.get_state_dtype()[1]),
749 ((workspace_size,), torch.uint8),
750 )
751 + elif self.kda_prefill_backend == "flashinfer":
752 + T = vllm_config.scheduler_config.max_num_batched_tokens
753 + H, D = self.local_num_heads, self.head_dim
754 + self._flashinfer_kda_output_spec = (
755 + (1, T, H, D),
756 + self.model_config.dtype,
757 + )
758
759 self.o_norm = FusedRMSNormGated(self.head_dim, activation="sigmoid")
760 decode_norm_weight = None
@@ -725,30 +919,50 @@ class KimiK3DeltaAttention(GatedDeltaNetAttention):
919 conv_state = conv_state.transpose(-1, -2)
920
921 if (
728 - self.decode_conv1d_weight is not None
922 + self.kda_decode_backend != "triton"
923 + and self.decode_conv1d_weight is not None
924 and self.decode_norm_weight is not None
925 and not has_spec_decode
926 and m.num_prefills == 0
927 and m.num_decodes > 0
928 ):
929 assert non_spec_state_indices_tensor is not None
735 - ops.fused_kda_decode(
736 - x=mixed_qkv,
737 - weight=self.decode_conv1d_weight,
738 - bias=self.conv1d.bias,
739 - conv_state=conv_state,
740 - raw_g=g1,
741 - raw_beta=beta,
742 - A_log=self.A_log,
743 - dt_bias=self.dt_bias,
744 - state_indices=non_spec_state_indices_tensor[:num_actual_tokens],
745 - state=recurrent_state,
746 - out=core_attn_out[:, :num_actual_tokens],
747 - lower_bound=self.gate_lower_bound,
748 - output_gate=g2[:num_actual_tokens],
749 - norm_weight=self.decode_norm_weight,
750 - norm_eps=self.o_norm.eps,
751 - )
930 + state_indices = non_spec_state_indices_tensor[:num_actual_tokens]
931 + if self.kda_decode_backend == "flashinfer":
932 + flashinfer_fused_kda_decode(
933 + x=mixed_qkv,
934 + weight=self.decode_conv1d_weight,
935 + conv_state=conv_state,
936 + raw_gate=g1,
937 + raw_beta=beta,
938 + A_log=self.A_log,
939 + dt_bias=self.dt_bias,
940 + state_indices=state_indices,
941 + state=recurrent_state,
942 + output_gate=g2[:num_actual_tokens],
943 + norm_weight=self.decode_norm_weight,
944 + lower_bound=self.gate_lower_bound,
945 + norm_eps=self.o_norm.eps,
946 + output=core_attn_out[:, :num_actual_tokens],
947 + )
948 + else:
949 + ops.fused_kda_decode(
950 + x=mixed_qkv,
951 + weight=self.decode_conv1d_weight,
952 + bias=self.conv1d.bias,
953 + conv_state=conv_state,
954 + raw_g=g1,
955 + raw_beta=beta,
956 + A_log=self.A_log,
957 + dt_bias=self.dt_bias,
958 + state_indices=state_indices,
959 + state=recurrent_state,
960 + out=core_attn_out[:, :num_actual_tokens],
961 + lower_bound=self.gate_lower_bound,
962 + output_gate=g2[:num_actual_tokens],
963 + norm_weight=self.decode_norm_weight,
964 + norm_eps=self.o_norm.eps,
965 + )
966 return
967
968 conv_weights = self.conv1d.weight.view(
@@ -991,6 +1205,35 @@ class KimiK3DeltaAttention(GatedDeltaNetAttention):
1205 final_state=final_state[: initial_state.shape[0]],
1206 workspace=workspace,
1207 )
1208 + elif self.kda_prefill_backend == "flashinfer":
1209 + assert self.gate_lower_bound is not None
1210 + assert m.flashinfer_prefill_query_start_loc is not None
1211 + if q_ns.shape[1] > initial_state.shape[0]:
1212 + assert m.flashinfer_prefill_seq_order is not None
1213 + flashinfer_out = core_attn_out[:, : q_ns.shape[1]]
1214 + if has_spec_decode:
1215 + assert self._flashinfer_kda_output_spec is not None
1216 + (workspace_out,) = current_workspace_manager().get_simultaneous(
1217 + self._flashinfer_kda_output_spec
1218 + )
1219 + flashinfer_out = workspace_out[:, : q_ns.shape[1]]
1220 + (
1221 + core_attn_out_non_spec,
1222 + last_recurrent_state,
1223 + ) = _flashinfer_kda_prefill(
1224 + q=q_ns,
1225 + k=k_ns,
1226 + v=v_ns,
1227 + raw_g=g1_ns,
1228 + raw_beta=beta_ns,
1229 + A_log=self.A_log,
1230 + dt_bias=self.dt_bias,
1231 + lower_bound=self.gate_lower_bound,
1232 + initial_state=initial_state,
1233 + cu_seqlens=m.flashinfer_prefill_query_start_loc,
1234 + out=flashinfer_out,
1235 + seq_order=m.flashinfer_prefill_seq_order,
1236 + )
1237 else:
1238 (
1239 core_attn_out_non_spec,
@@ -1009,7 +1252,9 @@ class KimiK3DeltaAttention(GatedDeltaNetAttention):
1252 use_qk_l2norm_in_kernel=True,
1253 cu_seqlens=non_spec_query_start_loc,
1254 )
1012 - recurrent_state[non_spec_state_indices_tensor] = last_recurrent_state
1255 + recurrent_state[non_spec_state_indices_tensor] = (
1256 + last_recurrent_state.to(recurrent_state.dtype)
1257 + )
1258 else:
1259 # Pure non-speculative decode.
1260 assert non_spec_state_indices_tensor is not None
@@ -1046,7 +1291,10 @@ class KimiK3DeltaAttention(GatedDeltaNetAttention):
1291 core_attn_out.index_copy_(1, spec_token_indx, core_attn_out_spec)
1292 core_attn_out.index_copy_(1, non_spec_token_indx, core_attn_out_non_spec)
1293 elif core_attn_out_non_spec is not None:
1049 - if self.kda_prefill_backend != "flashkda" or m.num_prefills == 0:
1294 + if (
1295 + self.kda_prefill_backend not in ("flashkda", "flashinfer")
1296 + or m.num_prefills == 0
1297 + ):
1298 # TODO: decode kernels write directly to core_attn_out
1299 core_attn_out[0, :num_actual_tokens] = core_attn_out_non_spec[
1300 0, :num_actual_tokens
vllm/models/kimi_k3/nvidia/kda_metadata.py
+23
@@ -270,6 +270,8 @@ class KDACheckpointMetadata:
270
271 @dataclass
272 class KimiK3KDAMetadata(GDNAttentionMetadata, RecoverSSMMetadata):
273 + flashinfer_prefill_query_start_loc: torch.Tensor | None = None
274 + flashinfer_prefill_seq_order: torch.Tensor | None = None
275 recoverssm_commit: KDARecoverSSMCommitMetadata | None = None
276 recoverssm_context: "KDARecoverSSMCommitContext | None" = field(
277 default=None, repr=False, compare=False
@@ -318,6 +320,11 @@ class KimiK3KDAMetadataBuilder(GDNAttentionMetadataBuilder):
320 device: torch.device,
321 ) -> None:
322 super().__init__(kv_cache_spec, layer_names, vllm_config, device)
323 + additional_config = vllm_config.additional_config
324 + self.use_flashinfer_prefill = (
325 + isinstance(additional_config, dict)
326 + and additional_config.get("kda_prefill_backend") == "flashinfer"
327 + )
328 self.use_recoverssm = vllm_config.cache_config.use_kda_recoverssm
329 self.spec_state_slots = 1 if self.use_recoverssm else self.num_spec + 1
330 self.recoverssm_num_accepted_tokens: torch.Tensor | None = None
@@ -721,6 +728,20 @@ class KimiK3KDAMetadataBuilder(GDNAttentionMetadataBuilder):
728 align=align,
729 )
730
731 + flashinfer_prefill_query_start_loc = None
732 + flashinfer_prefill_seq_order = None
733 + if self.use_flashinfer_prefill and num_prefills > 0:
734 + assert non_spec_query_start_loc is not None
735 + flashinfer_prefill_query_start_loc = non_spec_query_start_loc.to(
736 + torch.int64
737 + )
738 + num_non_spec_requests = non_spec_query_start_loc.shape[0] - 1
739 + num_non_spec_tokens = num_prefill_tokens + num_decode_tokens
740 + if num_non_spec_tokens > num_non_spec_requests:
741 + flashinfer_prefill_seq_order = torch.argsort(
742 + flashinfer_prefill_query_start_loc.diff(), descending=True
743 + ).to(torch.int32)
744 +
745 return KimiK3KDAMetadata(
746 num_prefills=num_prefills,
747 num_prefill_tokens=num_prefill_tokens,
@@ -738,6 +759,8 @@ class KimiK3KDAMetadataBuilder(GDNAttentionMetadataBuilder):
759 spec_token_indx=spec_token_indx,
760 non_spec_token_indx=non_spec_token_indx,
761 num_accepted_tokens=num_accepted_tokens,
762 + flashinfer_prefill_query_start_loc=flashinfer_prefill_query_start_loc,
763 + flashinfer_prefill_seq_order=flashinfer_prefill_seq_order,
764 recoverssm_commit=recoverssm_commit,
765 recoverssm_context=(
766 self._get_recoverssm_context()
vllm/models/kimi_k3/nvidia/model.py
+3 -1
@@ -1649,7 +1649,9 @@ class KimiLinearForCausalLM(
1649 vllm_config: "VllmConfig",
1650 ) -> tuple[torch.dtype, ...]:
1651 dtypes = MambaStateDtypeCalculator.kda_state_dtype(
1652 - vllm_config.model_config.dtype, vllm_config.cache_config.mamba_cache_dtype
1652 + vllm_config.model_config.dtype,
1653 + vllm_config.cache_config.mamba_cache_dtype,
1654 + vllm_config.cache_config.mamba_ssm_cache_dtype,
1655 )
1656 if vllm_config.cache_config.use_kda_recoverssm:
1657 dtypes = MambaStateDtypeCalculator.append_kda_recoverssm_record(
vllm/utils/flashinfer.py
+34
@@ -264,6 +264,14 @@ flashinfer_xqa_batch_decode_with_kv_cache = _lazy_import_wrapper(
264 "flashinfer.decode",
265 "xqa_batch_decode_with_kv_cache",
266 )
267 +flashinfer_recurrent_kda = _lazy_import_wrapper(
268 + "flashinfer.kda",
269 + "recurrent_kda",
270 +)
271 +flashinfer_fused_kda_decode = _lazy_import_wrapper(
272 + "flashinfer.kda_decode",
273 + "fused_kda_decode",
274 +)
275
276
277 # Special case for autotune since it returns a context manager
@@ -399,6 +407,28 @@ def has_flashinfer_bf16_fp4() -> bool:
407 )
408
409
410 +@functools.cache
411 +def has_flashinfer_recurrent_kda() -> bool:
412 + """Return whether FlashInfer recurrent KDA prefill is available."""
413 + if not has_flashinfer():
414 + return False
415 + mod = _get_submodule("flashinfer.kda")
416 + return mod is not None and callable(getattr(mod, "recurrent_kda", None))
417 +
418 +
419 +@functools.cache
420 +def has_flashinfer_fused_kda_decode() -> bool:
421 + """Return whether FlashInfer fused KDA decode is available."""
422 + if not has_flashinfer():
423 + return False
424 + mod = _get_submodule("flashinfer.kda_decode")
425 + return (
426 + mod is not None
427 + and bool(getattr(mod, "_FUSED_KDA_DECODE_AVAILABLE", False))
428 + and callable(getattr(mod, "fused_kda_decode", None))
429 + )
430 +
431 +
432 @functools.cache
433 def has_flashinfer_trtllm_fused_moe() -> bool:
434 """Return `True` if FlashInfer TRTLLM fused MoE is available."""
@@ -1261,6 +1291,8 @@ __all__ = [
1291 "flashinfer_trtllm_batch_decode_with_kv_cache_mla",
1292 "flashinfer_trtllm_batch_decode_sparse_mla_dsv4",
1293 "flashinfer_xqa_batch_decode_with_kv_cache",
1294 + "flashinfer_recurrent_kda",
1295 + "flashinfer_fused_kda_decode",
1296 "autotune",
1297 "has_flashinfer_moe",
1298 "has_flashinfer_comm",
@@ -1268,6 +1300,8 @@ __all__ = [
1300 "has_flashinfer_nvlink_one_sided",
1301 "has_flashinfer_cutlass_fused_moe",
1302 "has_flashinfer_cutedsl_grouped_gemm_nt_masked",
1303 + "has_flashinfer_recurrent_kda",
1304 + "has_flashinfer_fused_kda_decode",
1305 "has_flashinfer_cutedsl_moe_nvfp4",
1306 "has_flashinfer_bf16_fp4",
1307 "has_flashinfer_b12x_moe",