Skip to content

Commit 3db257f

Browse files
sumin-hongcodex
andcommitted
[ROCm][Perf] Fuse DSA indexer QK preprocessing with AITER
Fuse LayerNorm, RoPE, FP8 Q/K quantization, scale folding, and K-cache writes behind a default-off ROCm flag. Add MI3XX shape and parallelism guards, shared outputs, MTP wiring, correctness tests, and a kernel benchmark. Co-authored-by: OpenAI Codex <codex@openai.com> Signed-off-by: Sumin Hong <sumin.hong@moreh.io>
1 parent 14617c2 commit 3db257f

8 files changed

Lines changed: 944 additions & 2 deletions

File tree

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
# SPDX-License-Identifier: Apache-2.0
2+
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3+
"""Fused vs unfused DSA indexer QK pre-processing on ROCm.
4+
5+
The DSA indexer (DeepSeek-V3.2, GLM-5.x) runs five launches per layer per step:
6+
LayerNorm(k), RoPE(q, k), per-token-group fp8 quant of q, folding the q scale
7+
into the indexer weights, and the fp8 K quant + paged K-cache write. With
8+
VLLM_ROCM_USE_AITER_INDEXER_QK_FUSION=1 one AITER kernel does all five.
9+
10+
Usage:
11+
python benchmarks/kernels/benchmark_indexer_qk_fusion.py
12+
python benchmarks/kernels/benchmark_indexer_qk_fusion.py --num-tokens 1 8 64
13+
"""
14+
15+
import argparse
16+
import functools
17+
18+
import torch
19+
20+
from vllm import _custom_ops as ops
21+
from vllm.model_executor.layers.quantization.utils.fp8_utils import (
22+
per_token_group_quant_fp8,
23+
)
24+
from vllm.platforms import current_platform
25+
from vllm.triton_utils import triton
26+
from vllm.v1.attention.ops.rocm_aiter_mla_sparse import (
27+
indexer_k_quant_and_cache_triton,
28+
)
29+
30+
HEAD_DIM = 128
31+
ROPE_DIM = 64
32+
N_HEAD = 32
33+
MAX_POS = 65536
34+
QUANT_BLOCK = 128
35+
EPS = 1e-6
36+
SCALE_FMT = "ue8m0"
37+
WEIGHTS_SCALE = HEAD_DIM**-0.5 * N_HEAD**-0.5
38+
39+
40+
def _unfused(
41+
q, k_raw, weights_raw, positions, cache, norm_w, norm_b, kv, slots, is_neox
42+
):
43+
num_tokens = q.shape[0]
44+
k = torch.nn.functional.layer_norm(
45+
k_raw.float(), (HEAD_DIM,), norm_w.float(), norm_b.float(), EPS
46+
).to(q.dtype)
47+
q = q.clone()
48+
ops.rotary_embedding(
49+
positions,
50+
q[..., :ROPE_DIM],
51+
k[..., :ROPE_DIM].unsqueeze(1),
52+
ROPE_DIM,
53+
cache,
54+
is_neox,
55+
)
56+
q_fp8, q_scale = per_token_group_quant_fp8(
57+
q.view(-1, HEAD_DIM), QUANT_BLOCK, column_major_scales=False, use_ue8m0=True
58+
)
59+
_ = weights_raw.float() * q_scale.view(num_tokens, N_HEAD) * WEIGHTS_SCALE
60+
indexer_k_quant_and_cache_triton(k, kv, slots, QUANT_BLOCK, SCALE_FMT)
61+
62+
63+
def _fused(
64+
q,
65+
k_raw,
66+
weights_raw,
67+
positions,
68+
cache,
69+
norm_w,
70+
norm_b,
71+
kv,
72+
slots,
73+
q_out,
74+
w_out,
75+
is_neox,
76+
):
77+
from aiter import indexer_qk_rope_quant_and_cache
78+
79+
half = ROPE_DIM // 2
80+
indexer_qk_rope_quant_and_cache(
81+
q,
82+
q_out,
83+
weights_raw,
84+
w_out,
85+
k_raw,
86+
kv,
87+
slots,
88+
norm_w,
89+
norm_b,
90+
positions,
91+
cache[:, :half],
92+
cache[:, half:],
93+
EPS,
94+
QUANT_BLOCK,
95+
SCALE_FMT,
96+
WEIGHTS_SCALE,
97+
preshuffle=kv.shape[1] > 1,
98+
is_neox=is_neox,
99+
)
100+
101+
102+
def _time_us(fn) -> float:
103+
ms = triton.testing.do_bench(fn, warmup=25, rep=100)
104+
return ms * 1000.0
105+
106+
107+
def main() -> None:
108+
parser = argparse.ArgumentParser()
109+
parser.add_argument(
110+
"--num-tokens", type=int, nargs="+", default=[1, 8, 32, 64, 256, 1024]
111+
)
112+
parser.add_argument("--block-size", type=int, default=64)
113+
parser.add_argument(
114+
"--is-neox",
115+
action=argparse.BooleanOptionalAction,
116+
default=False,
117+
help="RoPE layout. GLM-5.x sets indexer_rope_interleave, i.e. is_neox "
118+
"False; DeepSeek-V3.2 leaves it at the NeoX default.",
119+
)
120+
parser.add_argument(
121+
"--repeat",
122+
type=int,
123+
default=3,
124+
help="Measure each point this many times and report the median. "
125+
"Process-level state (kernel tuning caches, clocks) moves these numbers "
126+
"more than do_bench's own variance does.",
127+
)
128+
args = parser.parse_args()
129+
130+
if not current_platform.is_rocm():
131+
raise SystemExit("ROCm only")
132+
fp8 = current_platform.fp8_dtype()
133+
dev, dt = "cuda", torch.bfloat16
134+
135+
def median(runs: list[float]) -> tuple[float, float, float]:
136+
ordered = sorted(runs)
137+
return ordered[len(ordered) // 2], ordered[0], ordered[-1]
138+
139+
rows = []
140+
for num_tokens in args.num_tokens:
141+
num_blocks = (num_tokens + args.block_size - 1) // args.block_size + 2
142+
q = torch.randn(num_tokens, N_HEAD, HEAD_DIM, device=dev, dtype=dt)
143+
kw = torch.randn(num_tokens, HEAD_DIM + N_HEAD, device=dev, dtype=dt)
144+
positions = torch.randint(
145+
0, MAX_POS, (num_tokens,), device=dev, dtype=torch.int64
146+
)
147+
norm_w = torch.randn(HEAD_DIM, device=dev, dtype=dt)
148+
norm_b = torch.randn(HEAD_DIM, device=dev, dtype=dt)
149+
cache = torch.randn(MAX_POS, ROPE_DIM, device=dev, dtype=dt)
150+
kv = torch.zeros(
151+
num_blocks, args.block_size, HEAD_DIM + 4, dtype=fp8, device=dev
152+
)
153+
slots = torch.randperm(
154+
num_blocks * args.block_size, device=dev, dtype=torch.int64
155+
)[:num_tokens]
156+
q_out = torch.zeros((num_tokens, N_HEAD, HEAD_DIM), dtype=fp8, device=dev)
157+
w_out = torch.zeros((num_tokens, N_HEAD), dtype=torch.float32, device=dev)
158+
159+
unfused_fn = functools.partial(
160+
_unfused,
161+
q,
162+
kw[:, :HEAD_DIM],
163+
kw[:, HEAD_DIM:],
164+
positions,
165+
cache,
166+
norm_w,
167+
norm_b,
168+
kv,
169+
slots,
170+
args.is_neox,
171+
)
172+
fused_fn = functools.partial(
173+
_fused,
174+
q,
175+
kw[:, :HEAD_DIM],
176+
kw[:, HEAD_DIM:],
177+
positions,
178+
cache,
179+
norm_w,
180+
norm_b,
181+
kv,
182+
slots,
183+
q_out,
184+
w_out,
185+
args.is_neox,
186+
)
187+
unfused_runs = [_time_us(unfused_fn) for _ in range(args.repeat)]
188+
fused_runs = [_time_us(fused_fn) for _ in range(args.repeat)]
189+
rows.append((num_tokens, median(unfused_runs), median(fused_runs)))
190+
191+
print(
192+
f"{'tokens':>8} {'unfused us':>12} {'fused us':>10} {'speedup':>9}"
193+
f" spread over {args.repeat} repeats"
194+
)
195+
for num_tokens, (u_med, u_lo, u_hi), (f_med, f_lo, f_hi) in rows:
196+
print(
197+
f"{num_tokens:>8} {u_med:>12.2f} {f_med:>10.2f} {u_med / f_med:>8.2f}x"
198+
f" unfused {u_lo:.1f}-{u_hi:.1f}, fused {f_lo:.1f}-{f_hi:.1f}"
199+
)
200+
201+
202+
if __name__ == "__main__":
203+
main()

0 commit comments

Comments
 (0)