| 1 | # SPDX-License-Identifier: Apache-2.0 |
| 2 | # SPDX-FileCopyrightText: Copyright contributors to the vLLM project |
| 3 | """Startup compilation of Kimi-K3 vision FA4 kernels.""" |
| 4 | |
| 5 | from __future__ import annotations |
| 6 | |
| 7 | import math |
| 8 | from collections.abc import Iterator |
| 9 | from dataclasses import dataclass |
| 10 | from functools import partial |
| 11 | |
| 12 | import torch |
| 13 | |
| 14 | from vllm.model_executor.warmup.cutedsl_warmup import ( |
| 15 | CuTeDSLCompileUnit, |
| 16 | register_cutedsl_warmup_provider, |
| 17 | ) |
| 18 | from vllm.platforms import current_platform |
| 19 | |
| 20 | _FA4_TILE_SIZE = 128 |
| 21 | _FA4_MAX_SPLITS = 128 |
| 22 | |
| 23 | |
| 24 | @dataclass(frozen=True) |
| 25 | class KimiK3VisionFA4WarmupConfig: |
| 26 | num_heads: int |
| 27 | head_dim: int |
| 28 | dtype: torch.dtype |
| 29 | max_batch_size: int |
| 30 | max_seqlen: int |
| 31 | |
| 32 | |
| 33 | @dataclass(frozen=True) |
| 34 | class _FA4WarmupProbe: |
| 35 | batch_size: int |
| 36 | max_seqlen: int |
| 37 | dispatch_key: tuple[int, bool, int | None] |
| 38 | |
| 39 | |
| 40 | def _combine_log_max_splits(head_dim: int, num_splits: int) -> int: |
| 41 | k_block_size = 64 if head_dim <= 64 else 128 |
| 42 | tile_m = 8 if k_block_size == 128 else 16 |
| 43 | min_log_max_splits = 5 if tile_m == 8 else 4 |
| 44 | return max(math.ceil(math.log2(num_splits)), min_log_max_splits) |
| 45 | |
| 46 | |
| 47 | def _get_dispatch( |
| 48 | config: KimiK3VisionFA4WarmupConfig, |
| 49 | *, |
| 50 | batch_size: int, |
| 51 | max_seqlen: int, |
| 52 | num_sms: int, |
| 53 | ) -> tuple[int, bool, int | None]: |
| 54 | # These are the shape-dependent fields in FA4's forward and combine |
| 55 | # compile keys for noncausal varlen MHA. Compile units still pass |
| 56 | # num_splits=0 so the production selector makes the final decision. |
| 57 | q_stage = 2 if max_seqlen > _FA4_TILE_SIZE else 1 |
| 58 | effective_q_tile = q_stage * _FA4_TILE_SIZE |
| 59 | num_m_blocks = math.ceil(max_seqlen / effective_q_tile) |
| 60 | num_n_blocks = math.ceil(max_seqlen / _FA4_TILE_SIZE) |
| 61 | |
| 62 | if num_n_blocks <= 4: |
| 63 | num_splits = 1 |
| 64 | else: |
| 65 | total_m_blocks = batch_size * config.num_heads * num_m_blocks |
| 66 | num_splits = min( |
| 67 | num_sms // total_m_blocks, |
| 68 | _FA4_MAX_SPLITS, |
| 69 | num_n_blocks, |
| 70 | ) |
| 71 | |
| 72 | is_split_kv = num_splits > 1 |
| 73 | combine_key = ( |
| 74 | _combine_log_max_splits(config.head_dim, num_splits) if is_split_kv else None |
| 75 | ) |
| 76 | return q_stage, is_split_kv, combine_key |
| 77 | |
| 78 | |
| 79 | def _get_warmup_probes( |
| 80 | config: KimiK3VisionFA4WarmupConfig, |
| 81 | *, |
| 82 | num_sms: int, |
| 83 | ) -> tuple[_FA4WarmupProbe, ...]: |
| 84 | if config.max_batch_size <= 0 or config.max_seqlen <= 0: |
| 85 | return () |
| 86 | |
| 87 | probes: dict[tuple[int, bool, int | None], _FA4WarmupProbe] = {} |
| 88 | |
| 89 | def add_probe(batch_size: int, max_seqlen: int) -> None: |
| 90 | dispatch_key = _get_dispatch( |
| 91 | config, |
| 92 | batch_size=batch_size, |
| 93 | max_seqlen=max_seqlen, |
| 94 | num_sms=num_sms, |
| 95 | ) |
| 96 | if dispatch_key not in probes: |
| 97 | probes[dispatch_key] = _FA4WarmupProbe( |
| 98 | batch_size=batch_size, |
| 99 | max_seqlen=max_seqlen, |
| 100 | dispatch_key=dispatch_key, |
| 101 | ) |
| 102 | |
| 103 | add_probe(batch_size=1, max_seqlen=1) |
| 104 | if config.max_seqlen > _FA4_TILE_SIZE: |
| 105 | add_probe(batch_size=1, max_seqlen=_FA4_TILE_SIZE + 1) |
| 106 | |
| 107 | max_n_blocks = math.ceil(config.max_seqlen / _FA4_TILE_SIZE) |
| 108 | for num_n_blocks in range(5, max_n_blocks + 1): |
| 109 | max_seqlen = min( |
| 110 | (num_n_blocks - 1) * _FA4_TILE_SIZE + 1, |
| 111 | config.max_seqlen, |
| 112 | ) |
| 113 | num_m_blocks = math.ceil(max_seqlen / (2 * _FA4_TILE_SIZE)) |
| 114 | max_split_batch_size = min( |
| 115 | config.max_batch_size, |
| 116 | num_sms // (2 * config.num_heads * num_m_blocks), |
| 117 | ) |
| 118 | if max_split_batch_size == 0: |
| 119 | break |
| 120 | for batch_size in range(1, max_split_batch_size + 1): |
| 121 | add_probe(batch_size, max_seqlen) |
| 122 | |
| 123 | return tuple(probes.values()) |
| 124 | |
| 125 | |
| 126 | def _iter_compile_units( |
| 127 | config: KimiK3VisionFA4WarmupConfig, |
| 128 | ) -> Iterator[CuTeDSLCompileUnit]: |
| 129 | device_id = current_platform.current_device() |
| 130 | num_sms = current_platform.num_compute_units(device_id) |
| 131 | for probe in _get_warmup_probes(config, num_sms=num_sms): |
| 132 | yield CuTeDSLCompileUnit( |
| 133 | name="kimi_k3_vision_fa4", |
| 134 | key=("kimi_k3_vision_fa4", config, probe.dispatch_key), |
| 135 | compile=partial(_compile, config, probe), |
| 136 | ) |
| 137 | |
| 138 | |
| 139 | def _compile( |
| 140 | config: KimiK3VisionFA4WarmupConfig, |
| 141 | probe: _FA4WarmupProbe, |
| 142 | ) -> None: |
| 143 | from vllm.vllm_flash_attn import flash_attn_varlen_func |
| 144 | |
| 145 | device = current_platform.current_device() |
| 146 | total_tokens = probe.batch_size * probe.max_seqlen |
| 147 | qkv = torch.empty( |
| 148 | 3, |
| 149 | total_tokens, |
| 150 | config.num_heads, |
| 151 | config.head_dim, |
| 152 | device=device, |
| 153 | dtype=config.dtype, |
| 154 | ) |
| 155 | cu_seqlens = torch.arange( |
| 156 | 0, |
| 157 | total_tokens + 1, |
| 158 | probe.max_seqlen, |
| 159 | device=qkv.device, |
| 160 | dtype=torch.int32, |
| 161 | ) |
| 162 | flash_attn_varlen_func( |
| 163 | qkv[0], |
| 164 | qkv[1], |
| 165 | qkv[2], |
| 166 | cu_seqlens_q=cu_seqlens, |
| 167 | cu_seqlens_k=cu_seqlens, |
| 168 | max_seqlen_q=probe.max_seqlen, |
| 169 | max_seqlen_k=probe.max_seqlen, |
| 170 | dropout_p=0.0, |
| 171 | causal=False, |
| 172 | softmax_scale=config.head_dim**-0.5, |
| 173 | fa_version=4, |
| 174 | num_splits=0, |
| 175 | ) |
| 176 | |
| 177 | |
| 178 | class _WarmupProvider: |
| 179 | def __init__(self) -> None: |
| 180 | self.configs: set[KimiK3VisionFA4WarmupConfig] = set() |
| 181 | |
| 182 | def get_cutedsl_warmup_compile_units(self) -> tuple[CuTeDSLCompileUnit, ...]: |
| 183 | return tuple( |
| 184 | unit for config in self.configs for unit in _iter_compile_units(config) |
| 185 | ) |
| 186 | |
| 187 | |
| 188 | _PROVIDER = _WarmupProvider() |
| 189 | |
| 190 | |
| 191 | def register_kimi_k3_vision_fa4_warmup( |
| 192 | config: KimiK3VisionFA4WarmupConfig, |
| 193 | ) -> None: |
| 194 | _PROVIDER.configs.add(config) |
| 195 | register_cutedsl_warmup_provider(_PROVIDER) |