| 1 | # SPDX-License-Identifier: Apache-2.0 |
| 2 | # SPDX-FileCopyrightText: Copyright contributors to the vLLM project |
| 3 | """Benchmark: Fused FP8 output quantization in merge_attn_states. |
| 4 | |
| 5 | Compares fused vs unfused approaches for producing FP8-quantized merged |
| 6 | attention output: |
| 7 | 1. Fused CUDA -- single CUDA kernel (merge + FP8 quant) |
| 8 | 2. Fused Triton -- single Triton kernel (merge + FP8 quant) |
| 9 | 3. Unfused CUDA -- CUDA merge + torch.compiled FP8 quant |
| 10 | 4. Unfused Triton -- Triton merge + torch.compiled FP8 quant |
| 11 | |
| 12 | Usage: |
| 13 | python benchmarks/fused_kernels/merge_attn_states_benchmarks.py |
| 14 | python benchmarks/fused_kernels/merge_attn_states_benchmarks.py --tp 1 4 8 |
| 15 | python benchmarks/fused_kernels/merge_attn_states_benchmarks.py --dtype bfloat16 |
| 16 | """ |
| 17 | |
| 18 | import argparse |
| 19 | import itertools |
| 20 | |
| 21 | import torch |
| 22 | |
| 23 | from vllm._custom_ops import merge_attn_states as merge_attn_states_cuda |
| 24 | from vllm.benchmarks.lib.utils import default_vllm_config |
| 25 | from vllm.model_executor.layers.quantization.input_quant_fp8 import QuantFP8 |
| 26 | from vllm.model_executor.layers.quantization.utils.quant_utils import GroupShape |
| 27 | from vllm.platforms import current_platform |
| 28 | from vllm.triton_utils import triton |
| 29 | from vllm.v1.attention.ops.triton_merge_attn_states import ( |
| 30 | merge_attn_states as merge_attn_states_triton, |
| 31 | ) |
| 32 | |
| 33 | # --------------------------------------------------------------------------- |
| 34 | # Configuration defaults |
| 35 | # --------------------------------------------------------------------------- |
| 36 | |
| 37 | NUM_TOKENS_LIST = [1, 16, 64, 256, 1024, 4096] |
| 38 | |
| 39 | # (label, num_heads, head_size) — num_heads is for TP=1 |
| 40 | HEAD_CONFIGS = [ |
| 41 | ("DeepSeek-V3 MLA", 128, 128), |
| 42 | ("Llama-70B", 64, 128), |
| 43 | ("Llama-8B", 32, 128), |
| 44 | ] |
| 45 | |
| 46 | TP_SIZES = [1, 2, 4, 8] |
| 47 | |
| 48 | INPUT_DTYPES = [torch.float32, torch.float16, torch.bfloat16] |
| 49 | |
| 50 | QUANTILES = [0.5, 0.2, 0.8] |
| 51 | |
| 52 | |
| 53 | # --------------------------------------------------------------------------- |
| 54 | # Helpers |
| 55 | # --------------------------------------------------------------------------- |
| 56 | |
| 57 | |
| 58 | def short_dtype(dtype: torch.dtype) -> str: |
| 59 | return str(dtype).removeprefix("torch.") |
| 60 | |
| 61 | |
| 62 | def make_inputs( |
| 63 | num_tokens: int, |
| 64 | num_heads: int, |
| 65 | head_size: int, |
| 66 | dtype: torch.dtype, |
| 67 | ): |
| 68 | """Create random prefix/suffix outputs and LSEs.""" |
| 69 | prefix_output = torch.randn( |
| 70 | (num_tokens, num_heads, head_size), dtype=dtype, device="cuda" |
| 71 | ) |
| 72 | suffix_output = torch.randn( |
| 73 | (num_tokens, num_heads, head_size), dtype=dtype, device="cuda" |
| 74 | ) |
| 75 | prefix_lse = torch.randn(num_heads, num_tokens, dtype=torch.float32, device="cuda") |
| 76 | suffix_lse = torch.randn(num_heads, num_tokens, dtype=torch.float32, device="cuda") |
| 77 | # Sprinkle some inf values to exercise edge-case paths |
| 78 | mask = torch.rand(num_heads, num_tokens, device="cuda") < 0.05 |
| 79 | prefix_lse[mask] = float("inf") |
| 80 | mask2 = torch.rand(num_heads, num_tokens, device="cuda") < 0.05 |
| 81 | suffix_lse[mask2] = float("inf") |
| 82 | return prefix_output, suffix_output, prefix_lse, suffix_lse |
| 83 | |
| 84 | |
| 85 | def build_configs(head_configs, num_tokens_list, input_dtypes, tp_sizes): |
| 86 | """Build (num_tokens, num_heads, head_size, dtype_str) config tuples, |
| 87 | applying TP division to num_heads and skipping invalid combos.""" |
| 88 | configs = [] |
| 89 | for (_, nh, hs), nt, dtype, tp in itertools.product( |
| 90 | head_configs, num_tokens_list, input_dtypes, tp_sizes |
| 91 | ): |
| 92 | nh_tp = nh // tp |
| 93 | if nh_tp >= 1: |
| 94 | configs.append((nt, nh_tp, hs, short_dtype(dtype))) |
| 95 | return configs |
| 96 | |
| 97 | |
| 98 | def parse_args(): |
| 99 | parser = argparse.ArgumentParser( |
| 100 | description="Benchmark merge_attn_states fused FP8 quantization" |
| 101 | ) |
| 102 | parser.add_argument( |
| 103 | "--num-tokens", |
| 104 | type=int, |
| 105 | nargs="+", |
| 106 | default=None, |
| 107 | help=f"Override token counts (default: {NUM_TOKENS_LIST})", |
| 108 | ) |
| 109 | parser.add_argument( |
| 110 | "--tp", |
| 111 | type=int, |
| 112 | nargs="+", |
| 113 | default=None, |
| 114 | help=f"TP sizes to simulate (divides num_heads) (default: {TP_SIZES})", |
| 115 | ) |
| 116 | parser.add_argument( |
| 117 | "--dtype", |
| 118 | type=str, |
| 119 | nargs="+", |
| 120 | default=None, |
| 121 | help="Input dtypes (e.g. bfloat16 float16 float32). " |
| 122 | f"Default: {[short_dtype(d) for d in INPUT_DTYPES]}", |
| 123 | ) |
| 124 | return parser.parse_args() |
| 125 | |
| 126 | |
| 127 | # --------------------------------------------------------------------------- |
| 128 | # Parse args and build configs before decorators |
| 129 | # --------------------------------------------------------------------------- |
| 130 | |
| 131 | args = parse_args() |
| 132 | |
| 133 | num_tokens_list = args.num_tokens if args.num_tokens else NUM_TOKENS_LIST |
| 134 | tp_sizes = args.tp if args.tp else TP_SIZES |
| 135 | |
| 136 | if args.dtype: |
| 137 | from vllm.utils.torch_utils import STR_DTYPE_TO_TORCH_DTYPE |
| 138 | |
| 139 | input_dtypes = [STR_DTYPE_TO_TORCH_DTYPE[d] for d in args.dtype] |
| 140 | else: |
| 141 | input_dtypes = INPUT_DTYPES |
| 142 | |
| 143 | configs = build_configs(HEAD_CONFIGS, num_tokens_list, input_dtypes, tp_sizes) |
| 144 | |
| 145 | torch._dynamo.config.recompile_limit = 8888 |
| 146 | |
| 147 | |
| 148 | # --------------------------------------------------------------------------- |
| 149 | # Benchmark function |
| 150 | # --------------------------------------------------------------------------- |
| 151 | |
| 152 | |
| 153 | @triton.testing.perf_report( |
| 154 | triton.testing.Benchmark( |
| 155 | x_names=["num_tokens", "num_heads", "head_size", "dtype_str"], |
| 156 | x_vals=configs, |
| 157 | line_arg="provider", |
| 158 | line_vals=["fused_cuda", "fused_triton", "unfused_cuda", "unfused_triton"], |
| 159 | line_names=["Fused CUDA", "Fused Triton", "Unfused CUDA", "Unfused Triton"], |
| 160 | styles=[("blue", "-"), ("green", "-"), ("blue", "--"), ("green", "--")], |
| 161 | ylabel="us", |
| 162 | plot_name="merge_attn_states FP8 (fused vs unfused)", |
| 163 | args={}, |
| 164 | ) |
| 165 | ) |
| 166 | @default_vllm_config() |
| 167 | def benchmark(num_tokens, num_heads, head_size, dtype_str, provider): |
| 168 | input_dtype = getattr(torch, dtype_str) |
| 169 | fp8_dtype = current_platform.fp8_dtype() |
| 170 | prefix_out, suffix_out, prefix_lse, suffix_lse = make_inputs( |
| 171 | num_tokens, num_heads, head_size, input_dtype |
| 172 | ) |
| 173 | output_scale = torch.tensor([0.1], dtype=torch.float32, device="cuda") |
| 174 | |
| 175 | if provider == "fused_cuda": |
| 176 | output = torch.empty( |
| 177 | (num_tokens, num_heads, head_size), dtype=fp8_dtype, device="cuda" |
| 178 | ) |
| 179 | fn = lambda: merge_attn_states_cuda( |
| 180 | output, |
| 181 | prefix_out, |
| 182 | prefix_lse, |
| 183 | suffix_out, |
| 184 | suffix_lse, |
| 185 | output_scale=output_scale, |
| 186 | ) |
| 187 | elif provider == "fused_triton": |
| 188 | output = torch.empty( |
| 189 | (num_tokens, num_heads, head_size), dtype=fp8_dtype, device="cuda" |
| 190 | ) |
| 191 | fn = lambda: merge_attn_states_triton( |
| 192 | output, |
| 193 | prefix_out, |
| 194 | prefix_lse, |
| 195 | suffix_out, |
| 196 | suffix_lse, |
| 197 | output_scale=output_scale, |
| 198 | ) |
| 199 | elif provider == "unfused_cuda": |
| 200 | merge_buf = torch.empty( |
| 201 | (num_tokens, num_heads, head_size), dtype=input_dtype, device="cuda" |
| 202 | ) |
| 203 | quant_fp8 = QuantFP8( |
| 204 | static=True, |
| 205 | group_shape=GroupShape.PER_TENSOR, |
| 206 | column_major_scales=False, |
| 207 | ) |
| 208 | quant_input = merge_buf.view(-1, head_size) |
| 209 | compiled_quant = torch.compile( |
| 210 | quant_fp8.forward_native, fullgraph=True, dynamic=False |
| 211 | ) |
| 212 | |
| 213 | def unfused_fn(): |
| 214 | merge_attn_states_cuda( |
| 215 | merge_buf, prefix_out, prefix_lse, suffix_out, suffix_lse |
| 216 | ) |
| 217 | compiled_quant(quant_input, output_scale) |
| 218 | |
| 219 | fn = unfused_fn |
| 220 | else: # unfused_triton |
| 221 | merge_buf = torch.empty( |
| 222 | (num_tokens, num_heads, head_size), dtype=input_dtype, device="cuda" |
| 223 | ) |
| 224 | quant_fp8 = QuantFP8( |
| 225 | static=True, |
| 226 | group_shape=GroupShape.PER_TENSOR, |
| 227 | column_major_scales=False, |
| 228 | ) |
| 229 | quant_input = merge_buf.view(-1, head_size) |
| 230 | compiled_quant = torch.compile( |
| 231 | quant_fp8.forward_native, fullgraph=True, dynamic=False |
| 232 | ) |
| 233 | |
| 234 | def unfused_fn(): |
| 235 | merge_attn_states_triton( |
| 236 | merge_buf, prefix_out, prefix_lse, suffix_out, suffix_lse |
| 237 | ) |
| 238 | compiled_quant(quant_input, output_scale) |
| 239 | |
| 240 | fn = unfused_fn |
| 241 | |
| 242 | ms, min_ms, max_ms = triton.testing.do_bench_cudagraph(fn, quantiles=QUANTILES) |
| 243 | return 1000 * ms, 1000 * max_ms, 1000 * min_ms # us |
| 244 | |
| 245 | |
| 246 | # --------------------------------------------------------------------------- |
| 247 | # Main |
| 248 | # --------------------------------------------------------------------------- |
| 249 | |
| 250 | |
| 251 | def main(): |
| 252 | device_name = current_platform.get_device_name() |
| 253 | print(f"Device: {device_name}") |
| 254 | print(f"Token counts: {num_tokens_list}") |
| 255 | print(f"TP sizes: {tp_sizes}") |
| 256 | print(f"Input dtypes: {[short_dtype(d) for d in input_dtypes]}") |
| 257 | print(f"Head configs: {[(c[0], c[1], c[2]) for c in HEAD_CONFIGS]}") |
| 258 | benchmark.run(print_data=True) |
| 259 | |
| 260 | |
| 261 | if __name__ == "__main__": |
| 262 | with torch.inference_mode(): |
| 263 | main() |