| 1 | # SPDX-License-Identifier: Apache-2.0 |
| 2 | # SPDX-FileCopyrightText: Copyright contributors to the vLLM project |
| 3 | """Indexer K production for DeepSeek V4.1 kv-source layers. |
| 4 | |
| 5 | In v4.1 the index key is derived from the *main* compressor's latent: |
| 6 | ``k = k_norm(wk(latent))`` (reference model.py Indexer.forward), then RoPE'd |
| 7 | at the group's first-token position and MXFP4/FP8-quantized into the paged |
| 8 | indexer K cache. The ``wk(latent)`` GEMM runs in torch; this kernel fuses the |
| 9 | remaining k_norm → RoPE → quant → paged store, one program per token. |
| 10 | |
| 11 | Unlike the legacy (v4.0) indexer path there is no per-token pooling from a |
| 12 | compressor state cache: the latent already stands for a whole group, so only |
| 13 | group-boundary tokens ``(position + 1) % compress_ratio == 0`` produce a key. |
| 14 | """ |
| 15 | |
| 16 | import torch |
| 17 | |
| 18 | from vllm.platforms import current_platform |
| 19 | from vllm.triton_utils import tl, triton |
| 20 | |
| 21 | from . import MXFP4_BLOCK_SIZE, _fp32x2_to_fp4x2 |
| 22 | |
| 23 | # ROCm tiled ("SHUFFLE") indexer K value layout, mirroring |
| 24 | # indexer_k_quant_and_cache_triton's defaults: 16 positions x 16 bytes. |
| 25 | _BLOCK_TILE_SIZE = 16 |
| 26 | _HEAD_TILE_SIZE = 16 |
| 27 | |
| 28 | |
| 29 | def indexer_k_norm_rope_store( |
| 30 | k_pre: torch.Tensor, |
| 31 | positions: torch.Tensor, |
| 32 | cos_sin_cache: torch.Tensor, |
| 33 | rms_norm_weight: torch.Tensor, |
| 34 | rms_norm_eps: float, |
| 35 | k_cache: torch.Tensor, |
| 36 | kv_slot_mapping: torch.Tensor, |
| 37 | compress_ratio: int, |
| 38 | use_fp4_cache: bool, |
| 39 | ) -> None: |
| 40 | """k_norm → RoPE → quant → paged store for indexer keys. |
| 41 | |
| 42 | Args: |
| 43 | k_pre: [num_tokens, 128] bf16, the ``wk(latent)`` projection. Only |
| 44 | group-boundary rows are read. |
| 45 | positions: [num_tokens] int64 token positions. |
| 46 | cos_sin_cache: [max_pos, rope_head_dim] GPT-J layout (cos half, then |
| 47 | sin half), from the layer's compress-RoPE instance. |
| 48 | rms_norm_weight: [128] k_norm weight. |
| 49 | rms_norm_eps: Epsilon of the k_norm RMSNorm. |
| 50 | k_cache: uint8 paged indexer cache [num_blocks, block_size, row_bytes]. |
| 51 | kv_slot_mapping: [num_tokens] slots in the indexer cache (-1 = skip). |
| 52 | compress_ratio: group size; keys are emitted at group boundaries. |
| 53 | use_fp4_cache: MXFP4 (2 nibbles/byte + ue8m0 per 32) when True, else |
| 54 | per-token FP8 with a single fp32 scale. |
| 55 | |
| 56 | """ |
| 57 | num_tokens = kv_slot_mapping.numel() |
| 58 | assert k_pre.ndim == 2 and k_pre.shape[1] == 128 |
| 59 | assert k_pre.dtype == torch.bfloat16 and k_pre.stride(1) == 1 |
| 60 | assert num_tokens <= k_pre.shape[0] and num_tokens <= positions.numel() |
| 61 | assert compress_ratio in (1, 2) |
| 62 | if num_tokens == 0: |
| 63 | return |
| 64 | |
| 65 | head_dim = k_pre.shape[1] |
| 66 | if use_fp4_cache: |
| 67 | token_stride = head_dim // 2 |
| 68 | scale_dim = head_dim // MXFP4_BLOCK_SIZE |
| 69 | else: |
| 70 | token_stride = head_dim |
| 71 | scale_dim = 4 # single float32 scale |
| 72 | |
| 73 | # ROCm reads this cache back with the 16x16-tiled ("SHUFFLE") value |
| 74 | # layout: cp_gather_indexer_k_quant_cache_triton on prefill and aiter's |
| 75 | # deepgemm_fp8_paged_mqa_logits(Preshuffle=True) on decode both select it |
| 76 | # from ``block_size > 1``, matching the v4.0 writer |
| 77 | # (indexer_k_quant_and_cache_triton). Writing row-major here would hand |
| 78 | # both readers permuted key bytes. |
| 79 | block_size = k_cache.shape[1] |
| 80 | shuffle = current_platform.is_rocm() and block_size > 1 |
| 81 | if shuffle: |
| 82 | if use_fp4_cache: |
| 83 | raise NotImplementedError( |
| 84 | "MXFP4 indexer K cache has no tiled ROCm layout; " |
| 85 | "the ROCm readers only implement the FP8 one." |
| 86 | ) |
| 87 | if block_size % _BLOCK_TILE_SIZE != 0 or head_dim % _HEAD_TILE_SIZE != 0: |
| 88 | raise ValueError( |
| 89 | f"ROCm tiled indexer K cache needs block_size " |
| 90 | f"({block_size}) % {_BLOCK_TILE_SIZE} == 0 and head_dim " |
| 91 | f"({head_dim}) % {_HEAD_TILE_SIZE} == 0." |
| 92 | ) |
| 93 | |
| 94 | launch_kwargs = {"launch_pdl": False} if current_platform.is_cuda() else {} |
| 95 | _indexer_k_norm_rope_quant_store_kernel[(num_tokens,)]( |
| 96 | k_pre, |
| 97 | k_pre.stride(0), |
| 98 | positions, |
| 99 | rms_norm_weight, |
| 100 | rms_norm_eps, |
| 101 | cos_sin_cache, |
| 102 | cos_sin_cache.stride(0), |
| 103 | k_cache, |
| 104 | kv_slot_mapping, |
| 105 | k_cache.shape[1], |
| 106 | HEAD_SIZE=head_dim, |
| 107 | ROPE_HEAD_DIM=64, |
| 108 | COMPRESS_RATIO=compress_ratio, |
| 109 | TOKEN_STRIDE=token_stride, |
| 110 | SCALE_DIM=scale_dim, |
| 111 | KV_BLOCK_STRIDE=k_cache.stride(0), |
| 112 | FP8_MAX=448.0, |
| 113 | USE_FP4=use_fp4_cache, |
| 114 | SHUFFLE=shuffle, |
| 115 | BLOCK_TILE_SIZE=_BLOCK_TILE_SIZE, |
| 116 | HEAD_TILE_SIZE=_HEAD_TILE_SIZE, |
| 117 | num_warps=1, |
| 118 | **launch_kwargs, |
| 119 | ) |
| 120 | |
| 121 | |
| 122 | @triton.jit |
| 123 | def _indexer_k_norm_rope_quant_store_kernel( |
| 124 | k_pre_ptr, |
| 125 | k_pre_stride, |
| 126 | positions_ptr, |
| 127 | rms_norm_weight_ptr, |
| 128 | rms_norm_eps, |
| 129 | cos_sin_cache_ptr, |
| 130 | cos_sin_stride, |
| 131 | k_cache_ptr, |
| 132 | kv_slot_mapping_ptr, |
| 133 | kv_cache_block_size, |
| 134 | HEAD_SIZE: tl.constexpr, |
| 135 | ROPE_HEAD_DIM: tl.constexpr, |
| 136 | COMPRESS_RATIO: tl.constexpr, |
| 137 | TOKEN_STRIDE: tl.constexpr, |
| 138 | SCALE_DIM: tl.constexpr, |
| 139 | KV_BLOCK_STRIDE: tl.constexpr, |
| 140 | FP8_MAX: tl.constexpr, |
| 141 | USE_FP4: tl.constexpr, |
| 142 | SHUFFLE: tl.constexpr, |
| 143 | BLOCK_TILE_SIZE: tl.constexpr, |
| 144 | HEAD_TILE_SIZE: tl.constexpr, |
| 145 | ): |
| 146 | token_idx = tl.program_id(0) |
| 147 | |
| 148 | kv_slot_idx = tl.load(kv_slot_mapping_ptr + token_idx) |
| 149 | if kv_slot_idx < 0: |
| 150 | return |
| 151 | position = tl.load(positions_ptr + token_idx) |
| 152 | # Only the last token of a group publishes that group's index key. |
| 153 | if (position + 1) % COMPRESS_RATIO != 0: |
| 154 | return |
| 155 | |
| 156 | block = tl.arange(0, HEAD_SIZE) |
| 157 | |
| 158 | # ── k_norm (fp32 throughout, bf16 roundtrip like the reference) ──── |
| 159 | k = tl.load(k_pre_ptr + token_idx * k_pre_stride + block).to(tl.float32) |
| 160 | rms_w = tl.load(rms_norm_weight_ptr + block).to(tl.float32) |
| 161 | variance = tl.sum(k * k, axis=0) / HEAD_SIZE |
| 162 | k = (k * tl.rsqrt(variance + rms_norm_eps) * rms_w).to(tl.bfloat16) |
| 163 | k = k.to(tl.float32) |
| 164 | |
| 165 | # ── Register-based GPT-J forward RoPE in fp32 ───────────────────── |
| 166 | # A latent stands for the first token of its group, so group j takes |
| 167 | # position j * compress_ratio. |
| 168 | NUM_PAIRS: tl.constexpr = HEAD_SIZE // 2 |
| 169 | NOPE_HEAD_DIM: tl.constexpr = HEAD_SIZE - ROPE_HEAD_DIM |
| 170 | NOPE_PAIRS: tl.constexpr = NOPE_HEAD_DIM // 2 |
| 171 | HALF_ROPE: tl.constexpr = ROPE_HEAD_DIM // 2 |
| 172 | |
| 173 | even, odd = tl.split(tl.reshape(k, (NUM_PAIRS, 2))) # each [NUM_PAIRS] |
| 174 | pair_idx = tl.arange(0, NUM_PAIRS) |
| 175 | rope_pair_local = pair_idx - NOPE_PAIRS |
| 176 | is_rope_pair = rope_pair_local >= 0 |
| 177 | cs_idx = tl.maximum(rope_pair_local, 0) |
| 178 | |
| 179 | compressed_pos = (position // COMPRESS_RATIO) * COMPRESS_RATIO |
| 180 | cache_base = cos_sin_cache_ptr + compressed_pos * cos_sin_stride |
| 181 | cos_v = tl.load(cache_base + cs_idx, mask=is_rope_pair, other=1.0) |
| 182 | sin_v = tl.load(cache_base + HALF_ROPE + cs_idx, mask=is_rope_pair, other=0.0) |
| 183 | |
| 184 | new_even = even * cos_v - odd * sin_v |
| 185 | new_odd = odd * cos_v + even * sin_v |
| 186 | |
| 187 | # bf16 roundtrip for parity with the reference / Q-side kernel numerics. |
| 188 | new_even = new_even.to(tl.bfloat16).to(tl.float32) |
| 189 | new_odd = new_odd.to(tl.bfloat16).to(tl.float32) |
| 190 | |
| 191 | # ── Paged cache pointers (segregated: values first, then scales) ── |
| 192 | kv_block_idx = kv_slot_idx // kv_cache_block_size |
| 193 | kv_pos_in_block = kv_slot_idx % kv_cache_block_size |
| 194 | cache_block_ptr = k_cache_ptr + kv_block_idx.to(tl.int64) * KV_BLOCK_STRIDE |
| 195 | if SHUFFLE: |
| 196 | # [block_size // BT, HEAD_SIZE // HT, BT, HT] value tiles. |
| 197 | val_ptr = ( |
| 198 | cache_block_ptr |
| 199 | + (kv_pos_in_block // BLOCK_TILE_SIZE) * BLOCK_TILE_SIZE * TOKEN_STRIDE |
| 200 | + (kv_pos_in_block % BLOCK_TILE_SIZE) * HEAD_TILE_SIZE |
| 201 | ) |
| 202 | else: |
| 203 | val_ptr = cache_block_ptr + kv_pos_in_block * TOKEN_STRIDE |
| 204 | scale_ptr = ( |
| 205 | cache_block_ptr |
| 206 | + kv_cache_block_size * TOKEN_STRIDE |
| 207 | + kv_pos_in_block * SCALE_DIM |
| 208 | ) |
| 209 | |
| 210 | if USE_FP4: |
| 211 | # MXFP4: each 32-element block = 16 consecutive even/odd pairs, so |
| 212 | # tiling the halves into (N_BLOCKS, 16) lands one block per row. |
| 213 | N_QUANT_BLOCKS: tl.constexpr = HEAD_SIZE // 32 |
| 214 | HALF_BLOCK: tl.constexpr = 16 |
| 215 | even_2d = tl.reshape(new_even, (N_QUANT_BLOCKS, HALF_BLOCK)) |
| 216 | odd_2d = tl.reshape(new_odd, (N_QUANT_BLOCKS, HALF_BLOCK)) |
| 217 | |
| 218 | amax = tl.maximum( |
| 219 | tl.max(tl.abs(even_2d), axis=1), |
| 220 | tl.max(tl.abs(odd_2d), axis=1), |
| 221 | ) |
| 222 | amax = tl.maximum(amax, 6.0 * (2**-126)) |
| 223 | # ue8m0 block scale: 2^ceil(log2(amax / 6.0)), stored (exp + 127). |
| 224 | log2_ratio = tl.ceil(tl.log2(amax * (1.0 / 6.0))) |
| 225 | log2_ratio = tl.minimum(tl.maximum(log2_ratio, -127.0), 127.0) |
| 226 | inv_scale = tl.exp2(-log2_ratio) |
| 227 | ue8m0 = (log2_ratio + 127.0).to(tl.uint8) # [N_QUANT_BLOCKS] |
| 228 | |
| 229 | inv_scale_col = tl.reshape(inv_scale, (N_QUANT_BLOCKS, 1)) |
| 230 | packed = _fp32x2_to_fp4x2( |
| 231 | even_2d * inv_scale_col, odd_2d * inv_scale_col |
| 232 | ) # (N_BLOCKS, HALF_BLOCK) uint8 |
| 233 | packed_flat = tl.reshape(packed, (TOKEN_STRIDE,)) |
| 234 | |
| 235 | tl.store(val_ptr + tl.arange(0, TOKEN_STRIDE), packed_flat) |
| 236 | tl.store(scale_ptr + tl.arange(0, SCALE_DIM), ue8m0) |
| 237 | else: |
| 238 | # Per-token FP8 (single 128-wide block) with one float32 scale. |
| 239 | result = tl.interleave(new_even, new_odd) # [HEAD_SIZE] fp32 |
| 240 | result_bf16 = result.to(tl.bfloat16).to(tl.float32) |
| 241 | INV_FP8_MAX: tl.constexpr = 1.0 / FP8_MAX |
| 242 | absmax = tl.maximum(tl.max(tl.abs(result_bf16), axis=0), 1e-4) |
| 243 | exponent = tl.ceil(tl.log2(absmax * INV_FP8_MAX)) |
| 244 | inv_scale = tl.exp2(-exponent) |
| 245 | x_clamped = tl.clamp(result_bf16 * inv_scale, -FP8_MAX, FP8_MAX) |
| 246 | x_uint8 = x_clamped.to(tl.float8e4nv).to(tl.uint8, bitcast=True) |
| 247 | if SHUFFLE: |
| 248 | tiled = ( |
| 249 | block // HEAD_TILE_SIZE * BLOCK_TILE_SIZE * HEAD_TILE_SIZE |
| 250 | + block % HEAD_TILE_SIZE |
| 251 | ) |
| 252 | tl.store(val_ptr + tiled, x_uint8) |
| 253 | else: |
| 254 | tl.store(val_ptr + block, x_uint8) |
| 255 | tl.store(scale_ptr.to(tl.pointer_type(tl.float32)), tl.exp2(exponent)) |