|
| 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