| 1 | # SPDX-License-Identifier: Apache-2.0 |
| 2 | # SPDX-FileCopyrightText: Copyright contributors to the vLLM project |
| 3 | |
| 4 | from dataclasses import dataclass |
| 5 | from typing import Any |
| 6 | |
| 7 | import torch |
| 8 | |
| 9 | from vllm.model_executor.warmup.jit_warmup import kernel_launcher |
| 10 | from vllm.model_executor.warmup.jit_warmup_triton_helper import ( |
| 11 | LaunchSpec, |
| 12 | TritonWarmupTensor, |
| 13 | VllmTritonJitKernel, |
| 14 | ) |
| 15 | from vllm.platforms import current_platform |
| 16 | from vllm.triton_utils import tl, triton |
| 17 | from vllm.utils.import_utils import has_cutedsl |
| 18 | |
| 19 | # MXFP4: 32 elements per block, packed 2 nibbles per byte, ue8m0 block scale. |
| 20 | MXFP4_BLOCK_SIZE = 32 |
| 21 | |
| 22 | |
| 23 | @triton.jit |
| 24 | def _get_cos_sin( |
| 25 | cos_sin_cache_ptr, |
| 26 | cos_sin_cache_stride, |
| 27 | pos, |
| 28 | HALF_ROT_DIM: tl.constexpr, |
| 29 | ): |
| 30 | block = tl.arange(0, HALF_ROT_DIM) |
| 31 | cos = tl.load(cos_sin_cache_ptr + pos * cos_sin_cache_stride + block) |
| 32 | cos = cos.to(tl.float32) |
| 33 | sin = tl.load(cos_sin_cache_ptr + pos * cos_sin_cache_stride + block + HALF_ROT_DIM) |
| 34 | sin = sin.to(tl.float32) |
| 35 | return cos, sin |
| 36 | |
| 37 | |
| 38 | @triton.jit |
| 39 | def _fp32x2_to_fp4x2(x_lo, x_hi): |
| 40 | # NOTE: $1 is high nibble, $2 is low nibble |
| 41 | return tl.inline_asm_elementwise( |
| 42 | """ |
| 43 | { |
| 44 | .reg .b8 tmp; |
| 45 | cvt.rn.satfinite.e2m1x2.f32 tmp, $1, $2; |
| 46 | cvt.u32.u8 $0, tmp; |
| 47 | } |
| 48 | """, |
| 49 | constraints="=r,f,f", |
| 50 | args=[x_hi, x_lo], |
| 51 | dtype=tl.uint32, |
| 52 | is_pure=True, |
| 53 | pack=1, |
| 54 | ).to(tl.uint8) |
| 55 | |
| 56 | |
| 57 | @triton.jit |
| 58 | def _quantize_mxfp4_pair(x_lo, x_hi): |
| 59 | """Quantize a block of MXFP4_BLOCK_SIZE fp32 values given as two |
| 60 | interleaved halves (x_lo = values at even positions in the block, |
| 61 | x_hi = values at odd positions). Returns: |
| 62 | - packed : uint8[BLOCK/2] (low nibble = quant(x_lo), high = quant(x_hi)) |
| 63 | - ue8m0 : scalar uint8 (block scale = 2^(ue8m0 - 127)) |
| 64 | """ |
| 65 | amax = tl.maximum(tl.max(tl.abs(x_lo)), tl.max(tl.abs(x_hi))) |
| 66 | # 6 * 2^-126 is from https://huggingface.co/deepseek-ai/DeepSeek-V4-Pro/blob/main/inference/kernel.py#L163 |
| 67 | amax = tl.maximum(amax, 6.0 * (2**-126)) |
| 68 | # ue8m0 block scale: 2^ceil(log2(amax/6.0)). |
| 69 | log2_ratio = tl.math.ceil(tl.math.log2(amax * (1.0 / 6.0))) |
| 70 | log2_ratio = tl.minimum(tl.maximum(log2_ratio, -127.0), 127.0) |
| 71 | scale = tl.math.exp2(log2_ratio) |
| 72 | ue8m0 = (log2_ratio + 127.0).to(tl.uint8) |
| 73 | |
| 74 | inv_scale = 1.0 / scale |
| 75 | packed = _fp32x2_to_fp4x2(x_lo * inv_scale, x_hi * inv_scale) |
| 76 | return packed, ue8m0 |
| 77 | |
| 78 | |
| 79 | class FusedIndexerQRopeQuantTritonKernel( |
| 80 | VllmTritonJitKernel["FusedIndexerQRopeQuantTritonKernel.CompileKey"] |
| 81 | ): |
| 82 | @dataclass(frozen=True) |
| 83 | class CompileKey: |
| 84 | dtype: torch.dtype |
| 85 | num_heads: int |
| 86 | index_q_half_rot_dim: int |
| 87 | index_q_head_dim: int |
| 88 | fp8_max: float |
| 89 | use_fnuz: bool |
| 90 | use_explicit_fma: bool |
| 91 | |
| 92 | @staticmethod |
| 93 | @triton.jit |
| 94 | def kernel( |
| 95 | pos_ptr, |
| 96 | # Index Q RoPE |
| 97 | index_q_ptr, |
| 98 | index_q_stride0, |
| 99 | index_q_stride1, |
| 100 | index_q_cos_sin_ptr, |
| 101 | index_q_cos_sin_stride, |
| 102 | INDEX_Q_HALF_ROT_DIM: tl.constexpr, |
| 103 | # Index Q Quantize |
| 104 | index_q_fp8_ptr, |
| 105 | index_q_fp8_stride0, |
| 106 | index_q_fp8_stride1, |
| 107 | INDEX_Q_HEAD_DIM: tl.constexpr, |
| 108 | # Index weights |
| 109 | index_weights_ptr, |
| 110 | index_weights_stride, |
| 111 | index_weights_softmax_scale, |
| 112 | index_weights_head_scale, |
| 113 | index_weights_out_ptr, |
| 114 | index_weights_out_stride, |
| 115 | FP8_MAX: tl.constexpr = 448.0, |
| 116 | USE_FNUZ: tl.constexpr = False, |
| 117 | USE_EXPLICIT_FMA: tl.constexpr = False, |
| 118 | ): |
| 119 | # Layout matches the unfused reference (DeepseekV4ScalingRotaryEmbedding |
| 120 | # + per_token_group_quant_fp8): GPT-J interleaved RoPE applied to the |
| 121 | # LAST rope_dim dims of each head; the leading [0, NOPE_DIM) is passed |
| 122 | # through unchanged. |
| 123 | INDEX_Q_ROT_DIM: tl.constexpr = 2 * INDEX_Q_HALF_ROT_DIM |
| 124 | INDEX_Q_NOPE_DIM: tl.constexpr = INDEX_Q_HEAD_DIM - INDEX_Q_ROT_DIM |
| 125 | tl.static_assert(INDEX_Q_NOPE_DIM >= 0) |
| 126 | |
| 127 | tok_idx = tl.program_id(0) |
| 128 | head_idx = tl.program_id(1) |
| 129 | |
| 130 | pos = tl.load(pos_ptr + tok_idx) |
| 131 | cos, sin = _get_cos_sin( |
| 132 | index_q_cos_sin_ptr, |
| 133 | index_q_cos_sin_stride, |
| 134 | pos, |
| 135 | INDEX_Q_HALF_ROT_DIM, |
| 136 | ) |
| 137 | half_offset = tl.arange(0, INDEX_Q_HALF_ROT_DIM) |
| 138 | base_ptr = index_q_ptr + tok_idx * index_q_stride0 + head_idx * index_q_stride1 |
| 139 | |
| 140 | # Interleaved (GPT-J) RoPE on dims [NOPE_DIM, HEAD_DIM): |
| 141 | # even = q[NOPE_DIM + 2*i], odd = q[NOPE_DIM + 2*i + 1] |
| 142 | rot_base = base_ptr + INDEX_Q_NOPE_DIM |
| 143 | x_even = tl.load(rot_base + half_offset * 2).to(tl.float32) |
| 144 | x_odd = tl.load(rot_base + half_offset * 2 + 1).to(tl.float32) |
| 145 | if USE_EXPLICIT_FMA: |
| 146 | # Match HIP rotary_embedding contraction before bf16 materialization. |
| 147 | r_even = tl.fma(x_even, cos, -(x_odd * sin)) |
| 148 | r_odd = tl.fma(x_odd, cos, x_even * sin) |
| 149 | else: |
| 150 | r_even = x_even * cos - x_odd * sin |
| 151 | r_odd = x_odd * cos + x_even * sin |
| 152 | |
| 153 | # Match reference numerics: fp32 → bf16 → fp32 before the ue8m0 absmax. |
| 154 | # Same pattern as the K-side compressor kernel (fused_compress_quant_cache.py). |
| 155 | r_even = r_even.to(tl.bfloat16).to(tl.float32) |
| 156 | r_odd = r_odd.to(tl.bfloat16).to(tl.float32) |
| 157 | |
| 158 | amax = tl.maximum(tl.max(tl.abs(r_even)), tl.max(tl.abs(r_odd))) |
| 159 | if INDEX_Q_NOPE_DIM > 0: |
| 160 | nope_offset = tl.arange(0, INDEX_Q_NOPE_DIM) |
| 161 | x_nope = tl.load(base_ptr + nope_offset).to(tl.float32) |
| 162 | amax = tl.maximum(amax, tl.max(tl.abs(x_nope))) |
| 163 | index_q_scale = tl.div_rn(tl.maximum(amax, 1e-4), FP8_MAX) |
| 164 | index_q_scale = tl.math.exp2(tl.math.ceil(tl.math.log2(index_q_scale))) |
| 165 | |
| 166 | # Store quantized values to index_q_fp8. FNUZ (e4m3fnuz) on gfx942, OCP |
| 167 | # (e4m3fn) elsewhere -- matches the K cache. |
| 168 | fp8_dtype = tl.float8e4b8 if USE_FNUZ else tl.float8e4nv |
| 169 | fp8_base_ptr = ( |
| 170 | index_q_fp8_ptr |
| 171 | + tok_idx * index_q_fp8_stride0 |
| 172 | + head_idx * index_q_fp8_stride1 |
| 173 | ) |
| 174 | if INDEX_Q_NOPE_DIM > 0: |
| 175 | tl.store( |
| 176 | fp8_base_ptr + nope_offset, |
| 177 | tl.div_rn(x_nope, index_q_scale).to(fp8_dtype), |
| 178 | ) |
| 179 | fp8_rot_base = fp8_base_ptr + INDEX_Q_NOPE_DIM |
| 180 | tl.store( |
| 181 | fp8_rot_base + half_offset * 2, |
| 182 | tl.div_rn(r_even, index_q_scale).to(fp8_dtype), |
| 183 | ) |
| 184 | tl.store( |
| 185 | fp8_rot_base + half_offset * 2 + 1, |
| 186 | tl.div_rn(r_odd, index_q_scale).to(fp8_dtype), |
| 187 | ) |
| 188 | |
| 189 | # FP8 weight-fold contract: |
| 190 | # index_weights_out = index_weights * q_scale * softmax_scale * head_scale |
| 191 | # The per-token-per-head q_scale (fp32) IS folded into the output weights |
| 192 | # here because FP8 Q is stored WITHOUT a companion scale tensor — the |
| 193 | # downstream fp8_fp4_mqa_logits/fp8_fp4_paged_mqa_logits kernels use |
| 194 | # `weights` to apply per-token Q scale inline. See the MXFP4 kernel below for |
| 195 | # contrasting convention (scales live with the Q values, weights are NOT |
| 196 | # q-scaled). |
| 197 | index_weights = tl.load( |
| 198 | index_weights_ptr + tok_idx * index_weights_stride + head_idx |
| 199 | ) |
| 200 | index_weights = index_weights.to(tl.float32) |
| 201 | index_weights *= index_q_scale |
| 202 | index_weights *= index_weights_softmax_scale |
| 203 | index_weights *= index_weights_head_scale |
| 204 | tl.store( |
| 205 | index_weights_out_ptr + tok_idx * index_weights_out_stride + head_idx, |
| 206 | index_weights, |
| 207 | ) |
| 208 | |
| 209 | def dispatch( # type: ignore[override] |
| 210 | self, |
| 211 | *, |
| 212 | dtype: torch.dtype, |
| 213 | num_heads: int, |
| 214 | head_dim: int, |
| 215 | rope_dim: int, |
| 216 | use_fnuz: bool, |
| 217 | ) -> CompileKey: |
| 218 | return self.CompileKey( |
| 219 | dtype=dtype, |
| 220 | num_heads=num_heads, |
| 221 | index_q_half_rot_dim=rope_dim // 2, |
| 222 | index_q_head_dim=head_dim, |
| 223 | fp8_max=224.0 if use_fnuz else 448.0, |
| 224 | use_fnuz=use_fnuz, |
| 225 | use_explicit_fma=current_platform.is_rocm(), |
| 226 | ) |
| 227 | |
| 228 | def get_warmup_keys(self, vllm_config: Any) -> list[CompileKey]: |
| 229 | hf_config = vllm_config.model_config.hf_config |
| 230 | num_heads = int(getattr(hf_config, "index_n_heads", 0) or 0) |
| 231 | head_dim = int(getattr(hf_config, "index_head_dim", 0) or 0) |
| 232 | rope_dim = int(getattr(hf_config, "qk_rope_head_dim", 0) or 0) |
| 233 | if num_heads <= 0 or head_dim <= 0 or rope_dim <= 0: |
| 234 | return [] |
| 235 | |
| 236 | return self._trace_dispatch(self.dispatch)( |
| 237 | dtype=vllm_config.model_config.dtype, |
| 238 | num_heads=num_heads, |
| 239 | head_dim=head_dim, |
| 240 | rope_dim=rope_dim, |
| 241 | use_fnuz=current_platform.fp8_dtype() == torch.float8_e4m3fnuz, |
| 242 | ) |
| 243 | |
| 244 | def warmup_inputs(self, compile_key: CompileKey) -> dict[str, Any]: |
| 245 | q_stride0 = compile_key.num_heads * compile_key.index_q_head_dim |
| 246 | return dict( |
| 247 | positions=TritonWarmupTensor(torch.int64), |
| 248 | index_q=TritonWarmupTensor( |
| 249 | compile_key.dtype, |
| 250 | shape=(1, compile_key.num_heads, compile_key.index_q_head_dim), |
| 251 | strides=(q_stride0, compile_key.index_q_head_dim, 1), |
| 252 | ), |
| 253 | index_q_cos_sin_cache=TritonWarmupTensor( |
| 254 | torch.float32, |
| 255 | shape=(1, compile_key.index_q_half_rot_dim * 2), |
| 256 | ), |
| 257 | index_weights=TritonWarmupTensor( |
| 258 | compile_key.dtype, |
| 259 | shape=(1, compile_key.num_heads), |
| 260 | ), |
| 261 | index_weights_softmax_scale=1.0, |
| 262 | index_weights_head_scale=1.0, |
| 263 | index_q_fp8=TritonWarmupTensor( |
| 264 | current_platform.fp8_dtype(), |
| 265 | shape=(1, compile_key.num_heads, compile_key.index_q_head_dim), |
| 266 | strides=(q_stride0, compile_key.index_q_head_dim, 1), |
| 267 | ), |
| 268 | index_weights_out=TritonWarmupTensor( |
| 269 | torch.float32, |
| 270 | shape=(1, compile_key.num_heads), |
| 271 | ), |
| 272 | fp8_max=compile_key.fp8_max, |
| 273 | use_fnuz=compile_key.use_fnuz, |
| 274 | ) |
| 275 | |
| 276 | @kernel_launcher |
| 277 | def __call__( |
| 278 | self, |
| 279 | positions: torch.Tensor, |
| 280 | index_q: torch.Tensor, |
| 281 | index_q_cos_sin_cache: torch.Tensor, |
| 282 | index_weights: torch.Tensor, |
| 283 | index_weights_softmax_scale: float, |
| 284 | index_weights_head_scale: float, |
| 285 | index_q_fp8: torch.Tensor, |
| 286 | index_weights_out: torch.Tensor, |
| 287 | *, |
| 288 | fp8_max: float, |
| 289 | use_fnuz: bool, |
| 290 | ) -> LaunchSpec: |
| 291 | num_tokens = positions.shape[0] |
| 292 | num_index_q_heads = index_q.shape[1] |
| 293 | return (num_tokens, num_index_q_heads), dict( |
| 294 | pos_ptr=positions, |
| 295 | index_q_stride0=index_q.stride(0), |
| 296 | index_q_stride1=index_q.stride(1), |
| 297 | index_q_cos_sin_ptr=index_q_cos_sin_cache, |
| 298 | index_q_cos_sin_stride=index_q_cos_sin_cache.stride(0), |
| 299 | INDEX_Q_HALF_ROT_DIM=index_q_cos_sin_cache.shape[-1] // 2, |
| 300 | index_q_fp8_stride0=index_q_fp8.stride(0), |
| 301 | index_q_fp8_stride1=index_q_fp8.stride(1), |
| 302 | INDEX_Q_HEAD_DIM=index_q.shape[2], |
| 303 | index_weights_stride=index_weights.stride(0), |
| 304 | index_weights_out_stride=index_weights_out.stride(0), |
| 305 | FP8_MAX=fp8_max, |
| 306 | USE_FNUZ=use_fnuz, |
| 307 | USE_EXPLICIT_FMA=current_platform.is_rocm(), |
| 308 | num_warps=1, |
| 309 | ) |
| 310 | |
| 311 | |
| 312 | def _indexer_weights_out_dtypes(vllm_config: Any) -> tuple[torch.dtype, ...]: |
| 313 | """Weights dtypes the model's indexer layers ask for: fp32 for the dense |
| 314 | scoring kernels, plus bf16 when the DeepSeek V4.1 sparse-logits indexer |
| 315 | (`SparseMQAIndexer`) is enabled.""" |
| 316 | if vllm_config.attention_config.indexer_sparse_logits: |
| 317 | return (torch.float32, torch.bfloat16) |
| 318 | return (torch.float32,) |
| 319 | |
| 320 | |
| 321 | class FusedIndexerQRopeMxFp4TritonKernel( |
| 322 | VllmTritonJitKernel["FusedIndexerQRopeMxFp4TritonKernel.CompileKey"] |
| 323 | ): |
| 324 | @dataclass(frozen=True) |
| 325 | class CompileKey: |
| 326 | dtype: torch.dtype |
| 327 | weights_out_dtype: torch.dtype |
| 328 | num_heads: int |
| 329 | index_q_half_rot_dim: int |
| 330 | index_q_head_dim: int |
| 331 | mxfp4_block: int |
| 332 | |
| 333 | @staticmethod |
| 334 | @triton.jit |
| 335 | def kernel( |
| 336 | pos_ptr, |
| 337 | # Index Q RoPE input (fp/bf16) |
| 338 | index_q_ptr, |
| 339 | index_q_stride0, |
| 340 | index_q_stride1, |
| 341 | index_q_cos_sin_ptr, |
| 342 | index_q_cos_sin_stride, |
| 343 | INDEX_Q_HALF_ROT_DIM: tl.constexpr, |
| 344 | # MXFP4 Q outputs |
| 345 | index_q_mxfp4_ptr, # uint8, (T, H, HEAD_DIM // 2) |
| 346 | index_q_mxfp4_stride0, |
| 347 | index_q_mxfp4_stride1, |
| 348 | index_q_scale_ptr, # uint8 ue8m0, (T, H, HEAD_DIM // BLOCK) |
| 349 | index_q_scale_stride0, |
| 350 | index_q_scale_stride1, |
| 351 | INDEX_Q_HEAD_DIM: tl.constexpr, |
| 352 | MXFP4_BLOCK: tl.constexpr, |
| 353 | # Weights (NO per-token q_scale fold for MXFP4; per-block scales stay |
| 354 | # with the Q values in the output scale tensor). |
| 355 | index_weights_ptr, |
| 356 | index_weights_stride, |
| 357 | index_weights_softmax_scale, |
| 358 | index_weights_head_scale, |
| 359 | index_weights_out_ptr, |
| 360 | index_weights_out_stride, |
| 361 | ): |
| 362 | INDEX_Q_ROT_DIM: tl.constexpr = 2 * INDEX_Q_HALF_ROT_DIM |
| 363 | INDEX_Q_NOPE_DIM: tl.constexpr = INDEX_Q_HEAD_DIM - INDEX_Q_ROT_DIM |
| 364 | NUM_NOPE_BLOCKS: tl.constexpr = INDEX_Q_NOPE_DIM // MXFP4_BLOCK |
| 365 | NUM_ROPE_BLOCKS: tl.constexpr = INDEX_Q_ROT_DIM // MXFP4_BLOCK |
| 366 | HALF_BLOCK: tl.constexpr = MXFP4_BLOCK // 2 |
| 367 | tl.static_assert(INDEX_Q_NOPE_DIM >= 0) |
| 368 | tl.static_assert(INDEX_Q_NOPE_DIM % MXFP4_BLOCK == 0) |
| 369 | tl.static_assert(INDEX_Q_ROT_DIM % MXFP4_BLOCK == 0) |
| 370 | tl.static_assert(MXFP4_BLOCK % 2 == 0) |
| 371 | |
| 372 | tok_idx = tl.program_id(0) |
| 373 | head_idx = tl.program_id(1) |
| 374 | |
| 375 | pos = tl.load(pos_ptr + tok_idx) |
| 376 | |
| 377 | q_base = index_q_ptr + tok_idx * index_q_stride0 + head_idx * index_q_stride1 |
| 378 | out_base = ( |
| 379 | index_q_mxfp4_ptr |
| 380 | + tok_idx * index_q_mxfp4_stride0 |
| 381 | + head_idx * index_q_mxfp4_stride1 |
| 382 | ) |
| 383 | scale_base = ( |
| 384 | index_q_scale_ptr |
| 385 | + tok_idx * index_q_scale_stride0 |
| 386 | + head_idx * index_q_scale_stride1 |
| 387 | ) |
| 388 | |
| 389 | half_off = tl.arange(0, HALF_BLOCK) |
| 390 | |
| 391 | # ---- NoPE blocks: direct load, pair as (even-index, odd-index) values ---- |
| 392 | for b in tl.static_range(NUM_NOPE_BLOCKS): |
| 393 | base = b * MXFP4_BLOCK |
| 394 | x_lo = tl.load(q_base + base + half_off * 2).to(tl.float32) |
| 395 | x_hi = tl.load(q_base + base + half_off * 2 + 1).to(tl.float32) |
| 396 | packed, ue8m0 = _quantize_mxfp4_pair(x_lo, x_hi) |
| 397 | tl.store(out_base + base // 2 + half_off, packed) |
| 398 | tl.store(scale_base + b, ue8m0) |
| 399 | |
| 400 | # ---- RoPE blocks: apply GPT-J interleaved RoPE to the block's 16 pairs, |
| 401 | # then quantize. Each block covers HALF_BLOCK (=16) cos/sin pairs. ---- |
| 402 | rot_q_base = q_base + INDEX_Q_NOPE_DIM |
| 403 | for b in tl.static_range(NUM_ROPE_BLOCKS): |
| 404 | pair_off = b * HALF_BLOCK + half_off # indices in [0, HALF_ROT_DIM) |
| 405 | cos_b = tl.load( |
| 406 | index_q_cos_sin_ptr + pos * index_q_cos_sin_stride + pair_off |
| 407 | ).to(tl.float32) |
| 408 | sin_b = tl.load( |
| 409 | index_q_cos_sin_ptr |
| 410 | + pos * index_q_cos_sin_stride |
| 411 | + pair_off |
| 412 | + INDEX_Q_HALF_ROT_DIM |
| 413 | ).to(tl.float32) |
| 414 | x_even = tl.load(rot_q_base + pair_off * 2).to(tl.float32) |
| 415 | x_odd = tl.load(rot_q_base + pair_off * 2 + 1).to(tl.float32) |
| 416 | r_even = x_even * cos_b - x_odd * sin_b |
| 417 | r_odd = x_odd * cos_b + x_even * sin_b |
| 418 | # bf16 roundtrip for parity with the FP8 kernel / reference numerics. |
| 419 | r_even = r_even.to(tl.bfloat16).to(tl.float32) |
| 420 | r_odd = r_odd.to(tl.bfloat16).to(tl.float32) |
| 421 | packed, ue8m0 = _quantize_mxfp4_pair(r_even, r_odd) |
| 422 | rope_byte_off = (INDEX_Q_NOPE_DIM + b * MXFP4_BLOCK) // 2 |
| 423 | tl.store(out_base + rope_byte_off + half_off, packed) |
| 424 | tl.store(scale_base + NUM_NOPE_BLOCKS + b, ue8m0) |
| 425 | |
| 426 | # MXFP4 weight-fold contract: |
| 427 | # index_weights_out = index_weights * softmax_scale * head_scale |
| 428 | # NOTE: q_scale is NOT folded here (contrast with the FP8 kernel above). |
| 429 | # MXFP4 Q emits a separate ue8m0 scale tensor of shape |
| 430 | # (T, H, HEAD_DIM // MXFP4_BLOCK) alongside the packed values, so each |
| 431 | # per-block scale is applied by the downstream MXFP4 logits kernel when |
| 432 | # dequantizing Q — there is no per-token scalar to fold into `weights`. |
| 433 | index_weights = tl.load( |
| 434 | index_weights_ptr + tok_idx * index_weights_stride + head_idx |
| 435 | ).to(tl.float32) |
| 436 | index_weights *= index_weights_softmax_scale |
| 437 | index_weights *= index_weights_head_scale |
| 438 | tl.store( |
| 439 | index_weights_out_ptr + tok_idx * index_weights_out_stride + head_idx, |
| 440 | index_weights, |
| 441 | ) |
| 442 | |
| 443 | def dispatch( # type: ignore[override] |
| 444 | self, |
| 445 | *, |
| 446 | dtype: torch.dtype, |
| 447 | weights_out_dtype: torch.dtype, |
| 448 | num_heads: int, |
| 449 | head_dim: int, |
| 450 | rope_dim: int, |
| 451 | ) -> CompileKey: |
| 452 | return self.CompileKey( |
| 453 | dtype=dtype, |
| 454 | weights_out_dtype=weights_out_dtype, |
| 455 | num_heads=num_heads, |
| 456 | index_q_half_rot_dim=rope_dim // 2, |
| 457 | index_q_head_dim=head_dim, |
| 458 | mxfp4_block=MXFP4_BLOCK_SIZE, |
| 459 | ) |
| 460 | |
| 461 | def get_warmup_keys(self, vllm_config: Any) -> list[CompileKey]: |
| 462 | hf_config = vllm_config.model_config.hf_config |
| 463 | num_heads = int(getattr(hf_config, "index_n_heads", 0) or 0) |
| 464 | head_dim = int(getattr(hf_config, "index_head_dim", 0) or 0) |
| 465 | rope_dim = int(getattr(hf_config, "qk_rope_head_dim", 0) or 0) |
| 466 | if num_heads <= 0 or head_dim <= 0 or rope_dim <= 0: |
| 467 | return [] |
| 468 | |
| 469 | return self._trace_dispatch(self.dispatch)( |
| 470 | dtype=vllm_config.model_config.dtype, |
| 471 | weights_out_dtype=_indexer_weights_out_dtypes(vllm_config), |
| 472 | num_heads=num_heads, |
| 473 | head_dim=head_dim, |
| 474 | rope_dim=rope_dim, |
| 475 | ) |
| 476 | |
| 477 | def warmup_inputs(self, compile_key: CompileKey) -> dict[str, Any]: |
| 478 | q_stride0 = compile_key.num_heads * compile_key.index_q_head_dim |
| 479 | packed_head_dim = compile_key.index_q_head_dim // 2 |
| 480 | scale_head_dim = compile_key.index_q_head_dim // compile_key.mxfp4_block |
| 481 | return dict( |
| 482 | positions=TritonWarmupTensor(torch.int64), |
| 483 | index_q=TritonWarmupTensor( |
| 484 | compile_key.dtype, |
| 485 | shape=(1, compile_key.num_heads, compile_key.index_q_head_dim), |
| 486 | strides=(q_stride0, compile_key.index_q_head_dim, 1), |
| 487 | ), |
| 488 | index_q_cos_sin_cache=TritonWarmupTensor( |
| 489 | torch.float32, |
| 490 | shape=(1, compile_key.index_q_half_rot_dim * 2), |
| 491 | ), |
| 492 | index_weights=TritonWarmupTensor( |
| 493 | compile_key.dtype, |
| 494 | shape=(1, compile_key.num_heads), |
| 495 | ), |
| 496 | index_weights_softmax_scale=1.0, |
| 497 | index_weights_head_scale=1.0, |
| 498 | index_q_packed=TritonWarmupTensor( |
| 499 | torch.uint8, |
| 500 | shape=(1, compile_key.num_heads, packed_head_dim), |
| 501 | ), |
| 502 | index_q_scale=TritonWarmupTensor( |
| 503 | torch.uint8, |
| 504 | shape=(1, compile_key.num_heads, scale_head_dim), |
| 505 | ), |
| 506 | index_weights_out=TritonWarmupTensor( |
| 507 | compile_key.weights_out_dtype, |
| 508 | shape=(1, compile_key.num_heads), |
| 509 | ), |
| 510 | ) |
| 511 | |
| 512 | @kernel_launcher |
| 513 | def __call__( |
| 514 | self, |
| 515 | positions: torch.Tensor, |
| 516 | index_q: torch.Tensor, |
| 517 | index_q_cos_sin_cache: torch.Tensor, |
| 518 | index_weights: torch.Tensor, |
| 519 | index_weights_softmax_scale: float, |
| 520 | index_weights_head_scale: float, |
| 521 | index_q_packed: torch.Tensor, |
| 522 | index_q_scale: torch.Tensor, |
| 523 | index_weights_out: torch.Tensor, |
| 524 | ) -> LaunchSpec: |
| 525 | num_tokens = positions.shape[0] |
| 526 | num_index_q_heads = index_q.shape[1] |
| 527 | return (num_tokens, num_index_q_heads), dict( |
| 528 | pos_ptr=positions, |
| 529 | index_q_stride0=index_q.stride(0), |
| 530 | index_q_stride1=index_q.stride(1), |
| 531 | index_q_cos_sin_ptr=index_q_cos_sin_cache, |
| 532 | index_q_cos_sin_stride=index_q_cos_sin_cache.stride(0), |
| 533 | INDEX_Q_HALF_ROT_DIM=index_q_cos_sin_cache.shape[-1] // 2, |
| 534 | index_q_mxfp4_ptr=index_q_packed, |
| 535 | index_q_mxfp4_stride0=index_q_packed.stride(0), |
| 536 | index_q_mxfp4_stride1=index_q_packed.stride(1), |
| 537 | index_q_scale_stride0=index_q_scale.stride(0), |
| 538 | index_q_scale_stride1=index_q_scale.stride(1), |
| 539 | INDEX_Q_HEAD_DIM=index_q.shape[2], |
| 540 | MXFP4_BLOCK=MXFP4_BLOCK_SIZE, |
| 541 | index_weights_stride=index_weights.stride(0), |
| 542 | index_weights_out_stride=index_weights_out.stride(0), |
| 543 | num_warps=1, |
| 544 | ) |
| 545 | |
| 546 | |
| 547 | def fused_indexer_q_rope_quant( |
| 548 | positions: torch.Tensor, |
| 549 | index_q: torch.Tensor, |
| 550 | index_q_cos_sin_cache: torch.Tensor, |
| 551 | # Index weights |
| 552 | index_weights: torch.Tensor, |
| 553 | index_weights_softmax_scale: float, |
| 554 | index_weights_head_scale: float, |
| 555 | use_fp4: bool = False, |
| 556 | weights_out_dtype: torch.dtype = torch.float32, |
| 557 | ) -> tuple[ |
| 558 | torch.Tensor | tuple[torch.Tensor, torch.Tensor], |
| 559 | torch.Tensor, |
| 560 | ]: |
| 561 | """Fused RoPE + quantize Q for the sparse indexer. |
| 562 | |
| 563 | ``weights_out_dtype`` is the dtype the downstream scoring kernel takes: |
| 564 | fp32 for the dense MQA-logits kernels, bf16 for DeepGEMM's sparse |
| 565 | MQA-logits kernels (CUDA MXFP4 path only). |
| 566 | |
| 567 | Weight-fold semantics (important — the two paths differ): |
| 568 | |
| 569 | FP8 path (use_fp4=False, default): |
| 570 | q_fp8 : (T, H, HEAD_DIM) platform fp8 (e4m3fnuz on gfx942, |
| 571 | e4m3fn elsewhere); per-token-per-head scalar scale |
| 572 | (NOT stored — folded into weights below) |
| 573 | weights_out = weights * q_scale * softmax_scale * head_scale |
| 574 | Rationale: a single per-token q_scale is a scalar the downstream FP8 |
| 575 | logits kernel would otherwise multiply in. Folding it into `weights` |
| 576 | avoids emitting a separate tensor and is free for the logits kernel. |
| 577 | |
| 578 | MXFP4 path (use_fp4=True): |
| 579 | q_packed : (T, H, HEAD_DIM // 2) uint8 (2 E2M1 nibbles per byte) |
| 580 | q_scale : (T, H, HEAD_DIM // MXFP4_BLOCK_SIZE) uint8 ue8m0 bytes |
| 581 | weights_out = weights * softmax_scale * head_scale |
| 582 | Rationale: MXFP4 has PER-BLOCK (32-element) scales that live with |
| 583 | the Q values — they cannot be folded into a per-token weight |
| 584 | scalar, so `weights` carries only the softmax and head scales. |
| 585 | |
| 586 | Returns (q_quant, weights_out) where q_quant is either a Tensor (FP8) or |
| 587 | a (values, scales) tuple (MXFP4). This matches the union type accepted |
| 588 | by `SparseAttnIndexer.forward_*`. |
| 589 | """ |
| 590 | assert positions.ndim == 1 |
| 591 | assert index_q.ndim == 3 |
| 592 | assert index_q_cos_sin_cache.ndim == 2 |
| 593 | |
| 594 | num_tokens = positions.shape[0] |
| 595 | num_index_q_heads = index_q.shape[1] |
| 596 | index_q_head_dim = index_q.shape[2] |
| 597 | |
| 598 | index_weights_out = torch.empty_like(index_weights, dtype=weights_out_dtype) |
| 599 | |
| 600 | if use_fp4: |
| 601 | assert index_q_head_dim % MXFP4_BLOCK_SIZE == 0, ( |
| 602 | f"head_dim={index_q_head_dim} must be a multiple of MXFP4 block " |
| 603 | f"size {MXFP4_BLOCK_SIZE}" |
| 604 | ) |
| 605 | num_scale_blocks = index_q_head_dim // MXFP4_BLOCK_SIZE |
| 606 | index_q_packed = torch.empty( |
| 607 | (num_tokens, num_index_q_heads, index_q_head_dim // 2), |
| 608 | dtype=torch.uint8, |
| 609 | device=index_q.device, |
| 610 | ) |
| 611 | index_q_scale = torch.empty( |
| 612 | (num_tokens, num_index_q_heads, num_scale_blocks), |
| 613 | dtype=torch.uint8, |
| 614 | device=index_q.device, |
| 615 | ) |
| 616 | if has_cutedsl(): |
| 617 | # lazily import, otherwise some tests fail due to CUDA driver init failure. |
| 618 | from vllm.models.deepseek_v4.nvidia.ops.fused_indexer_q_cutedsl import ( |
| 619 | _INDEXER_Q_MXFP4_KERNEL, |
| 620 | ) |
| 621 | |
| 622 | _INDEXER_Q_MXFP4_KERNEL( |
| 623 | positions=positions, |
| 624 | q=index_q, |
| 625 | cos_sin_cache=index_q_cos_sin_cache, |
| 626 | weights=index_weights, |
| 627 | weights_softmax_scale=index_weights_softmax_scale, |
| 628 | weights_head_scale=index_weights_head_scale, |
| 629 | q_packed=index_q_packed, |
| 630 | q_scale=index_q_scale, |
| 631 | weights_out=index_weights_out, |
| 632 | ) |
| 633 | elif current_platform.is_xpu(): |
| 634 | assert weights_out_dtype == torch.float32, weights_out_dtype |
| 635 | torch.ops.vllm.xpu_deepseek_fused_indexer_q_rope_mxfp4( |
| 636 | index_q, |
| 637 | positions, |
| 638 | index_q_cos_sin_cache, |
| 639 | index_weights, |
| 640 | index_weights_softmax_scale, |
| 641 | index_weights_head_scale, |
| 642 | index_q_packed, |
| 643 | index_q_scale, |
| 644 | index_weights_out, |
| 645 | ) |
| 646 | else: |
| 647 | _FUSED_INDEXER_Q_ROPE_MXFP4_TRITON_KERNEL( |
| 648 | positions, |
| 649 | index_q, |
| 650 | index_q_cos_sin_cache, |
| 651 | index_weights, |
| 652 | index_weights_softmax_scale, |
| 653 | index_weights_head_scale, |
| 654 | index_q_packed, |
| 655 | index_q_scale, |
| 656 | index_weights_out, |
| 657 | ) |
| 658 | |
| 659 | # Values stay uint8 (2 E2M1 nibbles per byte). Scales are 4 ue8m0 |
| 660 | # bytes per (token, head) reinterpreted as one int32, then squeezed |
| 661 | # from (T, H, 1) to (T, H) to match DeepGEMM's expected q_sf rank |
| 662 | # (prefill wants 2-D (seq_len, num_heads); decode reshapes this to |
| 663 | # 3-D (batch, next_n, num_heads)). |
| 664 | return ( |
| 665 | index_q_packed, |
| 666 | index_q_scale.view(torch.int32).squeeze(-1), |
| 667 | ), index_weights_out |
| 668 | |
| 669 | assert weights_out_dtype == torch.float32, weights_out_dtype |
| 670 | fp8_dtype = current_platform.fp8_dtype() |
| 671 | use_fnuz = fp8_dtype == torch.float8_e4m3fnuz |
| 672 | fp8_max = 224.0 if use_fnuz else 448.0 |
| 673 | index_q_fp8 = torch.empty_like(index_q, dtype=fp8_dtype) |
| 674 | if has_cutedsl(): |
| 675 | # lazily import, otherwise some tests fail due to CUDA driver init failure. |
| 676 | from vllm.models.deepseek_v4.nvidia.ops.fused_indexer_q_cutedsl import ( |
| 677 | _INDEXER_Q_FP8_KERNEL, |
| 678 | ) |
| 679 | |
| 680 | _INDEXER_Q_FP8_KERNEL( |
| 681 | positions=positions, |
| 682 | q=index_q, |
| 683 | cos_sin_cache=index_q_cos_sin_cache, |
| 684 | weights=index_weights, |
| 685 | weights_softmax_scale=index_weights_softmax_scale, |
| 686 | weights_head_scale=index_weights_head_scale, |
| 687 | q_fp8=index_q_fp8.view(torch.uint8), |
| 688 | weights_out=index_weights_out, |
| 689 | ) |
| 690 | elif current_platform.is_xpu(): |
| 691 | torch.ops.vllm.xpu_deepseek_fused_indexer_q_rope_fp8( |
| 692 | index_q, |
| 693 | positions, |
| 694 | index_q_cos_sin_cache, |
| 695 | index_weights, |
| 696 | index_weights_softmax_scale, |
| 697 | index_weights_head_scale, |
| 698 | index_q_fp8, |
| 699 | index_weights_out, |
| 700 | ) |
| 701 | elif current_platform.is_cpu(): |
| 702 | from vllm._custom_ops import fused_indexer_q_rope_quant_cpu |
| 703 | |
| 704 | fused_indexer_q_rope_quant_cpu( |
| 705 | positions.contiguous(), |
| 706 | index_q.to(torch.float32), |
| 707 | index_q_cos_sin_cache.to(torch.float32).contiguous(), |
| 708 | index_q_fp8, |
| 709 | index_weights.to(torch.float32), |
| 710 | index_weights_softmax_scale, |
| 711 | index_weights_head_scale, |
| 712 | index_weights_out, |
| 713 | ) |
| 714 | else: |
| 715 | _FUSED_INDEXER_Q_ROPE_QUANT_TRITON_KERNEL( |
| 716 | positions, |
| 717 | index_q, |
| 718 | index_q_cos_sin_cache, |
| 719 | index_weights, |
| 720 | index_weights_softmax_scale, |
| 721 | index_weights_head_scale, |
| 722 | index_q_fp8, |
| 723 | index_weights_out, |
| 724 | fp8_max=fp8_max, |
| 725 | use_fnuz=use_fnuz, |
| 726 | ) |
| 727 | return index_q_fp8, index_weights_out |
| 728 | |
| 729 | |
| 730 | _FUSED_INDEXER_Q_ROPE_QUANT_TRITON_KERNEL = FusedIndexerQRopeQuantTritonKernel() |
| 731 | _FUSED_INDEXER_Q_ROPE_MXFP4_TRITON_KERNEL = FusedIndexerQRopeMxFp4TritonKernel() |