main
py 1,065 lines 34.2 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 """Kimi-K3 decode GEMM selection for unquantized BF16 on SM90/SM100/SM103/SM107.
4
5 Dispatch is purely by local ``(N, K)`` shape and token count ``M`` — the module
6 name plays no role. Each measured shape maps to a :class:`ProjectionSpec`
7 holding the winning backend per token count. The static part of the decision is
8 resolved once per module at install time into a small ``{M: call}`` plan, so the
9 per-forward path is a single dict lookup.
10
11 The supported capabilities carry separate measured tables:
12 :data:`KIMI_K3_PROJECTIONS` was tuned on B300 (SM103),
13 :data:`KIMI_K3_PROJECTIONS_SM100` on B200 (SM100), and
14 :data:`KIMI_K3_PROJECTIONS_SM90` on H200 (SM90). The per-(shape, M) winners
15 genuinely differ between the parts, so the tables must not be merged. SM107
16 (Rubin) reuses the SM103 table: the plan was validated end-to-end on SM107
17 hardware, but the per-M crossovers have not been re-measured there and may
18 deserve their own table once retuned.
19 """
20
21 from __future__ import annotations
22
23 from dataclasses import dataclass
24 from typing import Literal
25
26 import torch
27 from torch import nn
28
29 import vllm.envs as envs
30 from vllm import _custom_ops as ops
31 from vllm.model_executor.kernels.linear.cute_dsl.skinny_gemm import (
32 SkinnyGemmConfig,
33 shape_dynamic_skinny_gemm,
34 )
35 from vllm.model_executor.layers.linear import LinearBase, UnquantizedLinearMethod
36 from vllm.model_executor.layers.vocab_parallel_embedding import (
37 ParallelLMHead,
38 UnquantizedEmbeddingMethod,
39 )
40 from vllm.platforms import current_platform
41 from vllm.utils.multi_stream_utils import maybe_execute_in_parallel
42
43 Backend = Literal["cute", "dsv3_fused_a"]
44 # A resolved per-token-count call: the backend plus its CuTe config (None for
45 # dsv3, which needs no config).
46 ResolvedCall = tuple[Backend, SkinnyGemmConfig | None]
47
48 # TP8 KDA projection split, measured together under CUDA graph capture on B300.
49 # Q/K/V/G stay on the graph's main stream while F_A/beta and F_B run on the
50 # model's auxiliary stream.
51 KDA_M1_QKVG_CONFIG = SkinnyGemmConfig(1, 64, 4, 2, 8)
52 KDA_M1_FAB_CONFIG = SkinnyGemmConfig(1, 224, 1, 2, 8)
53 KDA_QKVG_CONFIGS = {
54 1: KDA_M1_QKVG_CONFIG,
55 2: SkinnyGemmConfig(2, 64, 3, 2, 8),
56 }
57 # The captured end-to-end projection sweep wins through M=14 and regresses at
58 # M=15 and M=16, where the original packed projection remains selected.
59 KDA_PROJECTION_OVERLAP_MAX_TOKENS = 14
60 KDA_SKINNY_N_MAX_TOKENS = KDA_PROJECTION_OVERLAP_MAX_TOKENS
61 KDA_SKINNY_K_MAX_TOKENS = KDA_PROJECTION_OVERLAP_MAX_TOKENS
62 _KDA_QKVG_SIZE = 4 * 1536
63 _KDA_FAB_SIZE = 128 + 12
64 _KDA_PACKED_SIZE = 6288
65 _KDA_TP_SIZE = 8
66
67
68 def _kda_qkvg_flashinfer_backend() -> Literal["cute-dsl"] | None:
69 """Return the tested FlashInfer backend for the low-M QKVG branch.
70
71 FlashInfer's ``cute-dsl`` BF16 GEMM supports SM100 and SM103, but rejects
72 SM90 during backend validation. Other capabilities use ``torch.mm`` for
73 QKVG while retaining the concurrent F_A/beta and F_B branch.
74 """
75 if current_platform.is_device_capability(
76 (10, 0)
77 ) or current_platform.is_device_capability((10, 3)):
78 return "cute-dsl"
79 return None
80
81
82 @dataclass(frozen=True, slots=True)
83 class ProjectionSpec:
84 n: int
85 k: int
86 dsv3_tokens: frozenset[int] = frozenset()
87 cute_configs: tuple[tuple[int, SkinnyGemmConfig], ...] = ()
88 residual_configs: tuple[tuple[int, SkinnyGemmConfig], ...] = ()
89 name: str = "" # optional debug label; never used for dispatch
90
91 def cute_config(self, num_tokens: int) -> SkinnyGemmConfig | None:
92 return dict(self.cute_configs).get(num_tokens)
93
94 def residual_config(self, num_tokens: int) -> SkinnyGemmConfig | None:
95 return dict(self.residual_configs).get(num_tokens)
96
97
98 def _cute(
99 num_tokens: int,
100 block_size: int,
101 outputs_per_block: int,
102 k_unroll: int,
103 vector_width: int = 8,
104 ) -> SkinnyGemmConfig:
105 return SkinnyGemmConfig(
106 num_tokens,
107 block_size,
108 outputs_per_block,
109 k_unroll,
110 vector_width,
111 )
112
113
114 _M1_TO_16 = frozenset(range(1, 17))
115 _M1 = frozenset({1})
116
117 # Keyed by local (N, K). Where two projections share a shape (only 1536x7168:
118 # shared_gate_up_proj and mla_g_proj) the entry is unified.
119 KIMI_K3_PROJECTIONS: dict[tuple[int, int], ProjectionSpec] = {
120 (1536, 128): ProjectionSpec(1536, 128, _M1_TO_16, name="f_b_proj"),
121 (3072, 128): ProjectionSpec(3072, 128, _M1_TO_16, name="f_b_proj"),
122 # 1536x7168 is shared by shared_gate_up_proj and mla_g_proj. dsv3 M1..16 is
123 # only crash-safe once the mla_g aux-stream/PDL capture fix lands (subtask
124 # task_7388aba1); the fallback if it cannot be fixed is dsv3_tokens=_M1.
125 (1536, 7168): ProjectionSpec(
126 1536, 7168, _M1_TO_16, name="shared_gate_up_proj/mla_g_proj"
127 ),
128 (3072, 7168): ProjectionSpec(
129 3072,
130 7168,
131 cute_configs=(
132 (1, _cute(1, 224, 3, 4)),
133 (2, _cute(2, 128, 3, 2)),
134 (3, _cute(3, 128, 2, 1)),
135 (4, _cute(4, 64, 2, 2)),
136 (5, _cute(5, 128, 3, 1)),
137 ),
138 name="shared_gate_up_proj",
139 ),
140 (2112, 7168): ProjectionSpec(2112, 7168, _M1_TO_16, name="fused_qkv_a_proj"),
141 (2304, 1536): ProjectionSpec(2304, 1536, _M1_TO_16, name="q_b_proj"),
142 (4608, 1536): ProjectionSpec(4608, 1536, _M1_TO_16, name="q_b_proj"),
143 (3584, 7168): ProjectionSpec(
144 3584,
145 7168,
146 frozenset(range(2, 9)),
147 ((1, _cute(1, 224, 2, 4)),),
148 name="routed_expert_down_proj",
149 ),
150 (6288, 7168): ProjectionSpec(
151 6288,
152 7168,
153 cute_configs=(
154 (1, _cute(1, 224, 3, 4)),
155 (2, _cute(2, 64, 3, 2)),
156 (3, _cute(3, 32, 3, 4)),
157 (4, _cute(4, 128, 6, 1)),
158 ),
159 name="in_proj_qkvgfab",
160 ),
161 (12448, 7168): ProjectionSpec(
162 12448,
163 7168,
164 cute_configs=(
165 (1, _cute(1, 224, 4, 2)),
166 (2, _cute(2, 64, 4, 2)),
167 (3, _cute(3, 64, 2, 2)),
168 ),
169 name="in_proj_qkvgfab",
170 ),
171 (7168, 768): ProjectionSpec(7168, 768, _M1_TO_16, name="shared_down_proj"),
172 (7168, 1536): ProjectionSpec(
173 7168, 1536, cute_configs=((1, _cute(1, 96, 4, 2)),), name="o_proj"
174 ),
175 (7168, 3072): ProjectionSpec(
176 7168,
177 3072,
178 cute_configs=(
179 (1, _cute(1, 96, 2, 4)),
180 (2, _cute(2, 32, 4, 4)),
181 ),
182 name="o_proj",
183 ),
184 (7168, 3584): ProjectionSpec(
185 7168,
186 3584,
187 cute_configs=(
188 (1, _cute(1, 224, 4, 2)),
189 (2, _cute(2, 64, 4, 2)),
190 ),
191 residual_configs=(
192 (1, _cute(1, 64, 4, 2)),
193 (2, _cute(2, 64, 7, 2)),
194 (3, _cute(3, 64, 2, 1)),
195 (4, _cute(4, 64, 2, 1)),
196 ),
197 name="routed_expert_up_proj",
198 ),
199 (7168, 4224): ProjectionSpec(
200 7168,
201 4224,
202 cute_configs=((1, _cute(1, 96, 4, 2, 4)),),
203 name="dense_down_proj",
204 ),
205 (7168, 8448): ProjectionSpec(
206 7168,
207 8448,
208 cute_configs=(
209 (1, _cute(1, 32, 4, 4)),
210 (2, _cute(2, 96, 4, 1)),
211 (3, _cute(3, 96, 4, 1)),
212 ),
213 name="dense_down_proj",
214 ),
215 (8448, 7168): ProjectionSpec(
216 8448,
217 7168,
218 cute_configs=(
219 (1, _cute(1, 224, 3, 4)),
220 (2, _cute(2, 32, 4, 4)),
221 ),
222 name="dense_gate_up_proj",
223 ),
224 (16896, 7168): ProjectionSpec(
225 16896,
226 7168,
227 cute_configs=(
228 (1, _cute(1, 224, 6, 4)),
229 (2, _cute(2, 32, 4, 4)),
230 ),
231 name="dense_gate_up_proj",
232 ),
233 (7168, 14336): ProjectionSpec(
234 7168,
235 14336,
236 cute_configs=(
237 (1, SkinnyGemmConfig(1, 256, 2, vector_width=4, static_k=14336)),
238 (2, _cute(2, 224, 4, 2)),
239 ),
240 ),
241 (20480, 7168): ProjectionSpec(
242 20480,
243 7168,
244 cute_configs=(
245 (1, _cute(1, 224, 4, 2)),
246 (2, _cute(2, 64, 4, 2)),
247 (3, _cute(3, 64, 2, 2)),
248 (4, _cute(4, 64, 4, 1)),
249 ),
250 name="lm_head",
251 ),
252 (40960, 7168): ProjectionSpec(
253 40960,
254 7168,
255 cute_configs=(
256 (1, _cute(1, 128, 4, 2)),
257 (2, _cute(2, 64, 4, 2)),
258 (3, _cute(3, 64, 2, 2)),
259 (4, _cute(4, 64, 4, 1)),
260 ),
261 name="lm_head",
262 ),
263 # TP16. Measured on B300 over M=1..16 with the same >=5% threshold as the
264 # entries above. The replicated projections (2112x7168, 3584x7168,
265 # 7168x3584) keep their shapes at TP16 and reuse the entries above, and
266 # o_proj lands on 7168x768, which shared_down_proj already covers.
267 (3216, 7168): ProjectionSpec(
268 3216,
269 7168,
270 # Both gaps in this range are measured, not oversights: dsv3 is only
271 # 4% ahead at M6..M8, and at M16 cuBLAS switches to a faster kernel
272 # (11.42us vs dsv3's 11.83us) after trailing it by 6-8% at M9..M15.
273 frozenset(range(9, 16)),
274 cute_configs=(
275 (1, _cute(1, 224, 3, 4)),
276 (2, _cute(2, 128, 4, 2)),
277 (3, _cute(3, 128, 2, 1)),
278 (4, _cute(4, 64, 2, 2)),
279 (5, _cute(5, 128, 3, 1)),
280 ),
281 name="in_proj_qkvgfab",
282 ),
283 (768, 7168): ProjectionSpec(
284 768,
285 7168,
286 frozenset(range(5, 17)),
287 cute_configs=(
288 (1, _cute(1, 224, 2, 4)),
289 (2, _cute(2, 224, 2, 2)),
290 (3, _cute(3, 224, 2, 2)),
291 (4, _cute(4, 224, 2, 2)),
292 ),
293 name="mla_g_proj/shared_gate_up_proj",
294 ),
295 (1152, 1536): ProjectionSpec(
296 1152,
297 1536,
298 frozenset(range(2, 17)),
299 ((1, _cute(1, 192, 3, 4)),),
300 name="q_b_proj",
301 ),
302 (768, 128): ProjectionSpec(768, 128, _M1_TO_16, name="f_b_proj"),
303 # dsv3 drops under 5% from M9 on for this shape.
304 (7168, 384): ProjectionSpec(
305 7168, 384, frozenset(range(1, 9)), name="shared_down_proj"
306 ),
307 (4224, 7168): ProjectionSpec(
308 4224,
309 7168,
310 frozenset(range(4, 9)),
311 cute_configs=(
312 (1, _cute(1, 224, 3, 4)),
313 (2, _cute(2, 128, 2, 1)),
314 (3, _cute(3, 64, 2, 2)),
315 ),
316 name="dense_gate_up_proj",
317 ),
318 (10240, 7168): ProjectionSpec(
319 10240,
320 7168,
321 cute_configs=(
322 (1, _cute(1, 224, 4, 2)),
323 (2, _cute(2, 32, 2, 4)),
324 (3, _cute(3, 64, 4, 1)),
325 (4, _cute(4, 64, 4, 1)),
326 ),
327 name="lm_head",
328 ),
329 # 7168x2112 (TP16 dense down_proj) has no entry on purpose: K=2112 divides
330 # none of the fused-A tile_k values, and the CuTe kernel is left with
331 # vector_width=2, which measured slower than cuBLAS.
332 }
333
334 # Keyed by local (N, K), B200 (SM100) entries. Measured on B200 at TP8/TP16
335 # shapes over M=1..16 with the same >=5%-over-cuBLAS threshold as the SM103
336 # table. Per-(shape, M) entries take the best of the SM103-tuned config and a
337 # fresh SM100 measurement: several SM103 configs (7168x8448, 20480x7168,
338 # 40960x7168, 10240x7168, 7168x3072 M2, 7168x1536) beat the heuristic config on
339 # B200 outright, while a few (6288x7168 M2..4, 3072x7168 M3..5) lose to cuBLAS
340 # on B200 and are intentionally not inherited. The 7168x3584 residual configs
341 # are intentionally absent: the B200 residual path was not measured.
342 KIMI_K3_PROJECTIONS_SM100: dict[tuple[int, int], ProjectionSpec] = {
343 (1536, 128): ProjectionSpec(1536, 128, frozenset({1, 16}), name="f_b_proj"),
344 (3072, 128): ProjectionSpec(3072, 128, frozenset({8}), name="f_b_proj"),
345 # Same mla_g_proj aux-stream/PDL capture caveat as the SM103 entry applies;
346 # only M4/M8 measured a dsv3 win, so this is the narrow set either way.
347 (1536, 7168): ProjectionSpec(
348 1536,
349 7168,
350 frozenset({4, 8}),
351 (
352 (1, _cute(1, 224, 3, 2)),
353 (2, _cute(2, 128, 2, 2)),
354 ),
355 name="shared_gate_up_proj/mla_g_proj",
356 ),
357 (3072, 7168): ProjectionSpec(
358 3072,
359 7168,
360 cute_configs=(
361 (1, _cute(1, 224, 3, 4)),
362 (2, _cute(2, 128, 2, 1)),
363 ),
364 name="shared_gate_up_proj",
365 ),
366 (2112, 7168): ProjectionSpec(
367 2112,
368 7168,
369 frozenset({4, 16}),
370 (
371 (1, _cute(1, 224, 3, 2)),
372 (2, _cute(2, 128, 2, 2)),
373 ),
374 name="fused_qkv_a_proj",
375 ),
376 (2304, 1536): ProjectionSpec(2304, 1536, _M1_TO_16, name="q_b_proj"),
377 (4608, 1536): ProjectionSpec(4608, 1536, frozenset({1, 2, 4}), name="q_b_proj"),
378 # M2..4 of the SM103 configs lose to cuBLAS on B200; only M1 carries over.
379 (6288, 7168): ProjectionSpec(
380 6288,
381 7168,
382 cute_configs=((1, _cute(1, 224, 3, 4)),),
383 name="in_proj_qkvgfab",
384 ),
385 (12448, 7168): ProjectionSpec(
386 12448,
387 7168,
388 cute_configs=(
389 (1, _cute(1, 224, 4, 2)),
390 (2, _cute(2, 64, 4, 2)),
391 (3, _cute(3, 64, 2, 2)),
392 (4, _cute(4, 128, 2, 1)),
393 ),
394 name="in_proj_qkvgfab",
395 ),
396 (7168, 768): ProjectionSpec(7168, 768, frozenset({1}), name="shared_down_proj"),
397 # SM103-tuned config wins on B200 (+25%) where the heuristic config tied.
398 (7168, 1536): ProjectionSpec(
399 7168,
400 1536,
401 cute_configs=((1, _cute(1, 96, 4, 2)),),
402 name="o_proj",
403 ),
404 (7168, 3072): ProjectionSpec(
405 7168,
406 3072,
407 cute_configs=(
408 (1, _cute(1, 96, 2, 4)),
409 (2, _cute(2, 32, 4, 4)),
410 ),
411 name="o_proj",
412 ),
413 (7168, 3584): ProjectionSpec(
414 7168,
415 3584,
416 cute_configs=(
417 (1, _cute(1, 64, 4, 2)),
418 (2, _cute(2, 64, 4, 2)),
419 ),
420 name="routed_expert_up_proj",
421 ),
422 (7168, 8448): ProjectionSpec(
423 7168,
424 8448,
425 cute_configs=(
426 (1, _cute(1, 32, 4, 4)),
427 (2, _cute(2, 96, 4, 1)),
428 (3, _cute(3, 96, 4, 1)),
429 ),
430 name="dense_down_proj",
431 ),
432 (8448, 7168): ProjectionSpec(
433 8448,
434 7168,
435 cute_configs=(
436 (1, _cute(1, 224, 3, 4)),
437 (2, _cute(2, 32, 4, 4)),
438 ),
439 name="dense_gate_up_proj",
440 ),
441 (16896, 7168): ProjectionSpec(
442 16896,
443 7168,
444 cute_configs=((1, _cute(1, 224, 3, 4)),),
445 name="dense_gate_up_proj",
446 ),
447 (20480, 7168): ProjectionSpec(
448 20480,
449 7168,
450 cute_configs=(
451 (1, _cute(1, 224, 4, 2)),
452 (2, _cute(2, 64, 4, 2)),
453 (3, _cute(3, 64, 2, 2)),
454 (4, _cute(4, 64, 4, 1)),
455 ),
456 name="lm_head",
457 ),
458 (40960, 7168): ProjectionSpec(
459 40960,
460 7168,
461 cute_configs=(
462 (1, _cute(1, 128, 4, 2)),
463 (2, _cute(2, 64, 4, 2)),
464 (3, _cute(3, 64, 4, 1)),
465 (4, _cute(4, 64, 4, 1)),
466 ),
467 name="lm_head",
468 ),
469 (10240, 7168): ProjectionSpec(
470 10240,
471 7168,
472 cute_configs=(
473 (1, _cute(1, 224, 4, 2)),
474 (2, _cute(2, 32, 2, 4)),
475 (3, _cute(3, 64, 4, 1)),
476 (4, _cute(4, 64, 4, 1)),
477 ),
478 name="lm_head",
479 ),
480 # Latent-MoE replicated projections. dsv3 measured M2..8 wins on B300 for
481 # 3584x7168 but loses to cuBLAS there on B200; only CuTe wins land here.
482 (3584, 7168): ProjectionSpec(
483 3584,
484 7168,
485 cute_configs=(
486 (1, _cute(1, 224, 2, 4)),
487 (2, _cute(2, 128, 2, 1)),
488 (3, _cute(3, 128, 2, 1)),
489 ),
490 name="routed_expert_down_proj",
491 ),
492 # TP16 shapes, newly measured on B200 (they only appear in TP16 deployments;
493 # on TP8 these table entries simply never match).
494 (768, 7168): ProjectionSpec(
495 768,
496 7168,
497 cute_configs=(
498 (1, _cute(1, 224, 2, 4)),
499 (2, _cute(2, 224, 2, 2)),
500 (3, _cute(3, 224, 2, 2)),
501 (4, _cute(4, 224, 2, 2)),
502 ),
503 name="mla_g_proj/shared_gate_up_proj",
504 ),
505 (1152, 1536): ProjectionSpec(
506 1152,
507 1536,
508 cute_configs=((1, _cute(1, 192, 3, 4)),),
509 name="q_b_proj",
510 ),
511 (3216, 7168): ProjectionSpec(
512 3216,
513 7168,
514 cute_configs=(
515 (1, _cute(1, 224, 3, 4)),
516 (2, _cute(2, 128, 4, 2)),
517 ),
518 name="in_proj_qkvgfab",
519 ),
520 (4224, 7168): ProjectionSpec(
521 4224,
522 7168,
523 cute_configs=(
524 (1, _cute(1, 224, 3, 4)),
525 (2, _cute(2, 128, 2, 1)),
526 (3, _cute(3, 64, 2, 2)),
527 ),
528 name="dense_gate_up_proj",
529 ),
530 }
531
532 _SM90CuteConfig = tuple[int, int, int, int]
533
534
535 def _sm90_spec(
536 n: int,
537 k: int,
538 dsv3_tokens: frozenset[int] = frozenset(),
539 configs: tuple[_SM90CuteConfig, ...] = (),
540 ) -> ProjectionSpec:
541 cute_configs = tuple(
542 (m, _cute(m, *config)) for m, config in enumerate(configs, start=1)
543 )
544 return ProjectionSpec(n, k, dsv3_tokens, cute_configs)
545
546
547 KIMI_K3_PROJECTIONS_SM90: dict[tuple[int, int], ProjectionSpec] = {
548 (1536, 128): _sm90_spec(1536, 128, frozenset(range(1, 9))),
549 (3072, 128): _sm90_spec(3072, 128, frozenset({1, 2, 5, 6, 7, 8, 9})),
550 (1536, 7168): _sm90_spec(
551 1536,
552 7168,
553 frozenset(range(7, 17)),
554 (
555 (224, 2, 4, 8),
556 (128, 3, 2, 8),
557 (128, 2, 1, 8),
558 (128, 2, 1, 8),
559 (128, 3, 1, 8),
560 (128, 3, 1, 8),
561 ),
562 ),
563 (3072, 7168): _sm90_spec(
564 3072,
565 7168,
566 configs=((224, 2, 4, 8), (128, 2, 2, 8), (64, 4, 1, 8), (128, 6, 1, 8)),
567 ),
568 (2112, 7168): _sm90_spec(
569 2112,
570 7168,
571 frozenset(range(5, 17)),
572 ((224, 2, 4, 8), (128, 4, 2, 8), (128, 2, 1, 8), (128, 2, 1, 8)),
573 ),
574 (2304, 1536): _sm90_spec(
575 2304,
576 1536,
577 frozenset(range(3, 9)),
578 ((96, 4, 2, 8), (96, 4, 1, 8)),
579 ),
580 (4608, 1536): _sm90_spec(
581 4608,
582 1536,
583 configs=((96, 4, 2, 4), (96, 4, 1, 8), (64, 4, 1, 8)),
584 ),
585 (3584, 7168): _sm90_spec(
586 3584,
587 7168,
588 configs=((224, 2, 4, 8), (128, 4, 2, 8), (128, 2, 1, 8)),
589 ),
590 (6288, 7168): _sm90_spec(
591 6288,
592 7168,
593 configs=((224, 2, 4, 8), (128, 4, 2, 8), (64, 4, 1, 8)),
594 ),
595 (12448, 7168): _sm90_spec(
596 12448,
597 7168,
598 configs=((224, 2, 4, 8), (224, 4, 2, 8), (128, 2, 1, 8)),
599 ),
600 (7168, 768): _sm90_spec(7168, 768, configs=((96, 4, 2, 4), (96, 4, 1, 8))),
601 (7168, 1536): _sm90_spec(7168, 1536, configs=((96, 4, 2, 8), (96, 4, 1, 8))),
602 (7168, 3072): _sm90_spec(
603 7168,
604 3072,
605 configs=((96, 2, 4, 8), (64, 4, 2, 8), (64, 2, 2, 8)),
606 ),
607 (7168, 3584): _sm90_spec(
608 7168,
609 3584,
610 configs=((224, 4, 2, 8), (64, 4, 2, 8), (64, 4, 2, 8)),
611 ),
612 (7168, 4224): _sm90_spec(7168, 4224, configs=((96, 4, 2, 4), (32, 4, 2, 4))),
613 (7168, 8448): _sm90_spec(7168, 8448, configs=((96, 2, 4, 8), (32, 4, 2, 8))),
614 (7168, 14336): _sm90_spec(7168, 14336, configs=((224, 2, 4, 8), (128, 2, 2, 8))),
615 (20480, 7168): _sm90_spec(
616 20480,
617 7168,
618 configs=((224, 4, 2, 8), (224, 4, 2, 8), (128, 2, 1, 8)),
619 ),
620 }
621
622
623 def _backend_for(
624 spec: ProjectionSpec, num_tokens: int, has_residual: bool
625 ) -> Backend | None:
626 if has_residual:
627 return "cute" if spec.residual_config(num_tokens) is not None else None
628 if spec.cute_config(num_tokens) is not None:
629 return "cute"
630 if num_tokens in spec.dsv3_tokens:
631 return "dsv3_fused_a"
632 return None
633
634
635 def select_kimi_k3_backend(
636 num_tokens: int,
637 n: int,
638 k: int,
639 *,
640 has_residual: bool = False,
641 ) -> Backend | None:
642 """Backend for a local ``(N, K)`` at ``num_tokens``, or None to fall back."""
643 spec = KIMI_K3_PROJECTIONS.get((n, k))
644 return _backend_for(spec, num_tokens, has_residual) if spec is not None else None
645
646
647 def _build_plan(spec: ProjectionSpec) -> dict[int, ResolvedCall]:
648 plan: dict[int, ResolvedCall] = {}
649 for num_tokens in range(1, 17):
650 backend = _backend_for(spec, num_tokens, has_residual=False)
651 if backend == "cute":
652 plan[num_tokens] = ("cute", spec.cute_config(num_tokens))
653 elif backend == "dsv3_fused_a":
654 plan[num_tokens] = ("dsv3_fused_a", None)
655 return plan
656
657
658 def _build_residual_plan(spec: ProjectionSpec) -> dict[int, SkinnyGemmConfig]:
659 return {num_tokens: config for num_tokens, config in spec.residual_configs}
660
661
662 def _is_sm103() -> bool:
663 return current_platform.is_device_capability((10, 3))
664
665
666 def _is_sm107() -> bool:
667 return current_platform.is_device_capability((10, 7))
668
669
670 def _low_latency_table() -> dict[tuple[int, int], ProjectionSpec] | None:
671 """Measured dispatch table for the current device, or None if unsupported."""
672 if _is_sm103() or _is_sm107():
673 # SM107 reuses the SM103 table; see the module docstring.
674 return KIMI_K3_PROJECTIONS
675 if current_platform.is_device_capability((10, 0)):
676 return KIMI_K3_PROJECTIONS_SM100
677 if current_platform.is_device_capability((9, 0)):
678 return KIMI_K3_PROJECTIONS_SM90
679 return None
680
681
682 def _is_packed_row_major(tensor: torch.Tensor) -> bool:
683 return tensor.dim() == 2 and tensor.stride() == (tensor.shape[1], 1)
684
685
686 def _runtime_ok(x: torch.Tensor, weight: torch.Tensor) -> bool:
687 return (
688 x.dim() == 2
689 and x.stride(1) == 1
690 and (x.shape[0] == 1 or x.stride(0) % 8 == 0)
691 and _is_packed_row_major(weight)
692 and x.dtype == torch.bfloat16
693 and weight.dtype == torch.bfloat16
694 and x.is_cuda
695 and weight.is_cuda
696 and x.device == weight.device
697 and x.shape[1] == weight.shape[1]
698 )
699
700
701 def _residual_ok(x: torch.Tensor, weight: torch.Tensor, residual: torch.Tensor) -> bool:
702 return (
703 _is_packed_row_major(x)
704 and residual.dim() == 2
705 and residual.dtype == torch.bfloat16
706 and residual.is_cuda
707 and residual.device == x.device
708 and residual.is_contiguous()
709 and residual.shape == (x.shape[0], weight.shape[0])
710 )
711
712
713 def _run_plan(
714 plan: dict[int, ResolvedCall], x: torch.Tensor, weight: torch.Tensor
715 ) -> torch.Tensor | None:
716 entry = plan.get(x.shape[0])
717 if entry is None:
718 return None
719 backend, config = entry
720 if backend == "cute":
721 if x.shape[0] != 1 and not _is_packed_row_major(x):
722 return None
723 if not shape_dynamic_skinny_gemm.is_available():
724 return None
725 return shape_dynamic_skinny_gemm(x, weight, config, None)
726 if not hasattr(torch.ops._C, "dsv3_fused_a_gemm"):
727 return None
728 if x.shape[0] != 1 and not _is_packed_row_major(x):
729 return None
730 output = torch.empty((x.shape[0], weight.shape[0]), dtype=x.dtype, device=x.device)
731 ops.dsv3_fused_a_gemm(output, x, weight.t(), enable_pdl=True)
732 return output
733
734
735 def run_kda_projection_overlap(
736 hidden_states: torch.Tensor,
737 packed_weight: torch.Tensor,
738 f_b_weight: torch.Tensor,
739 aux_stream: torch.cuda.Stream,
740 events: tuple[torch.cuda.Event, torch.cuda.Event],
741 ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
742 """Run the TP8 KDA decode projection branches concurrently.
743
744 Args:
745 hidden_states: Packed BF16 input with shape ``[M, 7168]``.
746 packed_weight: Existing Q/K/V/G/F_A/beta/pad weight with shape
747 ``[6288, 7168]``.
748 f_b_weight: F_B weight with shape ``[1536, 128]``.
749 aux_stream: Stream for the F_A/beta then F_B branch.
750 events: Start and completion events for the stream fork and join.
751
752 Returns:
753 The Q/K/V/G projection, F_B output, and beta projection.
754
755 """
756 num_tokens = hidden_states.shape[0]
757 qkvg_weight = packed_weight[:_KDA_QKVG_SIZE]
758
759 def run_qkvg() -> torch.Tensor:
760 if config := KDA_QKVG_CONFIGS.get(num_tokens):
761 return shape_dynamic_skinny_gemm(
762 hidden_states,
763 qkvg_weight,
764 config,
765 None,
766 )
767 if num_tokens <= KDA_PROJECTION_OVERLAP_MAX_TOKENS and (
768 backend := _kda_qkvg_flashinfer_backend()
769 ):
770 from flashinfer.gemm import mm_bf16
771
772 return mm_bf16(
773 hidden_states,
774 qkvg_weight.t(),
775 pdl=True,
776 backend=backend,
777 )
778 return torch.mm(hidden_states, qkvg_weight.t())
779
780 def run_fab_fb() -> tuple[torch.Tensor, torch.Tensor]:
781 if num_tokens == 1:
782 projected_fab = shape_dynamic_skinny_gemm(
783 hidden_states,
784 packed_weight[_KDA_QKVG_SIZE : _KDA_QKVG_SIZE + _KDA_FAB_SIZE],
785 KDA_M1_FAB_CONFIG,
786 None,
787 )
788 f_a, beta = projected_fab.split([128, 12], dim=-1)
789 f_a = f_a.as_strided((1, 128), (128, 1))
790 g1 = torch.empty(
791 (1, 1536), dtype=hidden_states.dtype, device=hidden_states.device
792 )
793 ops.dsv3_fused_a_gemm(g1, f_a, f_b_weight.t(), enable_pdl=True)
794 else:
795 from vllm.models.kimi_k3.nvidia.ops.cute_dsl.kda_skinny_gemm import (
796 kda_skinny_gemm,
797 )
798
799 if num_tokens <= KDA_SKINNY_N_MAX_TOKENS:
800 projected_fab = kda_skinny_gemm.run_n(
801 hidden_states,
802 packed_weight[_KDA_QKVG_SIZE:],
803 )
804 else:
805 projected_fab = torch.mm(
806 hidden_states,
807 packed_weight[_KDA_QKVG_SIZE:].t(),
808 )
809 beta = projected_fab[:, 128:140]
810 if num_tokens <= KDA_SKINNY_K_MAX_TOKENS:
811 g1 = kda_skinny_gemm.run_k(projected_fab, f_b_weight)
812 else:
813 g1 = torch.mm(projected_fab[:, :128], f_b_weight.t())
814 return g1, beta
815
816 projected_qkvg, (g1, beta) = maybe_execute_in_parallel(
817 run_qkvg,
818 run_fab_fb,
819 events[0],
820 events[1],
821 aux_stream,
822 )
823 return projected_qkvg, g1, beta
824
825
826 def autotune_kda_qkvg(model: nn.Module) -> None:
827 """Autotune the supported QKVG GEMM before CUDA graph capture."""
828 from vllm.models.kimi_k3.nvidia.kda import KimiK3DeltaAttention
829
830 backend = _kda_qkvg_flashinfer_backend()
831 children: list[KimiK3DeltaAttention] = []
832 weights_by_shape: dict[
833 tuple[int, int, int, torch.dtype, torch.device], torch.Tensor
834 ] = {}
835 for child in model.modules():
836 if not isinstance(child, KimiK3DeltaAttention):
837 continue
838 if child._projection_overlap_max_tokens <= 0:
839 continue
840 children.append(child)
841 if backend is not None:
842 qkvg_weight = child.in_proj_qkvgfab.weight[:_KDA_QKVG_SIZE]
843 shape = (
844 KDA_PROJECTION_OVERLAP_MAX_TOKENS,
845 qkvg_weight.shape[0],
846 qkvg_weight.shape[1],
847 qkvg_weight.dtype,
848 qkvg_weight.device,
849 )
850 weights_by_shape.setdefault(shape, qkvg_weight)
851
852 if backend is not None:
853 from flashinfer.gemm import mm_bf16
854
855 for shape, qkvg_weight in weights_by_shape.items():
856 num_tokens = shape[0]
857 hidden_states = torch.empty(
858 (num_tokens, qkvg_weight.shape[1]),
859 dtype=qkvg_weight.dtype,
860 device=qkvg_weight.device,
861 )
862 mm_bf16(
863 hidden_states,
864 qkvg_weight.t(),
865 pdl=True,
866 backend=backend,
867 )
868 for child in children:
869 child._projection_overlap_max_tokens = KDA_PROJECTION_OVERLAP_MAX_TOKENS
870
871
872 def _run_residual_plan(
873 residual_plan: dict[int, SkinnyGemmConfig],
874 x: torch.Tensor,
875 weight: torch.Tensor,
876 residual: torch.Tensor,
877 ) -> torch.Tensor | None:
878 config = residual_plan.get(x.shape[0])
879 if config is None or not shape_dynamic_skinny_gemm.is_available():
880 return None
881 return shape_dynamic_skinny_gemm(x, weight, config, residual)
882
883
884 def try_low_latency_gemm(
885 x: torch.Tensor,
886 weight: torch.Tensor,
887 residual: torch.Tensor | None = None,
888 ) -> torch.Tensor | None:
889 """Run the shape-selected low-latency kernel, or None to fall back.
890
891 Resolves the plan from the shape table on each call; production installs a
892 precomputed plan (see :func:`enable_kimi_k3_low_latency_gemm`) and does not
893 use this path.
894 """
895 if envs.VLLM_BATCH_INVARIANT or not _runtime_ok(x, weight):
896 return None
897 table = _low_latency_table()
898 if table is None:
899 return None
900 spec = table.get((weight.shape[0], weight.shape[1]))
901 if spec is None:
902 return None
903 if residual is None:
904 return _run_plan(_build_plan(spec), x, weight)
905 if not _residual_ok(x, weight, residual):
906 return None
907 return _run_residual_plan(_build_residual_plan(spec), x, weight, residual)
908
909
910 class _KimiK3LowLatencyApply:
911 """Mixin: try the precomputed plan, else defer to the base method."""
912
913 def __init__(self, plan: dict[int, ResolvedCall]) -> None:
914 super().__init__()
915 self._plan = plan
916
917 def apply(
918 self,
919 layer: nn.Module,
920 x: torch.Tensor,
921 bias: torch.Tensor | None = None,
922 ) -> torch.Tensor:
923 if (
924 bias is None
925 and not envs.VLLM_BATCH_INVARIANT
926 and _runtime_ok(x, layer.weight)
927 ):
928 output = _run_plan(self._plan, x, layer.weight)
929 if output is not None:
930 return output
931 return super().apply(layer, x, bias) # type: ignore[misc]
932
933
934 class KimiK3LowLatencyLinearMethod(_KimiK3LowLatencyApply, UnquantizedLinearMethod):
935 def __init__(
936 self,
937 plan: dict[int, ResolvedCall],
938 residual_plan: dict[int, SkinnyGemmConfig],
939 ) -> None:
940 super().__init__(plan)
941 self._residual_plan = residual_plan
942
943 def apply_with_residual(
944 self,
945 layer: nn.Module,
946 x: torch.Tensor,
947 residual: torch.Tensor,
948 ) -> torch.Tensor:
949 if (
950 not envs.VLLM_BATCH_INVARIANT
951 and _runtime_ok(x, layer.weight)
952 and _residual_ok(x, layer.weight, residual)
953 ):
954 output = _run_residual_plan(self._residual_plan, x, layer.weight, residual)
955 if output is not None:
956 return output
957 return torch.addmm(residual, x, layer.weight.t())
958
959
960 class KimiK3LowLatencyEmbeddingMethod(
961 _KimiK3LowLatencyApply, UnquantizedEmbeddingMethod
962 ):
963 pass
964
965
966 def _enable_kda_projection_overlap(module: nn.Module) -> bool:
967 from vllm.models.kimi_k3.nvidia.kda import KimiK3DeltaAttention
968
969 if envs.VLLM_BATCH_INVARIANT:
970 return False
971
972 enabled = False
973 for child in module.modules():
974 if not isinstance(child, KimiK3DeltaAttention):
975 continue
976 if child.tp_size != _KDA_TP_SIZE:
977 continue
978 packed_weight = child.in_proj_qkvgfab.weight
979 f_b_weight = child.f_b_proj.weight
980 if (
981 type(child.in_proj_qkvgfab.quant_method) is not KimiK3LowLatencyLinearMethod
982 or type(child.f_b_proj.quant_method) is not KimiK3LowLatencyLinearMethod
983 or child._projection_aux_stream is None
984 or child._projection_events is None
985 or packed_weight.shape != (_KDA_PACKED_SIZE, 7168)
986 or f_b_weight.shape != (1536, 128)
987 or packed_weight.dtype != torch.bfloat16
988 or f_b_weight.dtype != torch.bfloat16
989 or not packed_weight.is_cuda
990 or not f_b_weight.is_cuda
991 or not packed_weight.is_contiguous()
992 or not f_b_weight.is_contiguous()
993 ):
994 continue
995 child._projection_overlap_max_tokens = max(KDA_QKVG_CONFIGS)
996 enabled = True
997 return enabled
998
999
1000 def enable_kimi_k3_low_latency_gemm(
1001 module: nn.Module,
1002 dtype: torch.dtype,
1003 ) -> None:
1004 """Install shape-selected low-latency GEMMs and register CuTe warmups.
1005
1006 Modules are matched purely by type, an exactly-unquantized method, and a
1007 local ``(N, K)`` present in the current device's measured table
1008 (:data:`KIMI_K3_PROJECTIONS` on SM103, :data:`KIMI_K3_PROJECTIONS_SM100`
1009 on SM100, :data:`KIMI_K3_PROJECTIONS_SM90` on SM90).
1010 """
1011 if dtype != torch.bfloat16:
1012 return
1013 table = _low_latency_table()
1014 if table is None:
1015 return
1016
1017 warmup_configs: set[SkinnyGemmConfig] = set()
1018 residual_warmup_configs: set[SkinnyGemmConfig] = set()
1019 for child in module.modules():
1020 is_linear = (
1021 isinstance(child, LinearBase)
1022 and type(child.quant_method) is UnquantizedLinearMethod
1023 )
1024 # ParallelLMHead is a VocabParallelEmbedding subclass; embed_tokens is
1025 # the parent type, so isinstance already excludes it.
1026 is_head = (
1027 isinstance(child, ParallelLMHead)
1028 and type(child.quant_method) is UnquantizedEmbeddingMethod
1029 )
1030 if not (is_linear or is_head):
1031 continue
1032 weight = getattr(child, "weight", None)
1033 if weight is None or weight.dim() != 2:
1034 continue
1035 spec = table.get((weight.shape[0], weight.shape[1]))
1036 if spec is None:
1037 continue
1038 if is_linear:
1039 child.quant_method = KimiK3LowLatencyLinearMethod(
1040 _build_plan(spec), _build_residual_plan(spec)
1041 )
1042 else:
1043 child.quant_method = KimiK3LowLatencyEmbeddingMethod(_build_plan(spec))
1044 # Warm up only the configs measured for this module's local (N, K) so a
1045 # TP8 deployment does not compile TP4 configs and vice versa.
1046 warmup_configs.update(config for _, config in spec.cute_configs)
1047 residual_warmup_configs.update(config for _, config in spec.residual_configs)
1048 if _enable_kda_projection_overlap(module):
1049 from vllm.models.kimi_k3.nvidia.ops.cute_dsl.kda_skinny_gemm import (
1050 kda_skinny_gemm,
1051 )
1052
1053 warmup_configs.update((*KDA_QKVG_CONFIGS.values(), KDA_M1_FAB_CONFIG))
1054 kda_skinny_gemm.request_warmup(
1055 set(range(2, KDA_SKINNY_N_MAX_TOKENS + 1)),
1056 set(range(2, KDA_SKINNY_K_MAX_TOKENS + 1)),
1057 )
1058
1059 if shape_dynamic_skinny_gemm.is_available():
1060 if warmup_configs:
1061 shape_dynamic_skinny_gemm.request_warmup_configs(dtype, warmup_configs)
1062 if residual_warmup_configs:
1063 shape_dynamic_skinny_gemm.request_warmup_configs(
1064 dtype, residual_warmup_configs, has_residual=True
1065 )