[Kimi-K3] Overlap low-M TP8 KDA projections (#54697)
Signed-off-by: Yongye Zhu <zyy1102000@gmail.com> Co-authored-by: OpenAI Codex <codex@openai.com>
Yongye Zhu committed
Sep 1, 2026 at 20:35 UTC
3ba9907a1db20c085be5c2d3cfbbbd6ec4a182dc
7 files changed
+1092
-12
benchmarks/kernels/benchmark_kimi_k3_kda_projection.py
new
+221
@@ -0,0 +1,221 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+"""Compare Kimi-K3 TP8 KDA sequential and multistream projections on B300."""
4
+
5
+from __future__ import annotations
6
+
7
+import argparse
8
+import statistics
9
+from collections.abc import Callable
10
+
11
+import torch
12
+
13
+from vllm.model_executor.kernels.linear.cute_dsl.skinny_gemm import (
14
+ shape_dynamic_skinny_gemm,
15
+)
16
+from vllm.models.kimi_k3.nvidia.low_latency_gemm import (
17
+ KDA_PROJECTION_OVERLAP_MAX_TOKENS,
18
+ run_kda_projection_overlap,
19
+ try_low_latency_gemm,
20
+)
21
+
22
+ProjectionOutput = tuple[torch.Tensor, torch.Tensor, torch.Tensor]
23
+
24
+
25
+def _capture(
26
+ fn: Callable[[], list[ProjectionOutput]],
27
+) -> tuple[torch.cuda.CUDAGraph, list[ProjectionOutput]]:
28
+ capture_stream = torch.cuda.Stream()
29
+ capture_stream.wait_stream(torch.cuda.current_stream())
30
+ with torch.cuda.stream(capture_stream):
31
+ fn()
32
+ fn()
33
+ capture_stream.synchronize()
34
+
35
+ graph = torch.cuda.CUDAGraph()
36
+ with torch.cuda.graph(graph, stream=capture_stream):
37
+ outputs = fn()
38
+ torch.cuda.current_stream().wait_stream(capture_stream)
39
+ torch.accelerator.synchronize()
40
+ return graph, outputs
41
+
42
+
43
+def _benchmark_graph(
44
+ graph: torch.cuda.CUDAGraph,
45
+ num_weight_sets: int,
46
+ repeats: int,
47
+) -> float:
48
+ start = torch.cuda.Event(enable_timing=True)
49
+ end = torch.cuda.Event(enable_timing=True)
50
+ start.record()
51
+ for _ in range(repeats):
52
+ graph.replay()
53
+ end.record()
54
+ end.synchronize()
55
+ return start.elapsed_time(end) * 1000 / repeats / num_weight_sets
56
+
57
+
58
+def _validate(
59
+ output: ProjectionOutput,
60
+ hidden_states: torch.Tensor,
61
+ packed_weight: torch.Tensor,
62
+ f_b_weight: torch.Tensor,
63
+) -> None:
64
+ expected_qkvg = torch.nn.functional.linear(
65
+ hidden_states.float(), packed_weight[:6144].float()
66
+ )
67
+ expected_fab = torch.nn.functional.linear(
68
+ hidden_states.float(), packed_weight[6144:6284].float()
69
+ )
70
+ expected_g1 = torch.nn.functional.linear(
71
+ expected_fab[:, :128].to(torch.bfloat16).float(),
72
+ f_b_weight.float(),
73
+ )
74
+ for actual, expected in zip(
75
+ output,
76
+ (expected_qkvg, expected_g1, expected_fab[:, 128:]),
77
+ ):
78
+ cosine = torch.nn.functional.cosine_similarity(
79
+ actual.float().flatten(), expected.flatten(), dim=0
80
+ ).item()
81
+ if cosine <= 0.999:
82
+ raise AssertionError(f"projection cosine similarity is {cosine}")
83
+
84
+
85
+def _sequential_projection(
86
+ hidden_states: torch.Tensor,
87
+ packed_weight: torch.Tensor,
88
+ f_b_weight: torch.Tensor,
89
+) -> ProjectionOutput:
90
+ projected = try_low_latency_gemm(hidden_states, packed_weight)
91
+ if projected is None:
92
+ projected = torch.mm(hidden_states, packed_weight.t())
93
+ f_a = projected[:, 6144:6272]
94
+ g1 = torch.mm(f_a, f_b_weight.t())
95
+ return projected[:, :6144], g1, projected[:, 6272:6284]
96
+
97
+
98
+def main() -> None:
99
+ parser = argparse.ArgumentParser(description=__doc__)
100
+ parser.add_argument("--tokens", type=int, nargs="+", default=range(1, 17))
101
+ parser.add_argument("--weight-sets", type=int, default=4)
102
+ parser.add_argument("--repeats", type=int, default=300)
103
+ parser.add_argument("--trials", type=int, default=5)
104
+ args = parser.parse_args()
105
+
106
+ if torch.cuda.get_device_capability() != (10, 3):
107
+ raise SystemExit("This benchmark requires an SM103 GPU")
108
+ if not shape_dynamic_skinny_gemm.is_available():
109
+ raise SystemExit("This benchmark requires CuTe DSL")
110
+ required_ops = ("dsv3_fused_a_gemm",)
111
+ if not all(hasattr(torch.ops._C, name) for name in required_ops):
112
+ raise SystemExit("This benchmark requires the KDA projection GEMMs")
113
+ if not all(1 <= num_tokens <= 16 for num_tokens in args.tokens):
114
+ raise SystemExit("--tokens must be in [1, 16]")
115
+
116
+ torch.manual_seed(42)
117
+ packed_weights = [
118
+ torch.randn(6288, 7168, dtype=torch.bfloat16, device="cuda")
119
+ for _ in range(args.weight_sets)
120
+ ]
121
+ f_b_weights = [
122
+ torch.randn(1536, 128, dtype=torch.bfloat16, device="cuda")
123
+ for _ in range(args.weight_sets)
124
+ ]
125
+
126
+ from flashinfer.autotuner import autotune
127
+ from flashinfer.gemm import mm_bf16
128
+
129
+ with torch.inference_mode(), autotune(tune_mode=True):
130
+ mm_bf16(
131
+ torch.empty(
132
+ (KDA_PROJECTION_OVERLAP_MAX_TOKENS, 7168),
133
+ dtype=torch.bfloat16,
134
+ device="cuda",
135
+ ),
136
+ packed_weights[0][:6144].t(),
137
+ pdl=True,
138
+ backend="cute-dsl",
139
+ )
140
+
141
+ print("M sequential (us) multistream (us) fastest")
142
+ for num_tokens in args.tokens:
143
+ hidden_states = [
144
+ torch.randn(
145
+ num_tokens,
146
+ 7168,
147
+ dtype=torch.bfloat16,
148
+ device="cuda",
149
+ )
150
+ for _ in range(args.weight_sets)
151
+ ]
152
+
153
+ def sequential(
154
+ hidden_states: list[torch.Tensor] = hidden_states,
155
+ ) -> list[ProjectionOutput]:
156
+ return [
157
+ _sequential_projection(x, weight, f_b_weight)
158
+ for x, weight, f_b_weight in zip(
159
+ hidden_states, packed_weights, f_b_weights
160
+ )
161
+ ]
162
+
163
+ candidates: list[tuple[str, Callable[[], list[ProjectionOutput]]]] = [
164
+ ("sequential", sequential)
165
+ ]
166
+ if num_tokens <= KDA_PROJECTION_OVERLAP_MAX_TOKENS:
167
+ aux_stream = torch.cuda.Stream()
168
+ events = [
169
+ (torch.cuda.Event(), torch.cuda.Event())
170
+ for _ in range(args.weight_sets)
171
+ ]
172
+
173
+ def overlap(
174
+ hidden_states: list[torch.Tensor] = hidden_states,
175
+ aux_stream: torch.cuda.Stream = aux_stream,
176
+ events: list[tuple[torch.cuda.Event, torch.cuda.Event]] = events,
177
+ ) -> list[ProjectionOutput]:
178
+ return [
179
+ run_kda_projection_overlap(x, weight, f_b_weight, aux_stream, pair)
180
+ for x, weight, f_b_weight, pair in zip(
181
+ hidden_states, packed_weights, f_b_weights, events
182
+ )
183
+ ]
184
+
185
+ candidates.append(("multistream", overlap))
186
+
187
+ graphs = {}
188
+ for name, candidate in candidates:
189
+ graph, outputs = _capture(candidate)
190
+ graph.replay()
191
+ torch.accelerator.synchronize()
192
+ _validate(outputs[0], hidden_states[0], packed_weights[0], f_b_weights[0])
193
+ graphs[name] = graph
194
+
195
+ samples = {name: [] for name in graphs}
196
+ graph_items = list(graphs.items())
197
+ for trial in range(args.trials):
198
+ ordered_graphs = graph_items if trial % 2 == 0 else graph_items[::-1]
199
+ for name, graph in ordered_graphs:
200
+ samples[name].append(
201
+ _benchmark_graph(graph, args.weight_sets, args.repeats)
202
+ )
203
+ results = {name: statistics.median(values) for name, values in samples.items()}
204
+
205
+ sequential_us = results["sequential"]
206
+ multistream_us = results.get("multistream")
207
+ multistream_text = (
208
+ f"{multistream_us:18.3f}"
209
+ if multistream_us is not None
210
+ else " -"
211
+ )
212
+ fastest = (
213
+ "multistream"
214
+ if multistream_us is not None and multistream_us < sequential_us
215
+ else "sequential"
216
+ )
217
+ print(f"{num_tokens:2d} {sequential_us:15.3f}{multistream_text} {fastest}")
218
+
219
+
220
+if __name__ == "__main__":
221
+ main()
tests/kernels/test_bf16_skinny_gemm.py
+118
@@ -253,6 +253,65 @@ def test_residual_cute_configs_match_measured_table() -> None:
253
assert actual == EXPECTED_RESIDUAL_CUTE_CONFIGS
254
255
256
+def test_kda_overlap_configs_match_measured_table() -> None:
257
+ assert k3_gemm.KDA_PROJECTION_OVERLAP_MAX_TOKENS == 14
258
+ assert k3_gemm.KDA_SKINNY_N_MAX_TOKENS == 14
259
+ assert k3_gemm.KDA_SKINNY_K_MAX_TOKENS == 14
260
+ assert _config_tuple(k3_gemm.KDA_M1_QKVG_CONFIG) == (64, 4, 2, 8)
261
+ assert _config_tuple(k3_gemm.KDA_M1_FAB_CONFIG) == (224, 1, 2, 8)
262
+ assert {
263
+ num_tokens: _config_tuple(config)
264
+ for num_tokens, config in k3_gemm.KDA_QKVG_CONFIGS.items()
265
+ } == {
266
+ 1: (64, 4, 2, 8),
267
+ 2: (64, 3, 2, 8),
268
+ }
269
+
270
+
271
+@pytest.mark.parametrize("tp_size", [1, 2, 4, 16])
272
+def test_kda_projection_overlap_is_tp8_only(tp_size: int) -> None:
273
+ from vllm.models.kimi_k3.nvidia.kda import KimiK3DeltaAttention
274
+
275
+ kda = KimiK3DeltaAttention.__new__(KimiK3DeltaAttention)
276
+ nn.Module.__init__(kda)
277
+ kda.tp_size = tp_size
278
+ kda._projection_overlap_max_tokens = 0
279
+
280
+ assert not k3_gemm._enable_kda_projection_overlap(kda)
281
+ assert kda._projection_overlap_max_tokens == 0
282
+
283
+
284
+def test_kda_qkvg_autotune_enables_full_overlap(
285
+ monkeypatch: pytest.MonkeyPatch,
286
+) -> None:
287
+ import flashinfer.gemm
288
+
289
+ from vllm.models.kimi_k3.nvidia.kda import KimiK3DeltaAttention
290
+
291
+ kda = KimiK3DeltaAttention.__new__(KimiK3DeltaAttention)
292
+ nn.Module.__init__(kda)
293
+ kda._projection_overlap_max_tokens = max(k3_gemm.KDA_QKVG_CONFIGS)
294
+ kda.in_proj_qkvgfab = nn.Module()
295
+ kda.in_proj_qkvgfab.weight = nn.Parameter(
296
+ torch.empty(6144, 8, dtype=torch.bfloat16),
297
+ requires_grad=False,
298
+ )
299
+ calls = []
300
+
301
+ def fake_mm_bf16(a, b, *, pdl, backend):
302
+ calls.append((a.shape, b.shape, pdl, backend))
303
+ return torch.empty(a.shape[0], b.shape[1], dtype=a.dtype)
304
+
305
+ monkeypatch.setattr(flashinfer.gemm, "mm_bf16", fake_mm_bf16)
306
+
307
+ k3_gemm.autotune_kda_qkvg(kda)
308
+
309
+ assert calls == [(torch.Size([14, 8]), torch.Size([8, 6144]), True, "cute-dsl")]
310
+ assert (
311
+ kda._projection_overlap_max_tokens == k3_gemm.KDA_PROJECTION_OVERLAP_MAX_TOKENS
312
+ )
313
+
314
+
315
def test_glm52_projection_plans_are_separate() -> None:
316
qkv_a = glm52_gemm.GLM52_QKV_A_PROJECTION
317
q_b = glm52_gemm.GLM52_Q_B_PROJECTION
@@ -812,6 +871,65 @@ def test_cute_selected_shapes(
871
assert cosine > 0.999
872
873
874
+@pytest.mark.parametrize("num_tokens", [1, 2, 3, 4, 8, 9, 10, 11, 12, 13, 14])
875
+def test_kda_projection_overlap_cuda_graph(num_tokens: int) -> None:
876
+ """The TP8 split must preserve projection results across both streams."""
877
+ _require_sm103_and_cute()
878
+ _require_sm103_and_dsv3()
879
+ torch.manual_seed(43)
880
+ hidden_states = torch.randn(num_tokens, 7168, dtype=torch.bfloat16, device="cuda")
881
+ packed_weight = torch.randn(6288, 7168, dtype=torch.bfloat16, device="cuda")
882
+ f_b_weight = torch.randn(1536, 128, dtype=torch.bfloat16, device="cuda")
883
+ aux_stream = torch.cuda.Stream()
884
+ events = (torch.cuda.Event(), torch.cuda.Event())
885
+
886
+ k3_gemm.run_kda_projection_overlap(
887
+ hidden_states,
888
+ packed_weight,
889
+ f_b_weight,
890
+ aux_stream,
891
+ events,
892
+ )
893
+ torch.accelerator.synchronize()
894
+
895
+ capture_stream = torch.cuda.Stream()
896
+ capture_stream.wait_stream(torch.cuda.current_stream())
897
+ graph = torch.cuda.CUDAGraph()
898
+ with torch.cuda.graph(graph, stream=capture_stream):
899
+ projected_qkvg, g1, beta = k3_gemm.run_kda_projection_overlap(
900
+ hidden_states,
901
+ packed_weight,
902
+ f_b_weight,
903
+ aux_stream,
904
+ events,
905
+ )
906
+ torch.cuda.current_stream().wait_stream(capture_stream)
907
+ graph.replay()
908
+ torch.accelerator.synchronize()
909
+
910
+ assert beta.stride() == ((140 if num_tokens == 1 else 144), 1)
911
+
912
+ expected_qkvg = torch.nn.functional.linear(
913
+ hidden_states.float(), packed_weight[:6144].float()
914
+ )
915
+ expected_fab = torch.nn.functional.linear(
916
+ hidden_states.float(), packed_weight[6144:6284].float()
917
+ )
918
+ expected_g1 = torch.nn.functional.linear(
919
+ expected_fab[:, :128].to(torch.bfloat16).float(),
920
+ f_b_weight.float(),
921
+ )
922
+ for actual, expected in (
923
+ (projected_qkvg, expected_qkvg),
924
+ (g1, expected_g1),
925
+ (beta, expected_fab[:, 128:]),
926
+ ):
927
+ cosine = torch.nn.functional.cosine_similarity(
928
+ actual.float().flatten(), expected.flatten(), dim=0
929
+ ).item()
930
+ assert cosine > 0.999
931
+
932
+
933
def _dsv3_probe_tokens(tokens: frozenset[int]) -> set[int]:
934
"""Extremes, plus both sides of the kernel's num_tokens<=8 tile_n branch."""
935
if not tokens:
vllm/model_executor/warmup/kernel_warmup.py
+7
@@ -114,6 +114,12 @@ def _warmup_kimi_k3_gemm_rs_ar() -> None:
114
logger.info_once("Warmed up %d Kimi-K3 GEMM-RS/AR variants.", compiled)
115
116
117
+def _autotune_kimi_k3_kda_qkvg(model: torch.nn.Module) -> None:
118
+ module = sys.modules.get("vllm.models.kimi_k3.nvidia.low_latency_gemm")
119
+ if module is not None:
120
+ module.autotune_kda_qkvg(model)
121
+
122
+
123
def kernel_warmup(worker: "Worker", *, process_local_only: bool = False):
124
from vllm.model_executor.warmup.minimax_m3_msa_warmup import (
125
minimax_m3_msa_warmup,
@@ -343,6 +349,7 @@ def flashinfer_autotune(runner: "GPUModelRunner") -> None:
349
):
350
_run_flashinfer_autotune_dummy_runs(runner)
351
replayssm_autotune_warmup(runner)
352
+ _autotune_kimi_k3_kda_qkvg(runner.get_model())
353
finally:
354
set_autotune_process_group(None)
355
vllm/models/kimi_k3/nvidia/kda.py
+43
-12
@@ -399,6 +399,7 @@ class KimiK3DeltaAttention(GatedDeltaNetAttention):
399
config: KimiLinearConfig,
400
vllm_config: VllmConfig,
401
prefix: str = "",
402
+ aux_stream: torch.cuda.Stream | None = None,
403
run_gemm_rs_ar: bool = False,
404
) -> None:
405
super().__init__(config, vllm_config, prefix)
@@ -421,6 +422,11 @@ class KimiK3DeltaAttention(GatedDeltaNetAttention):
422
assert kda_config.get("use_full_rank_gate", False), (
423
"KimiK3DeltaAttention requires a full-rank gate"
424
)
425
+ self._projection_aux_stream = aux_stream
426
+ self._projection_events = (
427
+ (torch.cuda.Event(), torch.cuda.Event()) if aux_stream is not None else None
428
+ )
429
+ self._projection_overlap_max_tokens = 0
430
431
# Keep f_a before the narrow beta shard, then pad each TP-local row
432
# to select the aligned BF16 GEMM path.
@@ -605,19 +611,44 @@ class KimiK3DeltaAttention(GatedDeltaNetAttention):
611
positions: torch.Tensor,
612
) -> torch.Tensor:
613
num_tokens = hidden_states.size(0)
608
- projected_qkvgfab = self.in_proj_qkvgfab(hidden_states)[0]
609
- split_sizes = [
610
- 3 * self.local_projection_size,
611
- self.local_projection_size,
612
- self.head_dim,
613
- self.local_num_heads,
614
- ]
615
- if self.in_proj_padding:
616
- split_sizes.append(self.in_proj_padding)
617
- projected = projected_qkvgfab.split(split_sizes, dim=-1)
618
- mixed_qkv, g_proj_states, f_a, beta = projected[:4]
614
+ projection_events = self._projection_events
615
+ projection_aux_stream = self._projection_aux_stream
616
+ if (
617
+ 0 < num_tokens <= self._projection_overlap_max_tokens
618
+ and hidden_states.stride() == (self.hidden_size, 1)
619
+ and projection_events is not None
620
+ and projection_aux_stream is not None
621
+ and torch.cuda.is_current_stream_capturing()
622
+ ):
623
+ from vllm.models.kimi_k3.nvidia.low_latency_gemm import (
624
+ run_kda_projection_overlap,
625
+ )
626
+
627
+ projected_qkvg, g1, beta = run_kda_projection_overlap(
628
+ hidden_states,
629
+ self.in_proj_qkvgfab.weight,
630
+ self.f_b_proj.weight,
631
+ projection_aux_stream,
632
+ projection_events,
633
+ )
634
+ mixed_qkv, g_proj_states = projected_qkvg.split(
635
+ [3 * self.local_projection_size, self.local_projection_size],
636
+ dim=-1,
637
+ )
638
+ else:
639
+ projected_qkvgfab = self.in_proj_qkvgfab(hidden_states)[0]
640
+ split_sizes = [
641
+ 3 * self.local_projection_size,
642
+ self.local_projection_size,
643
+ self.head_dim,
644
+ self.local_num_heads,
645
+ ]
646
+ if self.in_proj_padding:
647
+ split_sizes.append(self.in_proj_padding)
648
+ projected = projected_qkvgfab.split(split_sizes, dim=-1)
649
+ mixed_qkv, g_proj_states, f_a, beta = projected[:4]
650
+ g1 = self.f_b_proj(f_a)[0]
651
620
- g1 = self.f_b_proj(f_a)[0]
652
beta = beta.unsqueeze(0)
653
g1 = rearrange(g1, "n (h d) -> 1 n h d", d=self.head_dim)
654
g2 = rearrange(g_proj_states, "... (h d) -> ... h d", d=self.head_dim)
vllm/models/kimi_k3/nvidia/low_latency_gemm.py
+195
@@ -35,12 +35,32 @@ from vllm.model_executor.layers.vocab_parallel_embedding import (
35
UnquantizedEmbeddingMethod,
36
)
37
from vllm.platforms import current_platform
38
+from vllm.utils.multi_stream_utils import maybe_execute_in_parallel
39
40
Backend = Literal["cute", "dsv3_fused_a"]
41
# A resolved per-token-count call: the backend plus its CuTe config (None for
42
# dsv3, which needs no config).
43
ResolvedCall = tuple[Backend, SkinnyGemmConfig | None]
44
45
+# TP8 KDA projection split, measured together under CUDA graph capture on B300.
46
+# Q/K/V/G stay on the graph's main stream while F_A/beta and F_B run on the
47
+# model's auxiliary stream.
48
+KDA_M1_QKVG_CONFIG = SkinnyGemmConfig(1, 64, 4, 2, 8)
49
+KDA_M1_FAB_CONFIG = SkinnyGemmConfig(1, 224, 1, 2, 8)
50
+KDA_QKVG_CONFIGS = {
51
+ 1: KDA_M1_QKVG_CONFIG,
52
+ 2: SkinnyGemmConfig(2, 64, 3, 2, 8),
53
+}
54
+# The captured end-to-end projection sweep wins through M=14 and regresses at
55
+# M=15 and M=16, where the original packed projection remains selected.
56
+KDA_PROJECTION_OVERLAP_MAX_TOKENS = 14
57
+KDA_SKINNY_N_MAX_TOKENS = KDA_PROJECTION_OVERLAP_MAX_TOKENS
58
+KDA_SKINNY_K_MAX_TOKENS = KDA_PROJECTION_OVERLAP_MAX_TOKENS
59
+_KDA_QKVG_SIZE = 4 * 1536
60
+_KDA_FAB_SIZE = 128 + 12
61
+_KDA_PACKED_SIZE = 6288
62
+_KDA_TP_SIZE = 8
63
+
64
65
@dataclass(frozen=True, slots=True)
66
class ProjectionSpec:
@@ -683,6 +703,137 @@ def _run_plan(
703
return output
704
705
706
+def run_kda_projection_overlap(
707
+ hidden_states: torch.Tensor,
708
+ packed_weight: torch.Tensor,
709
+ f_b_weight: torch.Tensor,
710
+ aux_stream: torch.cuda.Stream,
711
+ events: tuple[torch.cuda.Event, torch.cuda.Event],
712
+) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
713
+ """Run the TP8 KDA decode projection branches concurrently.
714
+
715
+ Args:
716
+ hidden_states: Packed BF16 input with shape ``[M, 7168]``.
717
+ packed_weight: Existing Q/K/V/G/F_A/beta/pad weight with shape
718
+ ``[6288, 7168]``.
719
+ f_b_weight: F_B weight with shape ``[1536, 128]``.
720
+ aux_stream: Stream for the F_A/beta then F_B branch.
721
+ events: Start and completion events for the stream fork and join.
722
+
723
+ Returns:
724
+ The Q/K/V/G projection, F_B output, and beta projection.
725
+ """
726
+ num_tokens = hidden_states.shape[0]
727
+ qkvg_weight = packed_weight[:_KDA_QKVG_SIZE]
728
+
729
+ def run_qkvg() -> torch.Tensor:
730
+ if config := KDA_QKVG_CONFIGS.get(num_tokens):
731
+ return shape_dynamic_skinny_gemm(
732
+ hidden_states,
733
+ qkvg_weight,
734
+ config,
735
+ None,
736
+ )
737
+ if num_tokens <= KDA_PROJECTION_OVERLAP_MAX_TOKENS:
738
+ from flashinfer.gemm import mm_bf16
739
+
740
+ return mm_bf16(
741
+ hidden_states,
742
+ qkvg_weight.t(),
743
+ pdl=True,
744
+ backend="cute-dsl",
745
+ )
746
+ return torch.mm(hidden_states, qkvg_weight.t())
747
+
748
+ def run_fab_fb() -> tuple[torch.Tensor, torch.Tensor]:
749
+ if num_tokens == 1:
750
+ projected_fab = shape_dynamic_skinny_gemm(
751
+ hidden_states,
752
+ packed_weight[_KDA_QKVG_SIZE : _KDA_QKVG_SIZE + _KDA_FAB_SIZE],
753
+ KDA_M1_FAB_CONFIG,
754
+ None,
755
+ )
756
+ f_a, beta = projected_fab.split([128, 12], dim=-1)
757
+ f_a = f_a.as_strided((1, 128), (128, 1))
758
+ g1 = torch.empty(
759
+ (1, 1536), dtype=hidden_states.dtype, device=hidden_states.device
760
+ )
761
+ ops.dsv3_fused_a_gemm(g1, f_a, f_b_weight.t(), enable_pdl=True)
762
+ else:
763
+ from vllm.models.kimi_k3.nvidia.ops.cute_dsl.kda_skinny_gemm import (
764
+ kda_skinny_gemm,
765
+ )
766
+
767
+ if num_tokens <= KDA_SKINNY_N_MAX_TOKENS:
768
+ projected_fab = kda_skinny_gemm.run_n(
769
+ hidden_states,
770
+ packed_weight[_KDA_QKVG_SIZE:],
771
+ )
772
+ else:
773
+ projected_fab = torch.mm(
774
+ hidden_states,
775
+ packed_weight[_KDA_QKVG_SIZE:].t(),
776
+ )
777
+ beta = projected_fab[:, 128:140]
778
+ if num_tokens <= KDA_SKINNY_K_MAX_TOKENS:
779
+ g1 = kda_skinny_gemm.run_k(projected_fab, f_b_weight)
780
+ else:
781
+ g1 = torch.mm(projected_fab[:, :128], f_b_weight.t())
782
+ return g1, beta
783
+
784
+ projected_qkvg, (g1, beta) = maybe_execute_in_parallel(
785
+ run_qkvg,
786
+ run_fab_fb,
787
+ events[0],
788
+ events[1],
789
+ aux_stream,
790
+ )
791
+ return projected_qkvg, g1, beta
792
+
793
+
794
+def autotune_kda_qkvg(model: nn.Module) -> None:
795
+ """Autotune the FlashInfer QKVG GEMM before CUDA graph capture."""
796
+ from flashinfer.gemm import mm_bf16
797
+
798
+ from vllm.models.kimi_k3.nvidia.kda import KimiK3DeltaAttention
799
+
800
+ children: list[KimiK3DeltaAttention] = []
801
+ weights_by_shape: dict[
802
+ tuple[int, int, int, torch.dtype, torch.device], torch.Tensor
803
+ ] = {}
804
+ for child in model.modules():
805
+ if not isinstance(child, KimiK3DeltaAttention):
806
+ continue
807
+ if child._projection_overlap_max_tokens <= 0:
808
+ continue
809
+ children.append(child)
810
+ qkvg_weight = child.in_proj_qkvgfab.weight[:_KDA_QKVG_SIZE]
811
+ shape = (
812
+ KDA_PROJECTION_OVERLAP_MAX_TOKENS,
813
+ qkvg_weight.shape[0],
814
+ qkvg_weight.shape[1],
815
+ qkvg_weight.dtype,
816
+ qkvg_weight.device,
817
+ )
818
+ weights_by_shape.setdefault(shape, qkvg_weight)
819
+
820
+ for shape, qkvg_weight in weights_by_shape.items():
821
+ num_tokens = shape[0]
822
+ hidden_states = torch.empty(
823
+ (num_tokens, qkvg_weight.shape[1]),
824
+ dtype=qkvg_weight.dtype,
825
+ device=qkvg_weight.device,
826
+ )
827
+ mm_bf16(
828
+ hidden_states,
829
+ qkvg_weight.t(),
830
+ pdl=True,
831
+ backend="cute-dsl",
832
+ )
833
+ for child in children:
834
+ child._projection_overlap_max_tokens = KDA_PROJECTION_OVERLAP_MAX_TOKENS
835
+
836
+
837
def _run_residual_plan(
838
residual_plan: dict[int, SkinnyGemmConfig],
839
x: torch.Tensor,
@@ -777,6 +928,40 @@ class KimiK3LowLatencyEmbeddingMethod(
928
pass
929
930
931
+def _enable_kda_projection_overlap(module: nn.Module) -> bool:
932
+ from vllm.models.kimi_k3.nvidia.kda import KimiK3DeltaAttention
933
+
934
+ if envs.VLLM_BATCH_INVARIANT:
935
+ return False
936
+
937
+ enabled = False
938
+ for child in module.modules():
939
+ if not isinstance(child, KimiK3DeltaAttention):
940
+ continue
941
+ if child.tp_size != _KDA_TP_SIZE:
942
+ continue
943
+ packed_weight = child.in_proj_qkvgfab.weight
944
+ f_b_weight = child.f_b_proj.weight
945
+ if (
946
+ type(child.in_proj_qkvgfab.quant_method) is not KimiK3LowLatencyLinearMethod
947
+ or type(child.f_b_proj.quant_method) is not KimiK3LowLatencyLinearMethod
948
+ or child._projection_aux_stream is None
949
+ or child._projection_events is None
950
+ or packed_weight.shape != (_KDA_PACKED_SIZE, 7168)
951
+ or f_b_weight.shape != (1536, 128)
952
+ or packed_weight.dtype != torch.bfloat16
953
+ or f_b_weight.dtype != torch.bfloat16
954
+ or not packed_weight.is_cuda
955
+ or not f_b_weight.is_cuda
956
+ or not packed_weight.is_contiguous()
957
+ or not f_b_weight.is_contiguous()
958
+ ):
959
+ continue
960
+ child._projection_overlap_max_tokens = max(KDA_QKVG_CONFIGS)
961
+ enabled = True
962
+ return enabled
963
+
964
+
965
def enable_kimi_k3_low_latency_gemm(
966
module: nn.Module,
967
dtype: torch.dtype,
@@ -825,6 +1010,16 @@ def enable_kimi_k3_low_latency_gemm(
1010
# TP8 deployment does not compile TP4 configs and vice versa.
1011
warmup_configs.update(config for _, config in spec.cute_configs)
1012
residual_warmup_configs.update(config for _, config in spec.residual_configs)
1013
+ if _enable_kda_projection_overlap(module):
1014
+ from vllm.models.kimi_k3.nvidia.ops.cute_dsl.kda_skinny_gemm import (
1015
+ kda_skinny_gemm,
1016
+ )
1017
+
1018
+ warmup_configs.update((*KDA_QKVG_CONFIGS.values(), KDA_M1_FAB_CONFIG))
1019
+ kda_skinny_gemm.request_warmup(
1020
+ set(range(2, KDA_SKINNY_N_MAX_TOKENS + 1)),
1021
+ set(range(2, KDA_SKINNY_K_MAX_TOKENS + 1)),
1022
+ )
1023
1024
if shape_dynamic_skinny_gemm.is_available():
1025
if warmup_configs:
vllm/models/kimi_k3/nvidia/model.py
+1
@@ -895,6 +895,7 @@ class KimiDecoderLayer(nn.Module):
895
config,
896
vllm_config,
897
prefix=f"{prefix}.self_attn",
898
+ aux_stream=aux_stream,
899
run_gemm_rs_ar=run_gemm_rs_ar,
900
)
901
self._self_attn_writes_output = False
vllm/models/kimi_k3/nvidia/ops/cute_dsl/kda_skinny_gemm.py
new
+507
@@ -0,0 +1,507 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+# Adapted from
4
+# https://github.com/sgl-project/sglang/blob/main/python/sglang/kernels/jit/csrc/gemm/tiny_gemm.cuh
5
+"""Kimi-K3 TP8 skinny GEMMs for the KDA F_A/beta and F_B projections."""
6
+
7
+from __future__ import annotations
8
+
9
+from collections.abc import Callable
10
+from functools import partial
11
+
12
+import cuda.bindings.driver as cuda
13
+import cutlass
14
+import cutlass.cute as cute
15
+import torch
16
+from cutlass import BFloat16, Float32
17
+from cutlass._mlir.dialects import llvm
18
+from cutlass.cute.runtime import make_fake_stream, make_fake_tensor
19
+from cutlass.cutlass_dsl import T, dsl_user_op
20
+
21
+_FAB_N = 144
22
+_FAB_K = 7168
23
+_FAB_VECTOR_WIDTH = 16
24
+_FAB_BLOCK_SIZE = _FAB_K // _FAB_VECTOR_WIDTH
25
+_FAB_NUM_WARPS = _FAB_BLOCK_SIZE // cute.arch.WARP_SIZE
26
+
27
+_FB_N = 1536
28
+_FB_K = 128
29
+_FB_VECTOR_WIDTH = 8
30
+_FB_K_LANES = _FB_K // _FB_VECTOR_WIDTH
31
+
32
+SKINNY_N_SPLIT_N = {
33
+ 2: 1,
34
+ 3: 2,
35
+ 4: 3,
36
+ 5: 3,
37
+ 6: 3,
38
+ 7: 3,
39
+ 8: 3,
40
+ 9: 2,
41
+ 10: 2,
42
+ 11: 2,
43
+ 12: 2,
44
+ 13: 2,
45
+ 14: 1,
46
+ 15: 1,
47
+ 16: 1,
48
+}
49
+
50
+SKINNY_K_SPLIT_N = {
51
+ 2: 8,
52
+ 3: 16,
53
+ 4: 16,
54
+ 5: 24,
55
+ 6: 24,
56
+ 7: 24,
57
+ 8: 24,
58
+ 9: 12,
59
+ 10: 16,
60
+ 11: 16,
61
+ 12: 12,
62
+ 13: 8,
63
+ 14: 8,
64
+ 15: 8,
65
+ 16: 8,
66
+}
67
+
68
+
69
+@dsl_user_op
70
+def _fma_f32_bf16(
71
+ a: BFloat16,
72
+ b: BFloat16,
73
+ acc: Float32,
74
+ *,
75
+ loc=None,
76
+ ip=None,
77
+) -> Float32:
78
+ a_bits = llvm.bitcast(T.i16(), a.ir_value(loc=loc, ip=ip), loc=loc, ip=ip)
79
+ b_bits = llvm.bitcast(T.i16(), b.ir_value(loc=loc, ip=ip), loc=loc, ip=ip)
80
+ result = llvm.inline_asm(
81
+ T.f32(),
82
+ [a_bits, b_bits, acc.ir_value(loc=loc, ip=ip)],
83
+ "fma.rn.f32.bf16 $0, $1, $2, $3;",
84
+ "=f,h,h,f",
85
+ has_side_effects=False,
86
+ is_align_stack=False,
87
+ asm_dialect=llvm.AsmDialect.AD_ATT,
88
+ loc=loc,
89
+ ip=ip,
90
+ )
91
+ return Float32(result)
92
+
93
+
94
+class _KdaSkinnyNGemm:
95
+ def __init__(self, num_rows: int, split_n: int) -> None:
96
+ if _FAB_N % split_n:
97
+ raise ValueError("split_n must divide the FAB output size")
98
+ if num_rows * split_n > _FAB_BLOCK_SIZE:
99
+ raise ValueError("the FAB output tile must fit in one block")
100
+ self.num_rows = num_rows
101
+ self.split_n = split_n
102
+
103
+ @cute.jit
104
+ def __call__(
105
+ self,
106
+ a: cute.Tensor,
107
+ weight: cute.Tensor,
108
+ output: cute.Tensor,
109
+ stream: cuda.CUstream,
110
+ ) -> None:
111
+ copy_a = cute.make_copy_atom(
112
+ cute.nvgpu.CopyG2ROp(),
113
+ BFloat16,
114
+ num_bits_per_copy=_FAB_VECTOR_WIDTH * BFloat16.width,
115
+ load_cache_mode=cute.nvgpu.LoadCacheMode.ALWAYS,
116
+ )
117
+ copy_weight = cute.make_copy_atom(
118
+ cute.nvgpu.CopyG2ROp(),
119
+ BFloat16,
120
+ num_bits_per_copy=_FAB_VECTOR_WIDTH * BFloat16.width,
121
+ load_cache_mode=cute.nvgpu.LoadCacheMode.STREAMING,
122
+ )
123
+ self.kernel(a, weight, output, copy_a, copy_weight).launch(
124
+ grid=[_FAB_N // self.split_n, 1, 1],
125
+ block=[_FAB_BLOCK_SIZE, 1, 1],
126
+ smem=_FAB_NUM_WARPS * self.num_rows * self.split_n * 4,
127
+ stream=stream,
128
+ use_pdl=True,
129
+ min_blocks_per_mp=1,
130
+ )
131
+
132
+ @cute.kernel
133
+ def kernel(
134
+ self,
135
+ a: cute.Tensor,
136
+ weight: cute.Tensor,
137
+ output: cute.Tensor,
138
+ copy_a: cute.CopyAtom,
139
+ copy_weight: cute.CopyAtom,
140
+ ) -> None:
141
+ tidx, _, _ = cute.arch.thread_idx()
142
+ block_idx, _, _ = cute.arch.block_idx()
143
+ warp_idx = cute.arch.warp_idx()
144
+ num_rows: cutlass.Constexpr = self.num_rows
145
+ split_n: cutlass.Constexpr = self.split_n
146
+
147
+ a_vectors = cute.logical_divide(a, (None, _FAB_VECTOR_WIDTH))
148
+ weight_vectors = cute.logical_divide(weight, (None, _FAB_VECTOR_WIDTH))
149
+ a_tiles = cute.logical_divide(
150
+ a_vectors,
151
+ (None, (None, _FAB_BLOCK_SIZE)),
152
+ )
153
+ weight_tiles = cute.logical_divide(
154
+ weight_vectors,
155
+ (None, (None, _FAB_BLOCK_SIZE)),
156
+ )
157
+ a_tile = a_tiles[None, (None, (tidx, None))]
158
+ n_base = block_idx * split_n
159
+
160
+ a_regs = cute.make_rmem_tensor(
161
+ cute.make_layout(
162
+ (num_rows, _FAB_VECTOR_WIDTH),
163
+ stride=(_FAB_VECTOR_WIDTH, 1),
164
+ ),
165
+ BFloat16,
166
+ )
167
+ weight_regs = cute.make_rmem_tensor(
168
+ cute.make_layout(
169
+ (split_n, _FAB_VECTOR_WIDTH),
170
+ stride=(_FAB_VECTOR_WIDTH, 1),
171
+ ),
172
+ BFloat16,
173
+ )
174
+ acc = cute.make_rmem_tensor(
175
+ cute.make_layout((num_rows, split_n), stride=(split_n, 1)),
176
+ Float32,
177
+ )
178
+ acc.fill(0.0)
179
+
180
+ for ni in cutlass.range_constexpr(split_n):
181
+ weight_tile = weight_tiles[
182
+ n_base + ni,
183
+ (None, (tidx, None)),
184
+ ]
185
+ cute.copy(copy_weight, weight_tile[None, 0], weight_regs[ni, None])
186
+
187
+ cute.arch.griddepcontrol_wait()
188
+
189
+ for mi in cutlass.range_constexpr(num_rows):
190
+ cute.copy(copy_a, a_tile[mi, None, 0], a_regs[mi, None])
191
+ for mi in cutlass.range_constexpr(num_rows):
192
+ for ni in cutlass.range_constexpr(split_n):
193
+ for vi in cutlass.range_constexpr(_FAB_VECTOR_WIDTH):
194
+ acc[mi, ni] = _fma_f32_bf16(
195
+ a_regs[mi, vi],
196
+ weight_regs[ni, vi],
197
+ acc[mi, ni],
198
+ )
199
+ acc[mi, ni] = cute.arch.warp_reduction_sum(acc[mi, ni])
200
+
201
+ partials_layout = cute.make_layout(
202
+ (_FAB_NUM_WARPS, num_rows, split_n),
203
+ stride=(num_rows * split_n, split_n, 1),
204
+ )
205
+ smem = cutlass.utils.SmemAllocator()
206
+ partials = smem.allocate_tensor(
207
+ Float32,
208
+ partials_layout,
209
+ byte_alignment=16,
210
+ )
211
+ with cute.arch.elect_one():
212
+ for mi in cutlass.range_constexpr(num_rows):
213
+ for ni in cutlass.range_constexpr(split_n):
214
+ partials[warp_idx, mi, ni] = acc[mi, ni]
215
+
216
+ cute.arch.griddepcontrol_launch_dependents()
217
+ cute.arch.sync_threads()
218
+
219
+ if tidx < num_rows * split_n:
220
+ mi = tidx // split_n
221
+ ni = tidx % split_n
222
+ total = (
223
+ partials[None, mi, ni]
224
+ .load()
225
+ .reduce(
226
+ cute.ReductionOp.ADD,
227
+ init_val=Float32(0.0),
228
+ reduction_profile=0,
229
+ )
230
+ )
231
+ output[mi, n_base + ni] = Float32(total).to(BFloat16)
232
+
233
+
234
+class _KdaSkinnyKGemm:
235
+ def __init__(self, num_rows: int, split_n: int) -> None:
236
+ if _FB_N % split_n:
237
+ raise ValueError("split_n must divide the FB output size")
238
+ if split_n * _FB_K_LANES > 1024:
239
+ raise ValueError("the FB output tile exceeds the block-size limit")
240
+ self.num_rows = num_rows
241
+ self.split_n = split_n
242
+
243
+ @cute.jit
244
+ def __call__(
245
+ self,
246
+ a: cute.Tensor,
247
+ weight: cute.Tensor,
248
+ output: cute.Tensor,
249
+ stream: cuda.CUstream,
250
+ ) -> None:
251
+ copy_atom = cute.make_copy_atom(
252
+ cute.nvgpu.CopyG2ROp(),
253
+ BFloat16,
254
+ num_bits_per_copy=_FB_VECTOR_WIDTH * BFloat16.width,
255
+ load_cache_mode=cute.nvgpu.LoadCacheMode.ALWAYS,
256
+ )
257
+ self.kernel(a, weight, output, copy_atom).launch(
258
+ grid=[_FB_N // self.split_n, 1, 1],
259
+ block=[self.split_n * _FB_K_LANES, 1, 1],
260
+ stream=stream,
261
+ use_pdl=True,
262
+ min_blocks_per_mp=1,
263
+ )
264
+
265
+ @cute.kernel
266
+ def kernel(
267
+ self,
268
+ a: cute.Tensor,
269
+ weight: cute.Tensor,
270
+ output: cute.Tensor,
271
+ copy_atom: cute.CopyAtom,
272
+ ) -> None:
273
+ tidx, _, _ = cute.arch.thread_idx()
274
+ block_idx, _, _ = cute.arch.block_idx()
275
+ num_rows: cutlass.Constexpr = self.num_rows
276
+ split_n: cutlass.Constexpr = self.split_n
277
+ k_lane = tidx % _FB_K_LANES
278
+ n_idx = block_idx * split_n + tidx // _FB_K_LANES
279
+
280
+ a_vectors = cute.logical_divide(a, (None, _FB_VECTOR_WIDTH))
281
+ weight_vectors = cute.logical_divide(weight, (None, _FB_VECTOR_WIDTH))
282
+ a_tiles = cute.logical_divide(
283
+ a_vectors,
284
+ (None, (None, _FB_K_LANES)),
285
+ )
286
+ weight_tiles = cute.logical_divide(
287
+ weight_vectors,
288
+ (None, (None, _FB_K_LANES)),
289
+ )
290
+ a_tile = a_tiles[None, (None, (k_lane, None))]
291
+ weight_tile = weight_tiles[n_idx, (None, (k_lane, None))]
292
+ weight_regs = cute.make_rmem_tensor(_FB_VECTOR_WIDTH, BFloat16)
293
+ input_regs = cute.make_rmem_tensor(
294
+ cute.make_layout(
295
+ (num_rows, _FB_VECTOR_WIDTH),
296
+ stride=(_FB_VECTOR_WIDTH, 1),
297
+ ),
298
+ BFloat16,
299
+ )
300
+ cute.copy(
301
+ copy_atom,
302
+ weight_tile[None, 0],
303
+ weight_regs,
304
+ )
305
+
306
+ cute.arch.griddepcontrol_wait()
307
+
308
+ for mi in cutlass.range_constexpr(num_rows):
309
+ cute.copy(
310
+ copy_atom,
311
+ a_tile[mi, None, 0],
312
+ input_regs[mi, None],
313
+ )
314
+ for mi in cutlass.range_constexpr(num_rows):
315
+ acc = Float32(0.0)
316
+ for vi in cutlass.range_constexpr(_FB_VECTOR_WIDTH):
317
+ acc = _fma_f32_bf16(
318
+ input_regs[mi, vi],
319
+ weight_regs[vi],
320
+ acc,
321
+ )
322
+ for offset in (8, 4, 2, 1):
323
+ acc += cute.arch.shuffle_sync_bfly(acc, offset)
324
+ output[mi, n_idx] = acc.to(BFloat16)
325
+
326
+ cute.arch.griddepcontrol_launch_dependents()
327
+
328
+
329
+class KdaSkinnyGemm:
330
+ def __init__(self) -> None:
331
+ self._compiled_n: dict[tuple[int, int], Callable[..., None]] = {}
332
+ self._compiled_k: dict[tuple[int, int], Callable[..., None]] = {}
333
+ self._warmup_n: set[int] = set()
334
+ self._warmup_k: set[int] = set()
335
+ self._warmup_registered = False
336
+
337
+ def _compile_n(self, num_rows: int) -> None:
338
+ device = torch.accelerator.current_device_index()
339
+ key = (device, num_rows)
340
+ if key in self._compiled_n:
341
+ return
342
+ split_n = SKINNY_N_SPLIT_N[num_rows]
343
+ self._compiled_n[key] = cute.compile(
344
+ _KdaSkinnyNGemm(num_rows, split_n),
345
+ make_fake_tensor(
346
+ BFloat16,
347
+ (num_rows, _FAB_K),
348
+ stride=(_FAB_K, 1),
349
+ assumed_align=32,
350
+ ),
351
+ make_fake_tensor(
352
+ BFloat16,
353
+ (_FAB_N, _FAB_K),
354
+ stride=(_FAB_K, 1),
355
+ assumed_align=32,
356
+ ),
357
+ make_fake_tensor(
358
+ BFloat16,
359
+ (num_rows, _FAB_N),
360
+ stride=(_FAB_N, 1),
361
+ assumed_align=32,
362
+ ),
363
+ make_fake_stream(use_tvm_ffi_env_stream=True),
364
+ options="--enable-tvm-ffi --ptxas-options -maxrregcount=255",
365
+ )
366
+
367
+ def _compile_k(self, num_rows: int) -> None:
368
+ device = torch.accelerator.current_device_index()
369
+ key = (device, num_rows)
370
+ if key in self._compiled_k:
371
+ return
372
+ split_n = SKINNY_K_SPLIT_N[num_rows]
373
+ self._compiled_k[key] = cute.compile(
374
+ _KdaSkinnyKGemm(num_rows, split_n),
375
+ make_fake_tensor(
376
+ BFloat16,
377
+ (num_rows, _FB_K),
378
+ stride=(_FAB_N, 1),
379
+ assumed_align=32,
380
+ ),
381
+ make_fake_tensor(
382
+ BFloat16,
383
+ (_FB_N, _FB_K),
384
+ stride=(_FB_K, 1),
385
+ assumed_align=32,
386
+ ),
387
+ make_fake_tensor(
388
+ BFloat16,
389
+ (num_rows, _FB_N),
390
+ stride=(_FB_N, 1),
391
+ assumed_align=32,
392
+ ),
393
+ make_fake_stream(use_tvm_ffi_env_stream=True),
394
+ options="--enable-tvm-ffi --ptxas-options -maxrregcount=255",
395
+ )
396
+
397
+ def request_warmup(self, n_tokens: set[int], k_tokens: set[int]) -> None:
398
+ if not n_tokens <= SKINNY_N_SPLIT_N.keys():
399
+ raise ValueError("unsupported skinny-N warmup token count")
400
+ if not k_tokens <= SKINNY_K_SPLIT_N.keys():
401
+ raise ValueError("unsupported skinny-K warmup token count")
402
+ self._warmup_n.update(n_tokens)
403
+ self._warmup_k.update(k_tokens)
404
+ if self._warmup_registered:
405
+ return
406
+ from vllm.model_executor.warmup.cutedsl_warmup import (
407
+ register_cutedsl_warmup_provider,
408
+ )
409
+
410
+ register_cutedsl_warmup_provider(self)
411
+ self._warmup_registered = True
412
+
413
+ def get_cutedsl_warmup_compile_units(self):
414
+ from vllm.model_executor.warmup.cutedsl_warmup import CuTeDSLCompileUnit
415
+
416
+ n_units = tuple(
417
+ CuTeDSLCompileUnit(
418
+ name="Kimi-K3 KDA skinny-N GEMM",
419
+ key=("kimi-k3-kda-skinny-n", num_rows),
420
+ compile=partial(self._compile_n, num_rows),
421
+ )
422
+ for num_rows in sorted(self._warmup_n)
423
+ )
424
+ k_units = tuple(
425
+ CuTeDSLCompileUnit(
426
+ name="Kimi-K3 KDA skinny-K GEMM",
427
+ key=("kimi-k3-kda-skinny-k", num_rows),
428
+ compile=partial(self._compile_k, num_rows),
429
+ )
430
+ for num_rows in sorted(self._warmup_k)
431
+ )
432
+ return n_units + k_units
433
+
434
+ @staticmethod
435
+ def _validate(
436
+ a: torch.Tensor,
437
+ weight: torch.Tensor,
438
+ *,
439
+ a_shape: tuple[int, int],
440
+ weight_shape: tuple[int, int],
441
+ ) -> None:
442
+ if a.shape != a_shape or weight.shape != weight_shape:
443
+ raise ValueError(
444
+ f"expected input {a_shape} and weight {weight_shape}, got "
445
+ f"{tuple(a.shape)} and {tuple(weight.shape)}"
446
+ )
447
+ if a.dtype != torch.bfloat16 or weight.dtype != torch.bfloat16:
448
+ raise ValueError("KDA skinny GEMMs require BF16 input and weight")
449
+ if not a.is_cuda or not weight.is_cuda or a.device != weight.device:
450
+ raise ValueError("KDA skinny GEMM operands must share a CUDA device")
451
+ if not a.is_contiguous() or not weight.is_contiguous():
452
+ raise ValueError("KDA skinny GEMM operands must be contiguous")
453
+ if a.data_ptr() % 32 or weight.data_ptr() % 32:
454
+ raise ValueError("KDA skinny GEMM operands must be 32-byte aligned")
455
+
456
+ def run_n(self, a: torch.Tensor, weight: torch.Tensor) -> torch.Tensor:
457
+ num_rows = a.shape[0]
458
+ if num_rows not in SKINNY_N_SPLIT_N:
459
+ raise ValueError("skinny-N requires M in [2, 16]")
460
+ self._validate(
461
+ a,
462
+ weight,
463
+ a_shape=(num_rows, _FAB_K),
464
+ weight_shape=(_FAB_N, _FAB_K),
465
+ )
466
+ output = torch.empty(
467
+ (num_rows, _FAB_N),
468
+ dtype=a.dtype,
469
+ device=a.device,
470
+ )
471
+ with torch.accelerator.device_index(a.device.index):
472
+ self._compile_n(num_rows)
473
+ key = (a.device.index, num_rows)
474
+ self._compiled_n[key](
475
+ a,
476
+ weight,
477
+ output,
478
+ )
479
+ return output
480
+
481
+ def run_k(self, packed_fab: torch.Tensor, weight: torch.Tensor) -> torch.Tensor:
482
+ num_rows = packed_fab.shape[0]
483
+ if num_rows not in SKINNY_K_SPLIT_N:
484
+ raise ValueError("skinny-K requires M in [2, 16]")
485
+ self._validate(
486
+ packed_fab,
487
+ weight,
488
+ a_shape=(num_rows, _FAB_N),
489
+ weight_shape=(_FB_N, _FB_K),
490
+ )
491
+ output = torch.empty(
492
+ (num_rows, _FB_N),
493
+ dtype=packed_fab.dtype,
494
+ device=packed_fab.device,
495
+ )
496
+ with torch.accelerator.device_index(packed_fab.device.index):
497
+ self._compile_k(num_rows)
498
+ key = (packed_fab.device.index, num_rows)
499
+ self._compiled_k[key](
500
+ packed_fab[:, :_FB_K],
501
+ weight,
502
+ output,
503
+ )
504
+ return output
505
+
506
+
507
+kda_skinny_gemm = KdaSkinnyGemm()