Skip to content

Commit b5f79b8

Browse files
committed
[KMCompiler][Hygon] Token-tiled fused_deepseek_v4_qnorm_rope_kv_rope_quant_insert
1 parent a8a683c commit b5f79b8

2 files changed

Lines changed: 274 additions & 0 deletions

File tree

src/flag_gems/runtime/backend/_hygon/fused/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,14 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
from .fused_deepseek_v4_qnorm_rope_kv_rope_quant_insert import (
16+
fused_deepseek_v4_qnorm_rope_kv_rope_quant_insert,
17+
)
1518
from .sparse_attention import sparse_attn_triton
1619
from .top_k_per_row_prefill import top_k_per_row_prefill
1720

1821
__all__ = [
22+
"fused_deepseek_v4_qnorm_rope_kv_rope_quant_insert",
1923
"sparse_attn_triton",
2024
"top_k_per_row_prefill",
2125
]
Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
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

Comments
 (0)