| 1 | # SPDX-License-Identifier: Apache-2.0 |
| 2 | # SPDX-FileCopyrightText: Copyright contributors to the vLLM project |
| 3 | """Sub-byte packed (INT4) per-token-head KV cache mode. |
| 4 | |
| 5 | INT4 packs two 4-bit values per cache byte, pre-rotates with a single RHT, |
| 6 | and hides a 4-bit zero-point in the scale's low mantissa bits — too |
| 7 | different from the core kernel to share it. Owns the whole mode: nibble |
| 8 | pack/unpack, the reshape (write) kernel, the split-dot attention (read) |
| 9 | kernel, the RHT transform, and the public ``reshape_and_cache_int4`` / |
| 10 | ``unified_attention_int4`` entry points. |
| 11 | """ |
| 12 | |
| 13 | from __future__ import annotations |
| 14 | |
| 15 | from typing import Any |
| 16 | |
| 17 | import torch |
| 18 | |
| 19 | from vllm.platforms import current_platform |
| 20 | from vllm.triton_utils import tl, triton |
| 21 | from vllm.v1.attention.ops.triton_attention_helpers import ( |
| 22 | apply_alibi_to_score, |
| 23 | apply_softcap, |
| 24 | cdiv_fn, |
| 25 | compute_kv_seq_mask, |
| 26 | compute_tile_loop_bounds, |
| 27 | init_softmax_M, |
| 28 | load_qq_bias_tile, |
| 29 | resolve_seq_and_query_len, |
| 30 | softmax_step, |
| 31 | store_segm_reduce_scalars, |
| 32 | ) |
| 33 | from vllm.v1.attention.ops.triton_unified_attention import reduce_segments |
| 34 | |
| 35 | float8_info = torch.finfo(current_platform.fp8_dtype()) |
| 36 | |
| 37 | # 2 x int4 packed per storage byte. |
| 38 | _INT4_PACKING_FACTOR = 2 |
| 39 | |
| 40 | |
| 41 | # ---------------------------------------------------------------------- |
| 42 | # Nibble pack / unpack (shared write+read format) |
| 43 | # ---------------------------------------------------------------------- |
| 44 | |
| 45 | |
| 46 | @triton.jit |
| 47 | def pack_int4_nibbles(lo, hi): |
| 48 | """Pack two uint8 values (each in [0, 15]) into one byte.""" |
| 49 | return (lo & 0xF) | ((hi & 0xF) << 4) |
| 50 | |
| 51 | |
| 52 | @triton.jit |
| 53 | def unpack_int4_nibbles(packed): |
| 54 | """Split one packed byte into the (low, high) nibble pair as uint8.""" |
| 55 | return packed & 0xF, (packed >> 4) & 0xF |
| 56 | |
| 57 | |
| 58 | # ---------------------------------------------------------------------- |
| 59 | # Write path: RHT + pack + per-(token, head) scale |
| 60 | # ---------------------------------------------------------------------- |
| 61 | |
| 62 | |
| 63 | @triton.jit |
| 64 | def _reshape_cache_int4_kernel( |
| 65 | key_ptr, |
| 66 | value_ptr, |
| 67 | key_cache_ptr, |
| 68 | value_cache_ptr, |
| 69 | k_scale_cache_ptr, |
| 70 | v_scale_cache_ptr, |
| 71 | slot_mapping_ptr, |
| 72 | stride_key_tok: tl.int64, |
| 73 | stride_key_head: tl.int64, |
| 74 | stride_val_tok: tl.int64, |
| 75 | stride_val_head: tl.int64, |
| 76 | stride_kc_blk: tl.int64, |
| 77 | stride_kc_slot: tl.int64, |
| 78 | stride_kc_head: tl.int64, |
| 79 | stride_vc_blk: tl.int64, |
| 80 | stride_vc_slot: tl.int64, |
| 81 | stride_vc_head: tl.int64, |
| 82 | stride_ks_blk: tl.int64, |
| 83 | stride_ks_slot: tl.int64, |
| 84 | stride_ks_head: tl.int64, |
| 85 | stride_vs_blk: tl.int64, |
| 86 | stride_vs_slot: tl.int64, |
| 87 | stride_vs_head: tl.int64, |
| 88 | block_size: tl.constexpr, |
| 89 | head_size: tl.constexpr, |
| 90 | head_size_v: tl.constexpr, |
| 91 | PACKED_HEAD_PADDED: tl.constexpr, |
| 92 | ): |
| 93 | """INT4 asymmetric quantization with zero-point steganography.""" |
| 94 | tok = tl.program_id(0) |
| 95 | head = tl.program_id(1) |
| 96 | |
| 97 | slot = tl.load(slot_mapping_ptr + tok).to(tl.int64) |
| 98 | if slot < 0: |
| 99 | return |
| 100 | |
| 101 | blk = slot // block_size |
| 102 | slot_in_blk = slot % block_size |
| 103 | |
| 104 | half_offs = tl.arange(0, PACKED_HEAD_PADDED) |
| 105 | even_offs = half_offs * 2 |
| 106 | odd_offs = half_offs * 2 + 1 |
| 107 | |
| 108 | half_k = head_size // 2 |
| 109 | even_k_mask = even_offs < head_size |
| 110 | odd_k_mask = odd_offs < head_size |
| 111 | key_base = key_ptr + tok * stride_key_tok + head * stride_key_head |
| 112 | |
| 113 | k_even = tl.load(key_base + even_offs, mask=even_k_mask, other=0.0).to(tl.float32) |
| 114 | k_odd = tl.load(key_base + odd_offs, mask=odd_k_mask, other=0.0).to(tl.float32) |
| 115 | |
| 116 | k_min = tl.minimum( |
| 117 | tl.min(tl.where(even_k_mask, k_even, float("inf"))), |
| 118 | tl.min(tl.where(odd_k_mask, k_odd, float("inf"))), |
| 119 | ) |
| 120 | k_max = tl.maximum( |
| 121 | tl.max(tl.where(even_k_mask, k_even, float("-inf"))), |
| 122 | tl.max(tl.where(odd_k_mask, k_odd, float("-inf"))), |
| 123 | ) |
| 124 | k_scale = tl.maximum((k_max - k_min) / 15.0, 1e-6) |
| 125 | k_zp_f = tl.clamp( |
| 126 | tl.where( |
| 127 | -k_min / k_scale >= 0, |
| 128 | (-k_min / k_scale + 0.5).to(tl.int32), |
| 129 | (-k_min / k_scale - 0.5).to(tl.int32), |
| 130 | ).to(tl.float32), |
| 131 | 0.0, |
| 132 | 15.0, |
| 133 | ) |
| 134 | |
| 135 | inv_k = 1.0 / k_scale |
| 136 | k_even_s = k_even * inv_k + k_zp_f |
| 137 | k_odd_s = k_odd * inv_k + k_zp_f |
| 138 | k_even_q = tl.clamp( |
| 139 | tl.where( |
| 140 | k_even_s >= 0, |
| 141 | (k_even_s + 0.5).to(tl.int32), |
| 142 | (k_even_s - 0.5).to(tl.int32), |
| 143 | ).to(tl.float32), |
| 144 | 0.0, |
| 145 | 15.0, |
| 146 | ) |
| 147 | k_odd_q = tl.clamp( |
| 148 | tl.where( |
| 149 | k_odd_s >= 0, |
| 150 | (k_odd_s + 0.5).to(tl.int32), |
| 151 | (k_odd_s - 0.5).to(tl.int32), |
| 152 | ).to(tl.float32), |
| 153 | 0.0, |
| 154 | 15.0, |
| 155 | ) |
| 156 | |
| 157 | k_zp_int = k_zp_f.to(tl.int32) |
| 158 | k_scale_bits = k_scale.to(tl.int32, bitcast=True) |
| 159 | k_scale_packed = ((k_scale_bits & -16) | (k_zp_int & 0xF)).to( |
| 160 | tl.float32, bitcast=True |
| 161 | ) |
| 162 | |
| 163 | tl.store( |
| 164 | k_scale_cache_ptr |
| 165 | + blk * stride_ks_blk |
| 166 | + slot_in_blk * stride_ks_slot |
| 167 | + head * stride_ks_head, |
| 168 | k_scale_packed, |
| 169 | ) |
| 170 | |
| 171 | k_packed = pack_int4_nibbles(k_even_q.to(tl.uint8), k_odd_q.to(tl.uint8)) |
| 172 | tl.store( |
| 173 | key_cache_ptr |
| 174 | + blk * stride_kc_blk |
| 175 | + slot_in_blk * stride_kc_slot |
| 176 | + head * stride_kc_head |
| 177 | + half_offs, |
| 178 | k_packed, |
| 179 | mask=half_offs < half_k, |
| 180 | ) |
| 181 | |
| 182 | half_v = head_size_v // 2 |
| 183 | even_v_mask = even_offs < head_size_v |
| 184 | odd_v_mask = odd_offs < head_size_v |
| 185 | val_base = value_ptr + tok * stride_val_tok + head * stride_val_head |
| 186 | |
| 187 | v_even = tl.load(val_base + even_offs, mask=even_v_mask, other=0.0).to(tl.float32) |
| 188 | v_odd = tl.load(val_base + odd_offs, mask=odd_v_mask, other=0.0).to(tl.float32) |
| 189 | |
| 190 | v_min = tl.minimum( |
| 191 | tl.min(tl.where(even_v_mask, v_even, float("inf"))), |
| 192 | tl.min(tl.where(odd_v_mask, v_odd, float("inf"))), |
| 193 | ) |
| 194 | v_max = tl.maximum( |
| 195 | tl.max(tl.where(even_v_mask, v_even, float("-inf"))), |
| 196 | tl.max(tl.where(odd_v_mask, v_odd, float("-inf"))), |
| 197 | ) |
| 198 | v_scale = tl.maximum((v_max - v_min) / 15.0, 1e-6) |
| 199 | v_zp_f = tl.clamp( |
| 200 | tl.where( |
| 201 | -v_min / v_scale >= 0, |
| 202 | (-v_min / v_scale + 0.5).to(tl.int32), |
| 203 | (-v_min / v_scale - 0.5).to(tl.int32), |
| 204 | ).to(tl.float32), |
| 205 | 0.0, |
| 206 | 15.0, |
| 207 | ) |
| 208 | |
| 209 | inv_v = 1.0 / v_scale |
| 210 | v_even_s = v_even * inv_v + v_zp_f |
| 211 | v_odd_s = v_odd * inv_v + v_zp_f |
| 212 | v_even_q = tl.clamp( |
| 213 | tl.where( |
| 214 | v_even_s >= 0, |
| 215 | (v_even_s + 0.5).to(tl.int32), |
| 216 | (v_even_s - 0.5).to(tl.int32), |
| 217 | ).to(tl.float32), |
| 218 | 0.0, |
| 219 | 15.0, |
| 220 | ) |
| 221 | v_odd_q = tl.clamp( |
| 222 | tl.where( |
| 223 | v_odd_s >= 0, |
| 224 | (v_odd_s + 0.5).to(tl.int32), |
| 225 | (v_odd_s - 0.5).to(tl.int32), |
| 226 | ).to(tl.float32), |
| 227 | 0.0, |
| 228 | 15.0, |
| 229 | ) |
| 230 | |
| 231 | v_zp_int = v_zp_f.to(tl.int32) |
| 232 | v_scale_bits = v_scale.to(tl.int32, bitcast=True) |
| 233 | v_scale_packed = ((v_scale_bits & -16) | (v_zp_int & 0xF)).to( |
| 234 | tl.float32, bitcast=True |
| 235 | ) |
| 236 | |
| 237 | tl.store( |
| 238 | v_scale_cache_ptr |
| 239 | + blk * stride_vs_blk |
| 240 | + slot_in_blk * stride_vs_slot |
| 241 | + head * stride_vs_head, |
| 242 | v_scale_packed, |
| 243 | ) |
| 244 | |
| 245 | v_packed = pack_int4_nibbles(v_even_q.to(tl.uint8), v_odd_q.to(tl.uint8)) |
| 246 | tl.store( |
| 247 | value_cache_ptr |
| 248 | + blk * stride_vc_blk |
| 249 | + slot_in_blk * stride_vc_slot |
| 250 | + head * stride_vc_head |
| 251 | + half_offs, |
| 252 | v_packed, |
| 253 | mask=half_offs < half_v, |
| 254 | ) |
| 255 | |
| 256 | |
| 257 | def _run_reshape_kernel( |
| 258 | kernel, |
| 259 | *, |
| 260 | key: torch.Tensor, |
| 261 | value: torch.Tensor, |
| 262 | key_cache: torch.Tensor, |
| 263 | value_cache: torch.Tensor, |
| 264 | k_scale_cache: torch.Tensor, |
| 265 | v_scale_cache: torch.Tensor, |
| 266 | slot_mapping: torch.Tensor, |
| 267 | packing_factor: int, |
| 268 | ) -> None: |
| 269 | """Launch the packed INT4 reshape kernel.""" |
| 270 | num_tokens, num_kv_heads, head_size = key.shape |
| 271 | head_size_v = value.shape[2] |
| 272 | assert head_size % packing_factor == 0 and head_size_v % packing_factor == 0 |
| 273 | packed_padded = triton.next_power_of_2( |
| 274 | max(head_size, head_size_v) // packing_factor |
| 275 | ) |
| 276 | if current_platform.is_rocm() or current_platform.is_xpu(): |
| 277 | num_warps = 4 |
| 278 | else: |
| 279 | num_warps = min(16, max(1, packed_padded // 32)) |
| 280 | |
| 281 | kernel[(num_tokens, num_kv_heads)]( |
| 282 | key_ptr=key, |
| 283 | value_ptr=value, |
| 284 | key_cache_ptr=key_cache, |
| 285 | value_cache_ptr=value_cache, |
| 286 | k_scale_cache_ptr=k_scale_cache, |
| 287 | v_scale_cache_ptr=v_scale_cache, |
| 288 | slot_mapping_ptr=slot_mapping, |
| 289 | stride_key_tok=key.stride(0), |
| 290 | stride_key_head=key.stride(1), |
| 291 | stride_val_tok=value.stride(0), |
| 292 | stride_val_head=value.stride(1), |
| 293 | stride_kc_blk=key_cache.stride(0), |
| 294 | stride_kc_slot=key_cache.stride(1), |
| 295 | stride_kc_head=key_cache.stride(2), |
| 296 | stride_vc_blk=value_cache.stride(0), |
| 297 | stride_vc_slot=value_cache.stride(1), |
| 298 | stride_vc_head=value_cache.stride(2), |
| 299 | stride_ks_blk=k_scale_cache.stride(0), |
| 300 | stride_ks_slot=k_scale_cache.stride(1), |
| 301 | stride_ks_head=k_scale_cache.stride(2), |
| 302 | stride_vs_blk=v_scale_cache.stride(0), |
| 303 | stride_vs_slot=v_scale_cache.stride(1), |
| 304 | stride_vs_head=v_scale_cache.stride(2), |
| 305 | block_size=key_cache.shape[1], |
| 306 | head_size=head_size, |
| 307 | head_size_v=head_size_v, |
| 308 | PACKED_HEAD_PADDED=packed_padded, |
| 309 | num_warps=num_warps, |
| 310 | ) |
| 311 | |
| 312 | |
| 313 | # ---------------------------------------------------------------------- |
| 314 | # Read path: split-dot attention over the packed cache |
| 315 | # ---------------------------------------------------------------------- |
| 316 | |
| 317 | |
| 318 | @triton.jit |
| 319 | def _attn_packed( |
| 320 | # Output destinations. In 2D mode the final result is written into |
| 321 | # ``output_ptr``; in 3D mode per-segment partials go into the three |
| 322 | # ``segm_*`` tensors and ``output_ptr`` is unused. |
| 323 | output_ptr, |
| 324 | segm_output_ptr, |
| 325 | segm_max_ptr, |
| 326 | segm_expsum_ptr, |
| 327 | query_ptr, |
| 328 | key_cache_ptr, |
| 329 | value_cache_ptr, |
| 330 | sink_ptr, |
| 331 | block_tables_ptr, |
| 332 | seq_lens_ptr, |
| 333 | alibi_slopes_ptr, |
| 334 | qq_bias_ptr, |
| 335 | scale, |
| 336 | out_scale, |
| 337 | softcap, |
| 338 | k_scale_cache_ptr, |
| 339 | v_scale_cache_ptr, |
| 340 | num_query_heads: tl.constexpr, |
| 341 | num_queries_per_kv: tl.constexpr, |
| 342 | block_table_stride: tl.int64, |
| 343 | query_stride_0: tl.int64, |
| 344 | query_stride_1: tl.int64, |
| 345 | output_stride_0: tl.int64, |
| 346 | output_stride_1: tl.int64, |
| 347 | qq_bias_stride_0: tl.int64, |
| 348 | BLOCK_SIZE: tl.constexpr, |
| 349 | TILE_SIZE: tl.constexpr, |
| 350 | HEAD_SIZE: tl.constexpr, |
| 351 | HEAD_SIZE_PADDED: tl.constexpr, |
| 352 | PACKED_HEAD_PADDED: tl.constexpr, # HEAD_SIZE / PACKING_FACTOR, rounded up |
| 353 | USE_ALIBI_SLOPES: tl.constexpr, |
| 354 | USE_ALIBI_SQRT: tl.constexpr, |
| 355 | USE_QQ_BIAS: tl.constexpr, |
| 356 | USE_SOFTCAP: tl.constexpr, |
| 357 | USE_SINKS: tl.constexpr, |
| 358 | SLIDING_WINDOW: tl.constexpr, |
| 359 | USE_MM_PREFIX: tl.constexpr, |
| 360 | MAX_MM_RANGES: tl.constexpr, |
| 361 | mm_prefix_range_ptr, |
| 362 | stride_k_cache_0: tl.int64, |
| 363 | stride_k_cache_1: tl.int64, |
| 364 | stride_k_cache_2: tl.int64, |
| 365 | stride_k_cache_3: tl.constexpr, |
| 366 | stride_v_cache_0: tl.int64, |
| 367 | stride_v_cache_1: tl.int64, |
| 368 | stride_v_cache_2: tl.int64, |
| 369 | stride_v_cache_3: tl.constexpr, |
| 370 | stride_ks_blk: tl.int64, |
| 371 | stride_ks_slot: tl.int64, |
| 372 | stride_ks_head: tl.int64, |
| 373 | stride_vs_blk: tl.int64, |
| 374 | stride_vs_slot: tl.int64, |
| 375 | stride_vs_head: tl.int64, |
| 376 | query_start_len_ptr, |
| 377 | BLOCK_Q: tl.constexpr, |
| 378 | num_seqs: tl.int32, |
| 379 | BLOCK_M: tl.constexpr, |
| 380 | NUM_SEGMENTS_PER_SEQ: tl.constexpr, |
| 381 | USE_FP8: tl.constexpr, |
| 382 | IS_3D: tl.constexpr, |
| 383 | # 2 → INT4 nibble pair (asymmetric + zp). The packed KV cache stores |
| 384 | # one byte per ``packed_offs``, holding PACKING_FACTOR values. |
| 385 | PACKING_FACTOR: tl.constexpr, |
| 386 | FP8_MIN: tl.constexpr = float8_info.min, |
| 387 | FP8_MAX: tl.constexpr = float8_info.max, |
| 388 | ): |
| 389 | # Shared prologue: sequence lookup, q-block bounds, early returns. |
| 390 | q_block_global_idx = tl.program_id(0) |
| 391 | kv_head_idx = tl.program_id(1) |
| 392 | segm_idx = tl.program_id(2) if IS_3D else 0 |
| 393 | |
| 394 | ( |
| 395 | seq_idx, |
| 396 | q_block_local_idx, |
| 397 | cur_batch_in_all_start_index, |
| 398 | cur_batch_query_len, |
| 399 | seq_len, |
| 400 | ) = resolve_seq_and_query_len( |
| 401 | query_start_len_ptr, seq_lens_ptr, q_block_global_idx, num_seqs, BLOCK_Q |
| 402 | ) |
| 403 | |
| 404 | if q_block_local_idx * BLOCK_Q >= cur_batch_query_len: |
| 405 | return |
| 406 | |
| 407 | if IS_3D: |
| 408 | tiles_per_segment = cdiv_fn(seq_len, NUM_SEGMENTS_PER_SEQ * TILE_SIZE) |
| 409 | if segm_idx * tiles_per_segment * TILE_SIZE >= seq_len: |
| 410 | return |
| 411 | else: |
| 412 | tiles_per_segment = 0 |
| 413 | |
| 414 | offs_m = tl.arange(0, BLOCK_M) |
| 415 | offs_t = tl.arange(0, TILE_SIZE) |
| 416 | query_pos = q_block_local_idx * BLOCK_Q + offs_m // num_queries_per_kv |
| 417 | |
| 418 | query_offset_0 = cur_batch_in_all_start_index + query_pos |
| 419 | query_offset_1 = kv_head_idx * num_queries_per_kv + offs_m % num_queries_per_kv |
| 420 | |
| 421 | query_mask_0 = tl.where(query_pos < cur_batch_query_len, 1, 0).to(tl.int1) |
| 422 | query_mask_1 = tl.where(query_offset_1 < num_query_heads, 1, 0).to(tl.int1) |
| 423 | |
| 424 | # Split-Q prologue: PACKING_FACTOR interleaved streams of Q. |
| 425 | # INT4 uses 2 streams (even / odd). The packed KV cache stores one |
| 426 | # byte per ``packed_offs``, which holds PACKING_FACTOR values. |
| 427 | packed_offs = tl.arange(0, PACKED_HEAD_PADDED) |
| 428 | offs_s0 = packed_offs * PACKING_FACTOR |
| 429 | offs_s1 = packed_offs * PACKING_FACTOR + 1 |
| 430 | mask_s0 = tl.where(offs_s0 < HEAD_SIZE, 1, 0).to(tl.int1) |
| 431 | mask_s1 = tl.where(offs_s1 < HEAD_SIZE, 1, 0).to(tl.int1) |
| 432 | packed_dim_mask = tl.where(packed_offs < HEAD_SIZE // PACKING_FACTOR, 1, 0).to( |
| 433 | tl.int1 |
| 434 | ) |
| 435 | q_base = ( |
| 436 | query_offset_0[:, None] * query_stride_0 |
| 437 | + query_offset_1[:, None] * query_stride_1 |
| 438 | ) |
| 439 | q_mask = query_mask_0[:, None] & query_mask_1[:, None] |
| 440 | Q_s0 = tl.load( |
| 441 | query_ptr + q_base + offs_s0[None, :], |
| 442 | mask=mask_s0[None, :] & q_mask, |
| 443 | other=0.0, |
| 444 | ).to(tl.float32) |
| 445 | Q_s1 = tl.load( |
| 446 | query_ptr + q_base + offs_s1[None, :], |
| 447 | mask=mask_s1[None, :] & q_mask, |
| 448 | other=0.0, |
| 449 | ).to(tl.float32) |
| 450 | |
| 451 | # INT4 asymmetric correction needs sum(Q) per row. |
| 452 | Q_sum = tl.sum(Q_s0, axis=1) + tl.sum(Q_s1, axis=1) |
| 453 | |
| 454 | block_table_offset = seq_idx * block_table_stride |
| 455 | |
| 456 | # Online-softmax state + optional feature loads. |
| 457 | M = init_softmax_M( |
| 458 | sink_ptr, query_offset_1, query_mask_1, segm_idx, BLOCK_M, USE_SINKS, IS_3D |
| 459 | ) |
| 460 | L = tl.full([BLOCK_M], 1.0, dtype=tl.float32) |
| 461 | acc_s0 = tl.zeros([BLOCK_M, PACKED_HEAD_PADDED], dtype=tl.float32) |
| 462 | acc_s1 = tl.zeros([BLOCK_M, PACKED_HEAD_PADDED], dtype=tl.float32) |
| 463 | |
| 464 | context_len = seq_len - cur_batch_query_len |
| 465 | |
| 466 | if USE_ALIBI_SLOPES: |
| 467 | alibi_slope = tl.load( |
| 468 | alibi_slopes_ptr + query_offset_1, mask=query_mask_1, other=0.0 |
| 469 | ) |
| 470 | |
| 471 | if USE_QQ_BIAS: |
| 472 | qq_bias_row_ptrs = qq_bias_ptr + query_pos[:, None] * qq_bias_stride_0 |
| 473 | |
| 474 | loop_lo, loop_hi, max_seq_prefix_len = compute_tile_loop_bounds( |
| 475 | context_len, |
| 476 | seq_len, |
| 477 | cur_batch_query_len, |
| 478 | q_block_local_idx, |
| 479 | segm_idx, |
| 480 | tiles_per_segment, |
| 481 | TILE_SIZE, |
| 482 | BLOCK_M, |
| 483 | BLOCK_Q, |
| 484 | num_queries_per_kv, |
| 485 | SLIDING_WINDOW, |
| 486 | USE_MM_PREFIX, |
| 487 | IS_3D, |
| 488 | ) |
| 489 | |
| 490 | # Tile loop. Per-tile: load packed KV + scales, dequantize into |
| 491 | # PACKING_FACTOR streams, compute the split dot, run the shared |
| 492 | # softmax step, accumulate per stream. |
| 493 | for j in range(loop_lo, loop_hi): |
| 494 | seq_offset = j * TILE_SIZE + offs_t |
| 495 | tile_mask = seq_offset < max_seq_prefix_len |
| 496 | |
| 497 | physical_block_idx = tl.load( |
| 498 | block_tables_ptr + block_table_offset + seq_offset // BLOCK_SIZE |
| 499 | ).to(tl.int64) |
| 500 | |
| 501 | slot_in_blk = seq_offset % BLOCK_SIZE |
| 502 | k_off = ( |
| 503 | physical_block_idx[None, :] * stride_k_cache_0 |
| 504 | + kv_head_idx * stride_k_cache_2 |
| 505 | + packed_offs[:, None] * stride_k_cache_3 |
| 506 | + slot_in_blk[None, :] * stride_k_cache_1 |
| 507 | ) |
| 508 | K_packed = tl.load( |
| 509 | key_cache_ptr + k_off, |
| 510 | mask=packed_dim_mask[:, None] & tile_mask[None, :], |
| 511 | other=0, |
| 512 | ) |
| 513 | v_off = ( |
| 514 | physical_block_idx[:, None] * stride_v_cache_0 |
| 515 | + kv_head_idx * stride_v_cache_2 |
| 516 | + packed_offs[None, :] * stride_v_cache_3 |
| 517 | + slot_in_blk[:, None] * stride_v_cache_1 |
| 518 | ) |
| 519 | V_packed = tl.load( |
| 520 | value_cache_ptr + v_off, |
| 521 | mask=packed_dim_mask[None, :] & tile_mask[:, None], |
| 522 | other=0, |
| 523 | ) |
| 524 | # Dequantize KV. INT4 unpacks nibbles as plain uint [0..15]; |
| 525 | # the zero-point is applied on the score side. |
| 526 | K_s0_u, K_s1_u = unpack_int4_nibbles(K_packed) |
| 527 | K_s0 = K_s0_u.to(tl.float32) |
| 528 | K_s1 = K_s1_u.to(tl.float32) |
| 529 | V_s0_u, V_s1_u = unpack_int4_nibbles(V_packed) |
| 530 | V_s0 = V_s0_u.to(tl.float32) |
| 531 | V_s1 = V_s1_u.to(tl.float32) |
| 532 | |
| 533 | ks_idx = ( |
| 534 | physical_block_idx * stride_ks_blk |
| 535 | + slot_in_blk * stride_ks_slot |
| 536 | + kv_head_idx * stride_ks_head |
| 537 | ) |
| 538 | ks_raw = tl.load(k_scale_cache_ptr + ks_idx, mask=tile_mask, other=0) |
| 539 | vs_idx = ( |
| 540 | physical_block_idx * stride_vs_blk |
| 541 | + slot_in_blk * stride_vs_slot |
| 542 | + kv_head_idx * stride_vs_head |
| 543 | ) |
| 544 | vs_raw = tl.load(v_scale_cache_ptr + vs_idx, mask=tile_mask, other=0) |
| 545 | |
| 546 | # INT4 steganographs the 4-bit zero-point in the low 4 bits of |
| 547 | # the float32 scale's mantissa. |
| 548 | ks_bits = ks_raw.to(tl.int32, bitcast=True) |
| 549 | k_zp = (ks_bits & 0xF).to(tl.float32) |
| 550 | k_token_head_scales = (ks_bits & -16).to(tl.float32, bitcast=True) |
| 551 | vs_bits = vs_raw.to(tl.int32, bitcast=True) |
| 552 | v_zp = (vs_bits & 0xF).to(tl.float32) |
| 553 | v_token_head_scales = (vs_bits & -16).to(tl.float32, bitcast=True) |
| 554 | |
| 555 | query_abs_pos = context_len + query_pos[:, None] |
| 556 | seq_mask = compute_kv_seq_mask( |
| 557 | query_abs_pos, |
| 558 | seq_offset, |
| 559 | seq_idx, |
| 560 | seq_len, |
| 561 | mm_prefix_range_ptr, |
| 562 | SLIDING_WINDOW, |
| 563 | USE_MM_PREFIX, |
| 564 | MAX_MM_RANGES, |
| 565 | ) |
| 566 | |
| 567 | # Score: split-dot across the 2 INT4 streams; fused |
| 568 | # softmax_scale * per-(token, head) k_scale in one mul. INT4 |
| 569 | # subtracts the ``zp * sum(Q)`` correction term. |
| 570 | S = tl.zeros(shape=(BLOCK_M, TILE_SIZE), dtype=tl.float32) |
| 571 | raw_dot = tl.dot(Q_s0, K_s0) + tl.dot(Q_s1, K_s1) |
| 572 | S += (raw_dot - Q_sum[:, None] * k_zp[None, :]) * ( |
| 573 | scale * k_token_head_scales[None, :] |
| 574 | ) |
| 575 | |
| 576 | if USE_SOFTCAP: |
| 577 | S = apply_softcap(S, softcap) |
| 578 | |
| 579 | S = tl.where( |
| 580 | query_mask_1[:, None] & query_mask_0[:, None] & seq_mask, S, float("-inf") |
| 581 | ) |
| 582 | |
| 583 | if USE_ALIBI_SLOPES: |
| 584 | S = apply_alibi_to_score( |
| 585 | S, alibi_slope, seq_offset, context_len, query_pos, USE_ALIBI_SQRT |
| 586 | ) |
| 587 | |
| 588 | if USE_QQ_BIAS: |
| 589 | S += load_qq_bias_tile( |
| 590 | qq_bias_row_ptrs, seq_offset, context_len, qq_bias_stride_0 |
| 591 | ) |
| 592 | |
| 593 | M, L, P, alpha = softmax_step(S, M, L) |
| 594 | acc_s0 = acc_s0 * alpha[:, None] |
| 595 | acc_s1 = acc_s1 * alpha[:, None] |
| 596 | |
| 597 | if SLIDING_WINDOW: |
| 598 | qpos_lo = q_block_local_idx * BLOCK_Q |
| 599 | sw_mask = (context_len + qpos_lo - seq_offset) < SLIDING_WINDOW |
| 600 | V_s0 = tl.where(sw_mask[:, None], V_s0, 0.0) |
| 601 | V_s1 = tl.where(sw_mask[:, None], V_s1, 0.0) |
| 602 | |
| 603 | # Fuse v per-(token, head) scale into P. INT4 also subtracts |
| 604 | # the v-zero-point contribution from each stream once. |
| 605 | P_v = (P * v_token_head_scales[None, :]).to(tl.float32) |
| 606 | Pv_zp_sum = tl.sum(P_v * v_zp[None, :], axis=1) |
| 607 | acc_s0 += tl.dot(P_v, V_s0) - Pv_zp_sum[:, None] |
| 608 | acc_s1 += tl.dot(P_v, V_s1) - Pv_zp_sum[:, None] |
| 609 | |
| 610 | # Epilogue. 2D writes the final output with optional FP8 clamp; |
| 611 | # 3D writes the per-segment partials (output / max / expsum) for |
| 612 | # ``reduce_segments`` to finalize. Each stream writes its own |
| 613 | # stripe in the output layout. |
| 614 | out_mask = query_mask_0[:, None] & query_mask_1[:, None] |
| 615 | if IS_3D: |
| 616 | segm_base = ( |
| 617 | query_offset_0[:, None].to(tl.int64) |
| 618 | * (num_query_heads * NUM_SEGMENTS_PER_SEQ * HEAD_SIZE_PADDED) |
| 619 | + query_offset_1[:, None] * (NUM_SEGMENTS_PER_SEQ * HEAD_SIZE_PADDED) |
| 620 | + segm_idx * HEAD_SIZE_PADDED |
| 621 | ) |
| 622 | tl.store( |
| 623 | segm_output_ptr + segm_base + offs_s0[None, :], |
| 624 | acc_s0, |
| 625 | mask=mask_s0[None, :] & out_mask, |
| 626 | ) |
| 627 | tl.store( |
| 628 | segm_output_ptr + segm_base + offs_s1[None, :], |
| 629 | acc_s1, |
| 630 | mask=mask_s1[None, :] & out_mask, |
| 631 | ) |
| 632 | store_segm_reduce_scalars( |
| 633 | segm_max_ptr, |
| 634 | segm_expsum_ptr, |
| 635 | query_offset_0, |
| 636 | query_offset_1, |
| 637 | segm_idx, |
| 638 | M, |
| 639 | L, |
| 640 | query_mask_0, |
| 641 | query_mask_1, |
| 642 | num_query_heads, |
| 643 | NUM_SEGMENTS_PER_SEQ, |
| 644 | ) |
| 645 | else: |
| 646 | acc_s0 = acc_s0 / L[:, None] |
| 647 | acc_s1 = acc_s1 / L[:, None] |
| 648 | if USE_FP8: |
| 649 | out_s = tl.load(out_scale) |
| 650 | acc_s0 = tl.clamp(acc_s0 * out_s, FP8_MIN, FP8_MAX) |
| 651 | acc_s1 = tl.clamp(acc_s1 * out_s, FP8_MIN, FP8_MAX) |
| 652 | out_base = ( |
| 653 | query_offset_0[:, None] * output_stride_0 |
| 654 | + query_offset_1[:, None] * output_stride_1 |
| 655 | ) |
| 656 | tl.store( |
| 657 | output_ptr + out_base + offs_s0[None, :], |
| 658 | acc_s0, |
| 659 | mask=mask_s0[None, :] & out_mask, |
| 660 | ) |
| 661 | tl.store( |
| 662 | output_ptr + out_base + offs_s1[None, :], |
| 663 | acc_s1, |
| 664 | mask=mask_s1[None, :] & out_mask, |
| 665 | ) |
| 666 | |
| 667 | |
| 668 | def _launch_packed_attn( |
| 669 | *, |
| 670 | q, |
| 671 | k_cache, |
| 672 | v_cache, |
| 673 | out, |
| 674 | cu_seqlens_q, |
| 675 | max_seqlen_q, |
| 676 | seqused_k, |
| 677 | softmax_scale, |
| 678 | window_size, |
| 679 | block_table, |
| 680 | softcap, |
| 681 | sinks, |
| 682 | alibi_slopes, |
| 683 | use_alibi_sqrt, |
| 684 | qq_bias, |
| 685 | output_scale, |
| 686 | mm_prefix_range, |
| 687 | k_scale_cache, |
| 688 | v_scale_cache, |
| 689 | seq_threshold_3D, |
| 690 | num_par_softmax_segments, |
| 691 | softmax_segm_output, |
| 692 | softmax_segm_max, |
| 693 | softmax_segm_expsum, |
| 694 | packing_factor: int, |
| 695 | ): |
| 696 | """Launch ``_attn_packed`` for one of the sub-byte modes. |
| 697 | |
| 698 | Handles 2D-vs-3D dispatch, placeholder pointers for the unused side |
| 699 | of that split, and the trailing ``reduce_segments`` pass. Writes |
| 700 | into ``out`` (directly for 2D; via the segm buffers for 3D). |
| 701 | """ |
| 702 | import vllm.envs as envs |
| 703 | from vllm.v1.attention.ops.triton_unified_attention import _get_tile_size |
| 704 | |
| 705 | is_batch_invariant = envs.VLLM_BATCH_INVARIANT |
| 706 | |
| 707 | use_mm_prefix = False |
| 708 | max_mm_ranges = 0 |
| 709 | if mm_prefix_range is not None: |
| 710 | assert mm_prefix_range.ndim == 3, ( |
| 711 | f"Unsupported mm_prefix_range shape: {mm_prefix_range.shape}" |
| 712 | ) |
| 713 | use_mm_prefix = True |
| 714 | max_mm_ranges = mm_prefix_range.shape[1] |
| 715 | |
| 716 | block_size = v_cache.shape[1] |
| 717 | num_seqs = len(seqused_k) |
| 718 | num_query_heads = q.shape[1] |
| 719 | num_kv_heads = k_cache.shape[2] |
| 720 | num_queries_per_kv = num_query_heads // num_kv_heads |
| 721 | head_size = q.shape[2] |
| 722 | |
| 723 | BLOCK_M = ( |
| 724 | 16 if num_queries_per_kv <= 16 else triton.next_power_of_2(num_queries_per_kv) |
| 725 | ) |
| 726 | BLOCK_Q = BLOCK_M // num_queries_per_kv |
| 727 | total_num_q_blocks = q.shape[0] // BLOCK_Q + num_seqs |
| 728 | sliding_window_val = 1 + window_size[0] if window_size[0] >= 0 else 0 |
| 729 | TILE_SIZE_PREFILL = _get_tile_size( |
| 730 | head_size, sliding_window_val, q.element_size(), is_prefill=True |
| 731 | ) |
| 732 | TILE_SIZE_DECODE = _get_tile_size( |
| 733 | head_size, sliding_window_val, q.element_size(), is_prefill=False |
| 734 | ) |
| 735 | |
| 736 | use_3d = not ( |
| 737 | seq_threshold_3D is None |
| 738 | or num_par_softmax_segments is None |
| 739 | or softmax_segm_output is None |
| 740 | or softmax_segm_max is None |
| 741 | or softmax_segm_expsum is None |
| 742 | or max_seqlen_q > 1 |
| 743 | or num_seqs > seq_threshold_3D |
| 744 | or is_batch_invariant |
| 745 | ) |
| 746 | |
| 747 | # 3D never reads ``output_ptr`` and 2D never reads the segm tensors, |
| 748 | # but Triton needs a non-null pointer everywhere; reuse ``out`` as |
| 749 | # the placeholder for the unused side. |
| 750 | segm_output_ptr = softmax_segm_output if use_3d else out |
| 751 | segm_max_ptr = softmax_segm_max if use_3d else out |
| 752 | segm_expsum_ptr = softmax_segm_expsum if use_3d else out |
| 753 | num_segments = num_par_softmax_segments if use_3d else 1 |
| 754 | |
| 755 | grid: tuple[Any, ...] |
| 756 | if use_3d: |
| 757 | grid = (total_num_q_blocks, num_kv_heads, num_par_softmax_segments) |
| 758 | tile_size = TILE_SIZE_DECODE |
| 759 | else: |
| 760 | grid = (total_num_q_blocks, num_kv_heads) |
| 761 | tile_size = TILE_SIZE_PREFILL |
| 762 | |
| 763 | _attn_packed[grid]( |
| 764 | output_ptr=out, |
| 765 | segm_output_ptr=segm_output_ptr, |
| 766 | segm_max_ptr=segm_max_ptr, |
| 767 | segm_expsum_ptr=segm_expsum_ptr, |
| 768 | query_ptr=q, |
| 769 | key_cache_ptr=k_cache, |
| 770 | value_cache_ptr=v_cache, |
| 771 | sink_ptr=sinks, |
| 772 | block_tables_ptr=block_table, |
| 773 | seq_lens_ptr=seqused_k, |
| 774 | alibi_slopes_ptr=alibi_slopes, |
| 775 | qq_bias_ptr=qq_bias, |
| 776 | scale=softmax_scale, |
| 777 | out_scale=1 / output_scale if output_scale is not None else 1.0, |
| 778 | softcap=softcap, |
| 779 | k_scale_cache_ptr=k_scale_cache, |
| 780 | v_scale_cache_ptr=v_scale_cache, |
| 781 | num_query_heads=num_query_heads, |
| 782 | num_queries_per_kv=num_queries_per_kv, |
| 783 | block_table_stride=block_table.stride(0), |
| 784 | query_stride_0=q.stride(0), |
| 785 | query_stride_1=q.stride(1), |
| 786 | output_stride_0=out.stride(0), |
| 787 | output_stride_1=out.stride(1), |
| 788 | qq_bias_stride_0=qq_bias.stride(0) if qq_bias is not None else 0, |
| 789 | BLOCK_SIZE=block_size, |
| 790 | TILE_SIZE=tile_size, |
| 791 | HEAD_SIZE=head_size, |
| 792 | HEAD_SIZE_PADDED=triton.next_power_of_2(head_size), |
| 793 | PACKED_HEAD_PADDED=triton.next_power_of_2(head_size) // packing_factor, |
| 794 | USE_ALIBI_SLOPES=alibi_slopes is not None, |
| 795 | USE_ALIBI_SQRT=use_alibi_sqrt, |
| 796 | USE_QQ_BIAS=qq_bias is not None, |
| 797 | USE_SOFTCAP=(softcap > 0), |
| 798 | USE_SINKS=(sinks is not None), |
| 799 | SLIDING_WINDOW=(1 + window_size[0]), |
| 800 | USE_MM_PREFIX=use_mm_prefix, |
| 801 | MAX_MM_RANGES=max_mm_ranges, |
| 802 | mm_prefix_range_ptr=mm_prefix_range, |
| 803 | stride_k_cache_0=k_cache.stride(0), |
| 804 | stride_k_cache_1=k_cache.stride(1), |
| 805 | stride_k_cache_2=k_cache.stride(2), |
| 806 | stride_k_cache_3=k_cache.stride(3), |
| 807 | stride_v_cache_0=v_cache.stride(0), |
| 808 | stride_v_cache_1=v_cache.stride(1), |
| 809 | stride_v_cache_2=v_cache.stride(2), |
| 810 | stride_v_cache_3=v_cache.stride(3), |
| 811 | stride_ks_blk=k_scale_cache.stride(0), |
| 812 | stride_ks_slot=k_scale_cache.stride(1), |
| 813 | stride_ks_head=k_scale_cache.stride(2), |
| 814 | stride_vs_blk=v_scale_cache.stride(0), |
| 815 | stride_vs_slot=v_scale_cache.stride(1), |
| 816 | stride_vs_head=v_scale_cache.stride(2), |
| 817 | query_start_len_ptr=cu_seqlens_q, |
| 818 | BLOCK_Q=BLOCK_Q, |
| 819 | num_seqs=num_seqs, |
| 820 | BLOCK_M=BLOCK_M, |
| 821 | NUM_SEGMENTS_PER_SEQ=num_segments, |
| 822 | USE_FP8=output_scale is not None, |
| 823 | IS_3D=use_3d, |
| 824 | PACKING_FACTOR=packing_factor, |
| 825 | ) |
| 826 | |
| 827 | if use_3d: |
| 828 | reduce_segments[(q.shape[0], num_query_heads)]( |
| 829 | output_ptr=out, |
| 830 | segm_output_ptr=softmax_segm_output, |
| 831 | segm_max_ptr=softmax_segm_max, |
| 832 | segm_expsum_ptr=softmax_segm_expsum, |
| 833 | seq_lens_ptr=seqused_k, |
| 834 | num_seqs=num_seqs, |
| 835 | num_query_heads=num_query_heads, |
| 836 | out_scale_inv=1 / output_scale if output_scale is not None else 1.0, |
| 837 | output_stride_0=out.stride(0), |
| 838 | output_stride_1=out.stride(1), |
| 839 | block_table_stride=block_table.stride(0), |
| 840 | TILE_SIZE=TILE_SIZE_DECODE, |
| 841 | HEAD_SIZE=head_size, |
| 842 | HEAD_SIZE_PADDED=triton.next_power_of_2(head_size), |
| 843 | query_start_len_ptr=cu_seqlens_q, |
| 844 | BLOCK_Q=BLOCK_Q, |
| 845 | NUM_SEGMENTS_PER_SEQ=num_par_softmax_segments, |
| 846 | USE_FP8=output_scale is not None, |
| 847 | ) |
| 848 | |
| 849 | |
| 850 | # ---------------------------------------------------------------------- |
| 851 | # Public entry points |
| 852 | # ---------------------------------------------------------------------- |
| 853 | |
| 854 | |
| 855 | def reshape_and_cache_int4( |
| 856 | key: torch.Tensor, |
| 857 | value: torch.Tensor, |
| 858 | key_cache: torch.Tensor, |
| 859 | value_cache: torch.Tensor, |
| 860 | slot_mapping: torch.Tensor, |
| 861 | *, |
| 862 | k_scale_cache: torch.Tensor, |
| 863 | v_scale_cache: torch.Tensor, |
| 864 | ) -> None: |
| 865 | """Pre-rotate (RHT), pack to INT4 and write into the paged cache.""" |
| 866 | key = single_rht(key.float()).to(key.dtype) |
| 867 | value = single_rht(value.float()).to(value.dtype) |
| 868 | _run_reshape_kernel( |
| 869 | _reshape_cache_int4_kernel, |
| 870 | key=key, |
| 871 | value=value, |
| 872 | key_cache=key_cache, |
| 873 | value_cache=value_cache, |
| 874 | k_scale_cache=k_scale_cache, |
| 875 | v_scale_cache=v_scale_cache, |
| 876 | slot_mapping=slot_mapping, |
| 877 | packing_factor=_INT4_PACKING_FACTOR, |
| 878 | ) |
| 879 | |
| 880 | |
| 881 | def unified_attention_int4( |
| 882 | q: torch.Tensor, |
| 883 | k_cache: torch.Tensor, |
| 884 | v_cache: torch.Tensor, |
| 885 | out: torch.Tensor, |
| 886 | *, |
| 887 | cu_seqlens_q: torch.Tensor, |
| 888 | max_seqlen_q: int, |
| 889 | seqused_k: torch.Tensor, |
| 890 | max_seqlen_k: int, |
| 891 | softmax_scale: float, |
| 892 | window_size: tuple[int, int], |
| 893 | block_table: torch.Tensor, |
| 894 | softcap: float, |
| 895 | sinks: torch.Tensor | None, |
| 896 | alibi_slopes: torch.Tensor | None, |
| 897 | use_alibi_sqrt: bool, |
| 898 | qq_bias: torch.Tensor | None, |
| 899 | output_scale: torch.Tensor | None, |
| 900 | mm_prefix_range: torch.Tensor | None, |
| 901 | k_scale_cache: torch.Tensor, |
| 902 | v_scale_cache: torch.Tensor, |
| 903 | seq_threshold_3D: int | None = None, |
| 904 | num_par_softmax_segments: int | None = None, |
| 905 | softmax_segm_output: torch.Tensor | None = None, |
| 906 | softmax_segm_max: torch.Tensor | None = None, |
| 907 | softmax_segm_expsum: torch.Tensor | None = None, |
| 908 | ) -> None: |
| 909 | """Paged attention over the INT4 packed cache, writing into *out*. |
| 910 | |
| 911 | The forward RHT has norm ``sqrt(head_size)``, so ``softmax_scale`` is |
| 912 | divided by ``head_size`` and the inverse RHT divides the output by |
| 913 | ``head_size`` as well. |
| 914 | """ |
| 915 | q_orig_dtype = q.dtype |
| 916 | q = single_rht(q.float()).to(q_orig_dtype) |
| 917 | head_size = q.shape[2] |
| 918 | softmax_scale = softmax_scale / head_size |
| 919 | |
| 920 | _launch_packed_attn( |
| 921 | q=q, |
| 922 | k_cache=k_cache, |
| 923 | v_cache=v_cache, |
| 924 | out=out, |
| 925 | cu_seqlens_q=cu_seqlens_q, |
| 926 | max_seqlen_q=max_seqlen_q, |
| 927 | seqused_k=seqused_k, |
| 928 | softmax_scale=softmax_scale, |
| 929 | window_size=window_size, |
| 930 | block_table=block_table, |
| 931 | softcap=softcap, |
| 932 | sinks=sinks, |
| 933 | alibi_slopes=alibi_slopes, |
| 934 | use_alibi_sqrt=use_alibi_sqrt, |
| 935 | qq_bias=qq_bias, |
| 936 | output_scale=output_scale, |
| 937 | mm_prefix_range=mm_prefix_range, |
| 938 | k_scale_cache=k_scale_cache, |
| 939 | v_scale_cache=v_scale_cache, |
| 940 | seq_threshold_3D=seq_threshold_3D, |
| 941 | num_par_softmax_segments=num_par_softmax_segments, |
| 942 | softmax_segm_output=softmax_segm_output, |
| 943 | softmax_segm_max=softmax_segm_max, |
| 944 | softmax_segm_expsum=softmax_segm_expsum, |
| 945 | packing_factor=_INT4_PACKING_FACTOR, |
| 946 | ) |
| 947 | |
| 948 | out_f = single_rht(out.float(), inverse=True) / head_size |
| 949 | out.copy_(out_f.to(q_orig_dtype)) |
| 950 | |
| 951 | |
| 952 | # ---------------------------------------------------------------------- |
| 953 | # Randomized Hadamard Transform (RHT) — gaussianizes K/V before INT4 |
| 954 | # quantization; applied on write and to Q before the read kernels. |
| 955 | # ---------------------------------------------------------------------- |
| 956 | |
| 957 | # Hadacore (CUDA tensor core kernel) availability check |
| 958 | # Hadacore's CUDA impl is only registered when built for sm_80+, but the |
| 959 | # schema def is unconditional — on ROCm ``hasattr`` is True yet dispatch |
| 960 | # would crash, so we also gate on ``is_cuda()`` and the sm_80 capability. |
| 961 | _HADACORE_AVAILABLE: bool | None = None |
| 962 | |
| 963 | |
| 964 | def _hadacore_available() -> bool: |
| 965 | global _HADACORE_AVAILABLE |
| 966 | if _HADACORE_AVAILABLE is None: |
| 967 | _HADACORE_AVAILABLE = ( |
| 968 | current_platform.is_cuda() |
| 969 | and current_platform.has_device_capability(80) |
| 970 | and hasattr(torch.ops._C, "hadacore_transform") |
| 971 | ) |
| 972 | return _HADACORE_AVAILABLE |
| 973 | |
| 974 | |
| 975 | # Cached Hadamard matrices (one per (size, dtype, device) tuple) |
| 976 | _HADAMARD_MATRIX_CACHE: dict[tuple[int, torch.dtype, str], torch.Tensor] = {} |
| 977 | |
| 978 | |
| 979 | def _get_hadamard_matrix( |
| 980 | d: int, dtype: torch.dtype, device: torch.device |
| 981 | ) -> torch.Tensor: |
| 982 | key = (d, dtype, str(device)) |
| 983 | cached = _HADAMARD_MATRIX_CACHE.get(key) |
| 984 | if cached is None: |
| 985 | H = torch.ones(1, 1, dtype=torch.float32, device=device) |
| 986 | while H.shape[0] < d: |
| 987 | H = torch.cat( |
| 988 | [ |
| 989 | torch.cat([H, H], dim=1), |
| 990 | torch.cat([H, -H], dim=1), |
| 991 | ], |
| 992 | dim=0, |
| 993 | ) |
| 994 | cached = H.to(dtype).contiguous() |
| 995 | _HADAMARD_MATRIX_CACHE[key] = cached |
| 996 | return cached |
| 997 | |
| 998 | |
| 999 | # Triton MMA Hadamard kernel (Tier 2) |
| 1000 | @triton.jit |
| 1001 | def _hadamard_mma_kernel( |
| 1002 | x_ptr, |
| 1003 | h_ptr, |
| 1004 | out_ptr, |
| 1005 | n_rows, |
| 1006 | stride_x_row: tl.int64, |
| 1007 | stride_x_col: tl.int64, |
| 1008 | stride_o_row: tl.int64, |
| 1009 | stride_o_col: tl.int64, |
| 1010 | BLOCK_M: tl.constexpr, |
| 1011 | D: tl.constexpr, |
| 1012 | ): |
| 1013 | pid = tl.program_id(0) |
| 1014 | rows = pid * BLOCK_M + tl.arange(0, BLOCK_M) |
| 1015 | cols = tl.arange(0, D) |
| 1016 | row_mask = rows < n_rows |
| 1017 | |
| 1018 | x = tl.load( |
| 1019 | x_ptr + rows[:, None] * stride_x_row + cols[None, :] * stride_x_col, |
| 1020 | mask=row_mask[:, None], |
| 1021 | other=0.0, |
| 1022 | ) |
| 1023 | H = tl.load(h_ptr + cols[:, None] * D + cols[None, :]) |
| 1024 | |
| 1025 | out = tl.dot(x, H, out_dtype=tl.float32).to(x.dtype) |
| 1026 | |
| 1027 | tl.store( |
| 1028 | out_ptr + rows[:, None] * stride_o_row + cols[None, :] * stride_o_col, |
| 1029 | out, |
| 1030 | mask=row_mask[:, None], |
| 1031 | ) |
| 1032 | |
| 1033 | |
| 1034 | # H is D×D bf16 = 2·D² bytes of LDS. AMD CDNA has 64 KiB LDS, so D ≤ 128 |
| 1035 | # (32 KiB) leaves room for input + accumulator. Larger D falls back. |
| 1036 | _TRITON_HADAMARD_MIN_D = 16 |
| 1037 | _TRITON_HADAMARD_MAX_D = 128 |
| 1038 | |
| 1039 | |
| 1040 | def _triton_hadamard_transform(x: torch.Tensor) -> torch.Tensor: |
| 1041 | d = x.shape[-1] |
| 1042 | orig_shape = x.shape |
| 1043 | orig_dtype = x.dtype |
| 1044 | |
| 1045 | work_dtype = torch.bfloat16 if orig_dtype == torch.float32 else orig_dtype |
| 1046 | x2d = x.contiguous().to(work_dtype).reshape(-1, d) |
| 1047 | out2d = torch.empty_like(x2d) |
| 1048 | n_rows = x2d.shape[0] |
| 1049 | H_mat = _get_hadamard_matrix(d, work_dtype, x.device) |
| 1050 | |
| 1051 | BLOCK_M = 16 |
| 1052 | grid = (triton.cdiv(n_rows, BLOCK_M),) |
| 1053 | # num_stages=1: the kernel has no loop, so default 3-stage pipelining |
| 1054 | # would triple-buffer H and blow the AMD LDS budget. |
| 1055 | _hadamard_mma_kernel[grid]( |
| 1056 | x2d, |
| 1057 | H_mat, |
| 1058 | out2d, |
| 1059 | n_rows, |
| 1060 | x2d.stride(0), |
| 1061 | x2d.stride(1), |
| 1062 | out2d.stride(0), |
| 1063 | out2d.stride(1), |
| 1064 | BLOCK_M=BLOCK_M, |
| 1065 | D=d, |
| 1066 | num_stages=1, |
| 1067 | num_warps=4, |
| 1068 | ) |
| 1069 | return out2d.reshape(orig_shape).to(orig_dtype) |
| 1070 | |
| 1071 | |
| 1072 | # Public API |
| 1073 | def fast_hadamard_transform(x: torch.Tensor) -> torch.Tensor: |
| 1074 | """Unnormalized Walsh-Hadamard Transform along the last dimension. |
| 1075 | |
| 1076 | H_d × x where H_d × H_d = d × I. Last dim must be a power of 2. |
| 1077 | |
| 1078 | Three-tier dispatch: |
| 1079 | 1. Hadacore CUDA Tensor Core kernel (sm_80+). |
| 1080 | 2. Triton MMA matmul kernel (CUDA fallback + ROCm MFMA/WMMA path). |
| 1081 | 3. PyTorch butterfly (CPU and any GPU/dtype combo Triton can't take). |
| 1082 | """ |
| 1083 | d = x.shape[-1] |
| 1084 | assert d & (d - 1) == 0, f"Requires power-of-2 dim, got {d}" |
| 1085 | |
| 1086 | # Tier 1 — hadacore on CUDA. |
| 1087 | if _hadacore_available() and 0 < d <= (1 << 15): |
| 1088 | from vllm import _custom_ops as ops |
| 1089 | |
| 1090 | # hadacore returns x @ (H/√d); rescale to the unnormalized H × x |
| 1091 | # convention the INT4 scale math is calibrated to. |
| 1092 | rescale = d**0.5 |
| 1093 | if x.dtype in (torch.float16, torch.bfloat16): |
| 1094 | y = ops.hadacore_transform(x.contiguous().clone(), inplace=True) |
| 1095 | return y * rescale |
| 1096 | # fp32 → bf16 round-trip; precision loss is irrelevant before |
| 1097 | # INT4 quantization. |
| 1098 | orig_dtype = x.dtype |
| 1099 | x_bf16 = x.contiguous().to(torch.bfloat16) |
| 1100 | y_bf16 = ops.hadacore_transform(x_bf16, inplace=True) |
| 1101 | return y_bf16.to(orig_dtype) * rescale |
| 1102 | |
| 1103 | # Tier 2 — Triton MMA kernel (covers ROCm via MFMA/WMMA codegen, and |
| 1104 | # also CUDA when hadacore is unavailable). |
| 1105 | if ( |
| 1106 | x.is_cuda |
| 1107 | and _TRITON_HADAMARD_MIN_D <= d <= _TRITON_HADAMARD_MAX_D |
| 1108 | and x.dtype in (torch.float16, torch.bfloat16, torch.float32) |
| 1109 | ): |
| 1110 | return _triton_hadamard_transform(x) |
| 1111 | |
| 1112 | # Tier 3 — PyTorch butterfly (CPU / unsupported dtype / D < 16). |
| 1113 | h = 1 |
| 1114 | while h < d: |
| 1115 | xv = x.view(*x.shape[:-1], d // (2 * h), 2, h) |
| 1116 | a = xv[..., 0, :] |
| 1117 | b = xv[..., 1, :] |
| 1118 | x = torch.stack([a + b, a - b], dim=-2).reshape(x.shape) |
| 1119 | h <<= 1 |
| 1120 | return x |
| 1121 | |
| 1122 | |
| 1123 | # Randomized Hadamard Transform (used by INT4) |
| 1124 | # Deterministic ±1 signs for Randomized Hadamard Transform. |
| 1125 | # RHT = H × D × x (sign flip + Hadamard). Breaks residual structure |
| 1126 | # in KV vectors, improving quantization quality. |
| 1127 | _RHT_SIGNS_CACHE: dict[tuple[int, int, str], torch.Tensor] = {} |
| 1128 | |
| 1129 | |
| 1130 | def _get_rht_signs(d: int, round_idx: int, device: torch.device) -> torch.Tensor: |
| 1131 | """Return a cached deterministic ±1 sign vector of length *d*.""" |
| 1132 | key = (d, round_idx, str(device)) |
| 1133 | if key not in _RHT_SIGNS_CACHE: |
| 1134 | gen = torch.Generator(device="cpu") |
| 1135 | gen.manual_seed(0x9E3779B9 + round_idx * 0x517CC1B7) |
| 1136 | signs = ( |
| 1137 | 2.0 * torch.bernoulli(torch.full((d,), 0.5, device="cpu"), generator=gen) |
| 1138 | - 1.0 |
| 1139 | ) |
| 1140 | _RHT_SIGNS_CACHE[key] = signs.to(device) |
| 1141 | return _RHT_SIGNS_CACHE[key] |
| 1142 | |
| 1143 | |
| 1144 | def single_rht(x: torch.Tensor, inverse: bool = False) -> torch.Tensor: |
| 1145 | """Single Randomized Hadamard Transform: H × D₁ × x. |
| 1146 | |
| 1147 | Used by INT4 per-token-head quantization to gaussianize data |
| 1148 | before asymmetric quantization. |
| 1149 | """ |
| 1150 | d = x.shape[-1] |
| 1151 | d1 = _get_rht_signs(d, 0, x.device) |
| 1152 | if inverse: |
| 1153 | return fast_hadamard_transform(x) * d1 |
| 1154 | else: |
| 1155 | return fast_hadamard_transform(x * d1) |