| 1 | # SPDX-License-Identifier: Apache-2.0 |
| 2 | # SPDX-FileCopyrightText: Copyright contributors to the vLLM project |
| 3 | """Warm up Qwen Triton kernels from the loaded model's compile keys.""" |
| 4 | |
| 5 | from dataclasses import dataclass |
| 6 | from typing import TYPE_CHECKING |
| 7 | |
| 8 | import torch |
| 9 | |
| 10 | from vllm.logger import init_logger |
| 11 | |
| 12 | if TYPE_CHECKING: |
| 13 | from vllm.config import ModelConfig |
| 14 | from vllm.v1.worker.gpu_model_runner import GPUModelRunner |
| 15 | |
| 16 | logger = init_logger(__name__) |
| 17 | |
| 18 | _QWEN_MODEL_TYPES = frozenset( |
| 19 | { |
| 20 | "qwen3_next", |
| 21 | "qwen3_5", |
| 22 | "qwen3_5_text", |
| 23 | "qwen3_5_moe", |
| 24 | "qwen3_5_moe_text", |
| 25 | "qwen4_exp", |
| 26 | "qwen4_exp_text", |
| 27 | } |
| 28 | ) |
| 29 | |
| 30 | # Covers L=1 constexpr, non-divisible runtime L, and divisible runtime L. |
| 31 | _FLA_POST_CONV_WARMUP_LENGTHS = (1, 2, 16) |
| 32 | |
| 33 | |
| 34 | @dataclass(frozen=True) |
| 35 | class _QwenGDNWarmupConfig: |
| 36 | h: int |
| 37 | hv: int |
| 38 | k: int |
| 39 | v: int |
| 40 | conv_kernel_size: int |
| 41 | conv_state: torch.Tensor |
| 42 | conv_dtype: torch.dtype |
| 43 | norm_weight_dtype: torch.dtype |
| 44 | norm_before_gate: bool |
| 45 | norm_activation: str |
| 46 | a_log: torch.Tensor |
| 47 | dt_bias: torch.Tensor |
| 48 | state_stride_token: int |
| 49 | state_dtype: torch.dtype |
| 50 | |
| 51 | @property |
| 52 | def conv_dim(self) -> int: |
| 53 | return 2 * self.h * self.k + self.hv * self.v |
| 54 | |
| 55 | |
| 56 | def _is_non_empty_tensor(value: object) -> bool: |
| 57 | return isinstance(value, torch.Tensor) and value.numel() > 0 |
| 58 | |
| 59 | |
| 60 | def _is_qwen_gdn_layer(module: object) -> bool: |
| 61 | return all( |
| 62 | hasattr(module, attr) |
| 63 | for attr in ( |
| 64 | "num_k_heads", |
| 65 | "num_v_heads", |
| 66 | "head_k_dim", |
| 67 | "head_v_dim", |
| 68 | "conv_kernel_size", |
| 69 | "tp_size", |
| 70 | "kv_cache", |
| 71 | "norm", |
| 72 | "A_log", |
| 73 | "dt_bias", |
| 74 | ) |
| 75 | ) |
| 76 | |
| 77 | |
| 78 | def _iter_qwen_gdn_layers(static_forward_context: object): |
| 79 | if not isinstance(static_forward_context, dict): |
| 80 | return |
| 81 | |
| 82 | for module in static_forward_context.values(): |
| 83 | if _is_qwen_gdn_layer(module): |
| 84 | yield module |
| 85 | |
| 86 | |
| 87 | def _split_qwen_gdn_cache(kv_cache: object) -> tuple[torch.Tensor, torch.Tensor] | None: |
| 88 | if isinstance(kv_cache, (list, tuple)) and len(kv_cache) >= 2: |
| 89 | conv_cache, ssm_state = kv_cache[:2] |
| 90 | if _is_non_empty_tensor(conv_cache) and _is_non_empty_tensor(ssm_state): |
| 91 | return conv_cache, ssm_state |
| 92 | |
| 93 | if isinstance(kv_cache, torch.Tensor) and kv_cache.size(0) >= 2: |
| 94 | conv_cache = kv_cache[0] |
| 95 | ssm_state = kv_cache[1] |
| 96 | if _is_non_empty_tensor(conv_cache) and _is_non_empty_tensor(ssm_state): |
| 97 | return conv_cache, ssm_state |
| 98 | return None |
| 99 | |
| 100 | |
| 101 | def _qwen_gdn_warmup_config( |
| 102 | static_forward_context: object, |
| 103 | ) -> _QwenGDNWarmupConfig | None: |
| 104 | found_layer = False |
| 105 | for layer in _iter_qwen_gdn_layers(static_forward_context): |
| 106 | found_layer = True |
| 107 | cache_tensors = _split_qwen_gdn_cache(getattr(layer, "kv_cache", None)) |
| 108 | if cache_tensors is None: |
| 109 | continue |
| 110 | |
| 111 | conv_cache, ssm_state = cache_tensors |
| 112 | from vllm.model_executor.layers.mamba.mamba_utils import ( |
| 113 | is_conv_state_dim_first, |
| 114 | ) |
| 115 | |
| 116 | conv_state = ( |
| 117 | conv_cache if is_conv_state_dim_first() else conv_cache.transpose(-1, -2) |
| 118 | ) |
| 119 | tp_size = int(layer.tp_size) |
| 120 | h = int(layer.num_k_heads) // tp_size |
| 121 | hv = int(layer.num_v_heads) // tp_size |
| 122 | norm = layer.norm |
| 123 | |
| 124 | return _QwenGDNWarmupConfig( |
| 125 | h=h, |
| 126 | hv=hv, |
| 127 | k=int(layer.head_k_dim), |
| 128 | v=int(layer.head_v_dim), |
| 129 | conv_kernel_size=int(layer.conv_kernel_size), |
| 130 | conv_state=conv_state, |
| 131 | conv_dtype=conv_state.dtype, |
| 132 | norm_weight_dtype=norm.weight.dtype, |
| 133 | norm_before_gate=bool(norm.norm_before_gate), |
| 134 | norm_activation=str(norm.activation), |
| 135 | a_log=layer.A_log, |
| 136 | dt_bias=layer.dt_bias, |
| 137 | state_stride_token=int(ssm_state.stride(0)), |
| 138 | state_dtype=ssm_state.dtype, |
| 139 | ) |
| 140 | |
| 141 | if found_layer: |
| 142 | logger.info("Skipping Qwen GDN Triton warmup: no bound Qwen GDN cache found.") |
| 143 | else: |
| 144 | logger.info("Skipping Qwen GDN Triton warmup: no Qwen GDN layer found.") |
| 145 | return None |
| 146 | |
| 147 | |
| 148 | def _warm_gated_rms_norm_kernel( |
| 149 | device: torch.device, |
| 150 | config: _QwenGDNWarmupConfig, |
| 151 | max_num_tokens: int, |
| 152 | x_dtype: torch.dtype, |
| 153 | ) -> None: |
| 154 | from vllm.third_party.flash_linear_attention.ops.layernorm_guard import ( |
| 155 | warmup_layer_norm_fwd, |
| 156 | ) |
| 157 | |
| 158 | warmup_layer_norm_fwd( |
| 159 | max_num_tokens=max_num_tokens, |
| 160 | rows_per_token=config.hv, |
| 161 | group_size=config.v, |
| 162 | x_dtype=x_dtype, |
| 163 | weight_dtype=config.norm_weight_dtype, |
| 164 | device=device, |
| 165 | norm_before_gate=config.norm_before_gate, |
| 166 | is_rms_norm=True, |
| 167 | activation=config.norm_activation, |
| 168 | ) |
| 169 | |
| 170 | |
| 171 | def _warm_causal_conv1d_fwd_kernel( |
| 172 | device: torch.device, config: _QwenGDNWarmupConfig |
| 173 | ) -> None: |
| 174 | from vllm.model_executor.layers.mamba.ops.causal_conv1d import ( |
| 175 | causal_conv1d_fn, |
| 176 | ) |
| 177 | from vllm.v1.attention.backends.utils import NULL_BLOCK_ID, PAD_SLOT_ID |
| 178 | |
| 179 | x_storage = torch.empty( |
| 180 | (1, config.conv_dim), dtype=config.conv_dtype, device=device |
| 181 | ) |
| 182 | x = x_storage.t() |
| 183 | weight = torch.empty( |
| 184 | (config.conv_dim, config.conv_kernel_size), |
| 185 | dtype=config.conv_dtype, |
| 186 | device=device, |
| 187 | ) |
| 188 | cache_indices = torch.full((1,), NULL_BLOCK_ID, dtype=torch.int32, device=device) |
| 189 | has_initial_state = torch.empty(1, dtype=torch.bool, device=device) |
| 190 | query_start_loc = torch.tensor([0, 1], dtype=torch.int32, device=device) |
| 191 | |
| 192 | causal_conv1d_fn( |
| 193 | x, |
| 194 | weight, |
| 195 | None, |
| 196 | config.conv_state, |
| 197 | query_start_loc, |
| 198 | cache_indices=cache_indices, |
| 199 | has_initial_state=has_initial_state, |
| 200 | activation="silu", |
| 201 | pad_slot_id=PAD_SLOT_ID, |
| 202 | null_block_id=NULL_BLOCK_ID, |
| 203 | metadata=None, |
| 204 | validate_data=False, |
| 205 | ) |
| 206 | |
| 207 | |
| 208 | def _warm_fused_post_conv_kernel( |
| 209 | device: torch.device, config: _QwenGDNWarmupConfig |
| 210 | ) -> None: |
| 211 | from vllm.third_party.flash_linear_attention.ops.fused_gdn_prefill_post_conv import ( # noqa: E501 |
| 212 | fused_post_conv_prep, |
| 213 | ) |
| 214 | |
| 215 | qkv_dim = 2 * config.h * config.k + config.hv * config.v |
| 216 | for length in _FLA_POST_CONV_WARMUP_LENGTHS: |
| 217 | conv_output = torch.empty( |
| 218 | (length, qkv_dim), dtype=config.conv_dtype, device=device |
| 219 | ) |
| 220 | a = torch.empty((length, config.hv), dtype=config.conv_dtype, device=device) |
| 221 | b = torch.empty_like(a) |
| 222 | |
| 223 | fused_post_conv_prep( |
| 224 | conv_output, |
| 225 | a, |
| 226 | b, |
| 227 | config.a_log, |
| 228 | config.dt_bias, |
| 229 | config.h, |
| 230 | config.k, |
| 231 | config.v, |
| 232 | apply_l2norm=True, |
| 233 | output_g_exp=False, |
| 234 | ) |
| 235 | |
| 236 | |
| 237 | def _warm_fused_sigmoid_gating_delta_rule_update_kernel( |
| 238 | device: torch.device, |
| 239 | config: _QwenGDNWarmupConfig, |
| 240 | ) -> None: |
| 241 | from vllm.third_party.flash_linear_attention.ops.fused_sigmoid_gating import ( |
| 242 | fused_sigmoid_gating_delta_rule_update, |
| 243 | ) |
| 244 | |
| 245 | q = torch.empty((1, 1, config.h, config.k), dtype=config.conv_dtype, device=device) |
| 246 | k = torch.empty_like(q) |
| 247 | v = torch.empty((1, 1, config.hv, config.v), dtype=config.conv_dtype, device=device) |
| 248 | a = torch.empty((1, 1, config.hv), dtype=config.conv_dtype, device=device) |
| 249 | b = torch.empty_like(a) |
| 250 | state = torch.empty( |
| 251 | (1, config.state_stride_token), |
| 252 | dtype=config.state_dtype, |
| 253 | device=device, |
| 254 | ) |
| 255 | cu_seqlens = torch.tensor([0, 1], dtype=torch.int32, device=device) |
| 256 | ssm_state_indices = torch.empty((1, 1), dtype=torch.int32, device=device) |
| 257 | ssm_state_indices.zero_() |
| 258 | |
| 259 | fused_sigmoid_gating_delta_rule_update( |
| 260 | A_log=config.a_log, |
| 261 | a=a, |
| 262 | b=b, |
| 263 | dt_bias=config.dt_bias, |
| 264 | q=q, |
| 265 | k=k, |
| 266 | v=v, |
| 267 | beta=1.0, |
| 268 | threshold=20.0, |
| 269 | initial_state=state, |
| 270 | inplace_final_state=True, |
| 271 | cu_seqlens=cu_seqlens, |
| 272 | ssm_state_indices=ssm_state_indices, |
| 273 | use_qk_l2norm_in_kernel=True, |
| 274 | is_kda=False, |
| 275 | ) |
| 276 | |
| 277 | |
| 278 | def _synchronize_device(device: torch.device) -> None: |
| 279 | if device.type == "cuda": |
| 280 | torch.accelerator.synchronize(device) |
| 281 | |
| 282 | |
| 283 | @torch.inference_mode() |
| 284 | def qwen_triton_warmup( |
| 285 | runner: "GPUModelRunner", |
| 286 | model_config: "ModelConfig", |
| 287 | ) -> None: |
| 288 | """Warm Qwen GDN Triton kernels reported by the JIT monitor.""" |
| 289 | model_type = getattr(model_config.hf_text_config, "model_type", "") or getattr( |
| 290 | model_config.hf_config, "model_type", "" |
| 291 | ) |
| 292 | if model_type not in _QWEN_MODEL_TYPES: |
| 293 | return |
| 294 | |
| 295 | device = runner.device |
| 296 | logger.info("Warming up Qwen GDN Triton kernels for model_type=%s.", model_type) |
| 297 | |
| 298 | gdn_config = _qwen_gdn_warmup_config( |
| 299 | runner.compilation_config.static_forward_context |
| 300 | ) |
| 301 | if gdn_config is None: |
| 302 | return |
| 303 | |
| 304 | max_num_tokens = max(1, int(runner.max_num_tokens)) |
| 305 | _warm_gated_rms_norm_kernel(device, gdn_config, max_num_tokens, model_config.dtype) |
| 306 | _warm_causal_conv1d_fwd_kernel(device, gdn_config) |
| 307 | _warm_fused_post_conv_kernel(device, gdn_config) |
| 308 | # Pooling only runs full prefills; the decode update kernel is unused. |
| 309 | if not runner.is_pooling_model: |
| 310 | _warm_fused_sigmoid_gating_delta_rule_update_kernel(device, gdn_config) |
| 311 | _synchronize_device(device) |