|
| 1 | +# Copyright 2026 FlagOS Contributors |
| 2 | +# |
| 3 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 | +# you may not use this file except in compliance with the License. |
| 5 | +# You may obtain a copy of the License at |
| 6 | +# |
| 7 | +# http://www.apache.org/licenses/LICENSE-2.0 |
| 8 | +# |
| 9 | +# Unless required by applicable law or agreed to in writing, software |
| 10 | +# distributed under the License is distributed on an "AS IS" BASIS, |
| 11 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | +# See the License for the specific language governing permissions and |
| 13 | +# limitations under the License. |
| 14 | +"""Hygon override: token-tiled fused_deepseek_v4_qnorm_rope_kv_rope_quant_insert. |
| 15 | +
|
| 16 | +The generic kernel runs one program per (token, head) slot at num_warps=1 -- 64 |
| 17 | +threads for 512 elements, which on BW1000's 64-lane warp is 8 elements per lane. |
| 18 | +That leaves more than half the bandwidth unused: measured 604.8 GB/s at |
| 19 | +32768x64 against a 1340.3 GB/s ceiling (512 MiB device-to-device copy), i.e. |
| 20 | +45.1%. |
| 21 | +
|
| 22 | +Giving each program TPP tokens of ONE slot raises elements per lane to 16 and the |
| 23 | +block to 256 threads: |
| 24 | +
|
| 25 | + shape generic tiled of ceiling |
| 26 | + 32768 x 64 604.8 GB/s 1182.9 GB/s 45.1% -> 88.3% |
| 27 | + 32768 x 128 609.2 GB/s 1201.4 GB/s 45.5% -> 89.6% |
| 28 | +
|
| 29 | +Two axes matter and neither is visible on its own. A full TPP x num_warps sweep |
| 30 | +puts every optimum at TPP/num_warps = 2, which is two tokens per warp and so 16 |
| 31 | +elements per lane; bandwidth by elements per lane is 237 / 408 / 604 / 906 / |
| 32 | +**1183** / 1077 / 1050 for 1 / 2 / 4 / 8 / 16 / 32 / 64. But elements per lane |
| 33 | +does not explain everything: TPP=1/warps=1 and TPP=2/warps=2 are both 8 elements |
| 34 | +per lane and differ by 50% (604 vs 906 GB/s), because the second has a wider |
| 35 | +program. Sweeping num_warps alone at TPP=1 shows 8 and 4 elements per lane tied, |
| 36 | +which invites the wrong conclusion that access width does not matter -- at TPP=1 |
| 37 | +the block is only 512 elements and there is nothing to widen into. Do not tune |
| 38 | +these two parameters separately. |
| 39 | +
|
| 40 | +TPP=8 with num_warps=4 is one of the optimal points and is also what the MetaX |
| 41 | +C550 override uses; both parts have 64-lane warps, so the tuning transfers. |
| 42 | +
|
| 43 | +Below a threshold the generic kernel wins, because a 256-thread block costs about |
| 44 | +10 us more to launch here (measured at one token: 80.5 us versus 90.2 us) and |
| 45 | +TPP=8 masks off most of every program when there are fewer than 8 tokens to fill |
| 46 | +it. The crossover was measured at 256 tokens for 64 heads and 128 tokens for 128 |
| 47 | +heads -- which are 16640 and 16512 programs respectively, so the real quantity is |
| 48 | +the program count, not the token count. Dispatching on the grid size covers both |
| 49 | +head counts; a flat token threshold would forfeit the 1.16x-1.32x available |
| 50 | +between 128 and 256 tokens at 128 heads. |
| 51 | +
|
| 52 | +Output is bit-identical to the generic kernel: the FP8 cache matches byte for |
| 53 | +byte and q matches exactly, including at token counts that are not multiples of |
| 54 | +TPP. |
| 55 | +""" |
| 56 | + |
| 57 | +import torch |
| 58 | +import triton |
| 59 | +import triton.language as tl |
| 60 | + |
| 61 | +# Measured on Hygon BW1000; see the module docstring. The threshold is a program |
| 62 | +# count -- num_tokens * (num_heads + 1) -- because that is what the measured |
| 63 | +# crossovers agree on across head counts. |
| 64 | +_TILED_MIN_PROGRAMS = 16384 |
| 65 | +_TPP = 8 |
| 66 | +_NUM_WARPS = 4 |
| 67 | + |
| 68 | + |
| 69 | +@triton.jit |
| 70 | +def _tiled_kernel( |
| 71 | + q, |
| 72 | + kv, |
| 73 | + k_cache, |
| 74 | + slot_mapping, |
| 75 | + position_ids, |
| 76 | + cos_sin_cache, |
| 77 | + eps, |
| 78 | + cache_block_size: tl.constexpr, |
| 79 | + num_tokens, |
| 80 | + num_heads: tl.constexpr, |
| 81 | + kv_block_stride, |
| 82 | + num_tokens_insert, |
| 83 | + TPP: tl.constexpr, # tokens per program |
| 84 | +): |
| 85 | + HEAD_DIM: tl.constexpr = 512 |
| 86 | + NOPE_DIM: tl.constexpr = 448 |
| 87 | + ROPE_DIM: tl.constexpr = 64 |
| 88 | + HALF_ROPE_DIM: tl.constexpr = 32 |
| 89 | + QUANT_BLOCK: tl.constexpr = 64 |
| 90 | + NUM_QUANT_BLOCKS: tl.constexpr = NOPE_DIM // QUANT_BLOCK # 7 |
| 91 | + SCALE_BYTES_PER_TOKEN: tl.constexpr = NUM_QUANT_BLOCKS + 1 # 8 |
| 92 | + TOKEN_DATA_BYTES: tl.constexpr = NOPE_DIM + 2 * ROPE_DIM # 576 |
| 93 | + FP8_MAX: tl.constexpr = 448.0 |
| 94 | + |
| 95 | + pid = tl.program_id(0).to(tl.int64) |
| 96 | + blocks_per_token: tl.constexpr = num_heads + 1 |
| 97 | + |
| 98 | + # grid = cdiv(num_tokens, TPP) * blocks_per_token |
| 99 | + tile = pid // blocks_per_token |
| 100 | + slot_idx = pid % blocks_per_token |
| 101 | + is_kv = slot_idx == num_heads |
| 102 | + |
| 103 | + tok = tile * TPP + tl.arange(0, TPP).to(tl.int64) # [TPP] |
| 104 | + tok_ok = tok < num_tokens |
| 105 | + |
| 106 | + off = tl.arange(0, HEAD_DIM) # [HEAD_DIM] |
| 107 | + off_rope = tl.arange(0, ROPE_DIM) |
| 108 | + off_half = tl.arange(0, HALF_ROPE_DIM) |
| 109 | + off_quant = tl.arange(0, QUANT_BLOCK) |
| 110 | + |
| 111 | + # cos/sin are needed by both paths |
| 112 | + pos = tl.load(position_ids + tok, mask=tok_ok, other=0) # [TPP] |
| 113 | + cs_base = cos_sin_cache + pos[:, None] * ROPE_DIM |
| 114 | + cos_blk = tl.load(cs_base + off_half[None, :], mask=tok_ok[:, None], other=0.0) |
| 115 | + sin_blk = tl.load( |
| 116 | + cs_base + (off_half + HALF_ROPE_DIM)[None, :], mask=tok_ok[:, None], other=0.0 |
| 117 | + ) |
| 118 | + |
| 119 | + if not is_kv: |
| 120 | + # ── Q: per-head RMSNorm (no weight) + GPT-J RoPE, in place ── |
| 121 | + q_base = q + (tok * num_heads + slot_idx) * HEAD_DIM # [TPP] |
| 122 | + q_blk = tl.load( |
| 123 | + q_base[:, None] + off[None, :], mask=tok_ok[:, None], other=0.0 |
| 124 | + ).to(tl.float32) |
| 125 | + variance = tl.sum(q_blk * q_blk, axis=1) / HEAD_DIM # [TPP] |
| 126 | + rsqrt = tl.rsqrt(variance + eps) |
| 127 | + q_blk = q_blk * rsqrt[:, None] |
| 128 | + tl.store( |
| 129 | + q_base[:, None] + off[None, :], |
| 130 | + q_blk.to(tl.bfloat16), |
| 131 | + mask=tok_ok[:, None] & (off[None, :] < NOPE_DIM), |
| 132 | + ) |
| 133 | + rope = ( |
| 134 | + tl.load( |
| 135 | + q_base[:, None] + NOPE_DIM + off_rope[None, :], |
| 136 | + mask=tok_ok[:, None], |
| 137 | + other=0.0, |
| 138 | + ).to(tl.float32) |
| 139 | + * rsqrt[:, None] |
| 140 | + ) |
| 141 | + else: |
| 142 | + kv_base = kv + tok * HEAD_DIM |
| 143 | + rope = tl.load( |
| 144 | + kv_base[:, None] + NOPE_DIM + off_rope[None, :], |
| 145 | + mask=tok_ok[:, None], |
| 146 | + other=0.0, |
| 147 | + ).to(tl.float32) |
| 148 | + |
| 149 | + # ── GPT-J interleaved RoPE on the trailing ROPE_DIM ── |
| 150 | + rope = tl.reshape(rope, TPP, HALF_ROPE_DIM, 2) |
| 151 | + even, odd = tl.split(rope) # [TPP, HALF_ROPE_DIM] |
| 152 | + new_even = even * cos_blk - odd * sin_blk |
| 153 | + new_odd = even * sin_blk + odd * cos_blk |
| 154 | + rope = tl.reshape(tl.join(new_even, new_odd), TPP, ROPE_DIM).to(tl.bfloat16) |
| 155 | + |
| 156 | + if not is_kv: |
| 157 | + q_base = q + (tok * num_heads + slot_idx) * HEAD_DIM |
| 158 | + tl.store( |
| 159 | + q_base[:, None] + NOPE_DIM + off_rope[None, :], |
| 160 | + rope, |
| 161 | + mask=tok_ok[:, None], |
| 162 | + ) |
| 163 | + return |
| 164 | + |
| 165 | + # ── KV: RoPE already applied; UE8M0 FP8 quant + paged cache insert ── |
| 166 | + kv_base = kv + tok * HEAD_DIM |
| 167 | + ins_ok = tok_ok & (tok < num_tokens_insert) |
| 168 | + slot_id = tl.load(slot_mapping + tok, mask=ins_ok, other=-1) # [TPP] |
| 169 | + ins_ok = ins_ok & (slot_id >= 0) |
| 170 | + |
| 171 | + block_idx = slot_id // cache_block_size |
| 172 | + pos_in_block = slot_id % cache_block_size |
| 173 | + block_base = block_idx * kv_block_stride |
| 174 | + token_fp8 = block_base + pos_in_block * TOKEN_DATA_BYTES # [TPP] byte offset |
| 175 | + token_scale = ( |
| 176 | + block_base |
| 177 | + + cache_block_size * TOKEN_DATA_BYTES |
| 178 | + + pos_in_block * SCALE_BYTES_PER_TOKEN |
| 179 | + ) |
| 180 | + |
| 181 | + bf16_ptr = (k_cache + token_fp8 + NOPE_DIM).to(tl.pointer_type(tl.bfloat16)) |
| 182 | + tl.store(bf16_ptr[:, None] + off_rope[None, :], rope, mask=ins_ok[:, None]) |
| 183 | + |
| 184 | + for b in tl.static_range(NUM_QUANT_BLOCKS): |
| 185 | + blk = tl.load( |
| 186 | + kv_base[:, None] + b * QUANT_BLOCK + off_quant[None, :], |
| 187 | + mask=ins_ok[:, None], |
| 188 | + other=0.0, |
| 189 | + ).to(tl.float32) |
| 190 | + block_max = tl.maximum(tl.max(tl.abs(blk), axis=1), 1e-4) # [TPP] |
| 191 | + exponent = tl.ceil(tl.log2(block_max / FP8_MAX)) |
| 192 | + scale = tl.exp2(exponent) |
| 193 | + x = tl.clamp(blk / scale[:, None], -FP8_MAX, FP8_MAX) |
| 194 | + tl.store( |
| 195 | + k_cache + token_fp8[:, None] + b * QUANT_BLOCK + off_quant[None, :], |
| 196 | + x.to(tl.float8e4nv).to(tl.uint8, bitcast=True), |
| 197 | + mask=ins_ok[:, None], |
| 198 | + ) |
| 199 | + enc = tl.maximum(tl.minimum(exponent + 127.0, 255.0), 0.0) |
| 200 | + tl.store(k_cache + token_scale + b, enc.to(tl.uint8), mask=ins_ok) |
| 201 | + |
| 202 | + tl.store( |
| 203 | + k_cache + token_scale + NUM_QUANT_BLOCKS, |
| 204 | + tl.zeros((TPP,), dtype=tl.uint8), |
| 205 | + mask=ins_ok, |
| 206 | + ) |
| 207 | + |
| 208 | + |
| 209 | +def fused_deepseek_v4_qnorm_rope_kv_rope_quant_insert( |
| 210 | + q: torch.Tensor, |
| 211 | + kv: torch.Tensor, |
| 212 | + k_cache: torch.Tensor, |
| 213 | + slot_mapping: torch.Tensor, |
| 214 | + position_ids: torch.Tensor, |
| 215 | + cos_sin_cache: torch.Tensor, |
| 216 | + eps: float, |
| 217 | + cache_block_size: int, |
| 218 | +): |
| 219 | + """See the generic implementation for the layout contract.""" |
| 220 | + assert q.is_contiguous() and kv.is_contiguous() |
| 221 | + num_tokens, num_heads, head_dims = q.shape |
| 222 | + |
| 223 | + if num_tokens * (num_heads + 1) < _TILED_MIN_PROGRAMS: |
| 224 | + # Small shapes are launch-bound and the narrower generic kernel wins. |
| 225 | + from flag_gems.fused.fused_deepseek_v4_qnorm_rope_kv_rope_quant_insert import ( |
| 226 | + fused_deepseek_v4_qnorm_rope_kv_rope_quant_insert as _generic, |
| 227 | + ) |
| 228 | + |
| 229 | + return _generic( |
| 230 | + q, |
| 231 | + kv, |
| 232 | + k_cache, |
| 233 | + slot_mapping, |
| 234 | + position_ids, |
| 235 | + cos_sin_cache, |
| 236 | + eps, |
| 237 | + cache_block_size, |
| 238 | + ) |
| 239 | + |
| 240 | + assert head_dims == 512 |
| 241 | + assert kv.shape == (num_tokens, 512) |
| 242 | + assert q.dtype == torch.bfloat16 and kv.dtype == torch.bfloat16 |
| 243 | + assert k_cache.dtype == torch.uint8 |
| 244 | + assert slot_mapping.dim() == 1 |
| 245 | + num_tokens_insert = slot_mapping.shape[0] |
| 246 | + assert num_tokens_insert <= num_tokens |
| 247 | + assert slot_mapping.dtype == torch.int64 |
| 248 | + assert position_ids.shape == (num_tokens,) |
| 249 | + assert position_ids.dtype == torch.int64 |
| 250 | + assert cos_sin_cache.dim() == 2 and cos_sin_cache.shape[1] == 64 |
| 251 | + assert cos_sin_cache.dtype == torch.float32 |
| 252 | + |
| 253 | + grid = triton.cdiv(num_tokens, _TPP) * (num_heads + 1) |
| 254 | + _tiled_kernel[(grid,)]( |
| 255 | + q, |
| 256 | + kv, |
| 257 | + k_cache, |
| 258 | + slot_mapping, |
| 259 | + position_ids, |
| 260 | + cos_sin_cache, |
| 261 | + eps, |
| 262 | + cache_block_size, |
| 263 | + num_tokens, |
| 264 | + num_heads, |
| 265 | + k_cache.stride(0), |
| 266 | + num_tokens_insert, |
| 267 | + TPP=_TPP, |
| 268 | + num_warps=_NUM_WARPS, |
| 269 | + num_stages=2, |
| 270 | + ) |
0 commit comments