| 1 | # SPDX-License-Identifier: Apache-2.0 |
| 2 | # SPDX-FileCopyrightText: Copyright contributors to the vLLM project |
| 3 | """Benchmark vLLM Kimi-K3 AttnRes kernels on NVIDIA Blackwell. |
| 4 | |
| 5 | The benchmark uses a zero delta so repeated in-place launches remain stable. |
| 6 | |
| 7 | Example: |
| 8 | |
| 9 | .. code-block:: console |
| 10 | |
| 11 | .venv/bin/python benchmarks/kernels/benchmark_kimi_k3_attn_res.py \ |
| 12 | --output /tmp/attn_res.json |
| 13 | |
| 14 | """ |
| 15 | |
| 16 | from __future__ import annotations |
| 17 | |
| 18 | import argparse |
| 19 | import json |
| 20 | from collections.abc import Callable |
| 21 | from dataclasses import asdict, dataclass |
| 22 | from functools import partial |
| 23 | from pathlib import Path |
| 24 | |
| 25 | import torch |
| 26 | |
| 27 | import vllm._custom_ops # noqa: F401 - loads torch.ops._C |
| 28 | from vllm.models.kimi_k3.nvidia.ops.attn_res import _attn_res_kernel |
| 29 | from vllm.platforms import current_platform |
| 30 | from vllm.triton_utils import triton |
| 31 | |
| 32 | HIDDEN_SIZE = 7168 |
| 33 | MAX_BLOCKS = 8 |
| 34 | EPS = 1e-5 |
| 35 | |
| 36 | |
| 37 | @dataclass |
| 38 | class Inputs: |
| 39 | prefix: torch.Tensor |
| 40 | delta: torch.Tensor |
| 41 | blocks: torch.Tensor |
| 42 | norm_weight: torch.Tensor |
| 43 | qk_weight: torch.Tensor |
| 44 | output_norm_weight: torch.Tensor |
| 45 | |
| 46 | |
| 47 | @dataclass |
| 48 | class Result: |
| 49 | num_tokens: int |
| 50 | num_blocks: int |
| 51 | backend: str |
| 52 | median_us: float |
| 53 | p20_us: float |
| 54 | p80_us: float |
| 55 | |
| 56 | |
| 57 | def parse_args() -> argparse.Namespace: |
| 58 | parser = argparse.ArgumentParser(description=__doc__) |
| 59 | parser.add_argument( |
| 60 | "--num-tokens", |
| 61 | type=int, |
| 62 | nargs="+", |
| 63 | default=[1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192], |
| 64 | ) |
| 65 | parser.add_argument("--num-blocks", type=int, nargs="+", default=[1, 4, 8]) |
| 66 | parser.add_argument("--warmup-ms", type=int, default=50) |
| 67 | parser.add_argument("--repeat-ms", type=int, default=200) |
| 68 | parser.add_argument("--output", type=Path) |
| 69 | return parser.parse_args() |
| 70 | |
| 71 | |
| 72 | def make_inputs(num_tokens: int) -> Inputs: |
| 73 | prefix = torch.randn(num_tokens, HIDDEN_SIZE, device="cuda", dtype=torch.bfloat16) |
| 74 | blocks = torch.randn( |
| 75 | num_tokens, |
| 76 | MAX_BLOCKS, |
| 77 | HIDDEN_SIZE, |
| 78 | device="cuda", |
| 79 | dtype=torch.bfloat16, |
| 80 | ) |
| 81 | norm_weight = torch.ones(HIDDEN_SIZE, device="cuda", dtype=torch.bfloat16) |
| 82 | qk_weight = ( |
| 83 | torch.randn(HIDDEN_SIZE, device="cuda", dtype=torch.bfloat16) / HIDDEN_SIZE**0.5 |
| 84 | ) |
| 85 | output_norm_weight = 1 + 0.1 * torch.randn( |
| 86 | HIDDEN_SIZE, device="cuda", dtype=torch.bfloat16 |
| 87 | ) |
| 88 | return Inputs( |
| 89 | prefix=prefix, |
| 90 | delta=torch.zeros_like(prefix), |
| 91 | blocks=blocks, |
| 92 | norm_weight=norm_weight, |
| 93 | qk_weight=qk_weight, |
| 94 | output_norm_weight=output_norm_weight, |
| 95 | ) |
| 96 | |
| 97 | |
| 98 | def launch_triton(inputs: Inputs, output: torch.Tensor, num_blocks: int) -> None: |
| 99 | num_tokens = inputs.prefix.shape[0] |
| 100 | if num_tokens >= 256 or num_blocks <= 1: |
| 101 | block_l, num_warps = 1, 4 |
| 102 | else: |
| 103 | block_l, num_warps = 4, 8 |
| 104 | _attn_res_kernel[(num_tokens,)]( |
| 105 | inputs.prefix, |
| 106 | inputs.delta, |
| 107 | inputs.blocks, |
| 108 | inputs.norm_weight, |
| 109 | inputs.qk_weight, |
| 110 | inputs.output_norm_weight, |
| 111 | output, |
| 112 | inputs.prefix.stride(0), |
| 113 | inputs.delta.stride(0), |
| 114 | inputs.blocks.stride(0), |
| 115 | inputs.blocks.stride(1), |
| 116 | output.stride(0), |
| 117 | num_blocks, |
| 118 | HIDDEN_SIZE, |
| 119 | -1, |
| 120 | EPS, |
| 121 | EPS, |
| 122 | HAS_DELTA=True, |
| 123 | WRITE_BLOCK=False, |
| 124 | APPLY_OUTPUT_NORM=True, |
| 125 | BLOCK_L=block_l, |
| 126 | BLOCK_D=triton.next_power_of_2(HIDDEN_SIZE), |
| 127 | num_warps=num_warps, |
| 128 | num_stages=2, |
| 129 | launch_pdl=current_platform.is_arch_support_pdl(), |
| 130 | ) |
| 131 | |
| 132 | |
| 133 | def launch_cuda(inputs: Inputs, output: torch.Tensor, num_blocks: int) -> None: |
| 134 | torch.ops._C.kimi_k3_attn_res( |
| 135 | inputs.prefix, |
| 136 | inputs.delta, |
| 137 | inputs.blocks, |
| 138 | inputs.norm_weight, |
| 139 | inputs.qk_weight, |
| 140 | inputs.output_norm_weight, |
| 141 | output, |
| 142 | num_blocks, |
| 143 | -1, |
| 144 | EPS, |
| 145 | EPS, |
| 146 | ) |
| 147 | |
| 148 | |
| 149 | def benchmark( |
| 150 | function: Callable[[], None], warmup_ms: int, repeat_ms: int |
| 151 | ) -> tuple[float, float, float]: |
| 152 | median_ms, p20_ms, p80_ms = triton.testing.do_bench( |
| 153 | function, |
| 154 | warmup=warmup_ms, |
| 155 | rep=repeat_ms, |
| 156 | quantiles=[0.5, 0.2, 0.8], |
| 157 | ) |
| 158 | return median_ms * 1000, p20_ms * 1000, p80_ms * 1000 |
| 159 | |
| 160 | |
| 161 | def validate(inputs: Inputs, num_blocks: int) -> None: |
| 162 | outputs = [torch.empty_like(inputs.prefix) for _ in range(2)] |
| 163 | launch_triton(inputs, outputs[0], num_blocks) |
| 164 | launch_cuda(inputs, outputs[1], num_blocks) |
| 165 | torch.accelerator.synchronize() |
| 166 | torch.testing.assert_close(outputs[1], outputs[0], atol=8e-2, rtol=3e-2) |
| 167 | |
| 168 | |
| 169 | def main() -> None: |
| 170 | args = parse_args() |
| 171 | if torch.cuda.get_device_capability()[0] != 10: |
| 172 | raise SystemExit("The vLLM CUDA AttnRes kernel requires the SM100 family") |
| 173 | if not hasattr(torch.ops._C, "kimi_k3_attn_res"): |
| 174 | raise SystemExit("This vLLM build does not contain kimi_k3_attn_res") |
| 175 | if any(not 0 <= blocks <= MAX_BLOCKS for blocks in args.num_blocks): |
| 176 | raise SystemExit(f"--num-blocks values must be in [0, {MAX_BLOCKS}]") |
| 177 | |
| 178 | torch.manual_seed(0) |
| 179 | check_inputs = make_inputs(5) |
| 180 | for num_blocks in args.num_blocks: |
| 181 | validate(check_inputs, num_blocks) |
| 182 | print("correctness: all selected block counts passed", flush=True) |
| 183 | |
| 184 | results: list[Result] = [] |
| 185 | for num_blocks in args.num_blocks: |
| 186 | for num_tokens in args.num_tokens: |
| 187 | inputs = make_inputs(num_tokens) |
| 188 | output = torch.empty_like(inputs.prefix) |
| 189 | candidates = { |
| 190 | "vllm_triton": partial(launch_triton, inputs, output, num_blocks), |
| 191 | "vllm_cuda": partial(launch_cuda, inputs, output, num_blocks), |
| 192 | } |
| 193 | row: dict[str, float] = {} |
| 194 | for backend, candidate in candidates.items(): |
| 195 | median_us, p20_us, p80_us = benchmark( |
| 196 | candidate, args.warmup_ms, args.repeat_ms |
| 197 | ) |
| 198 | results.append( |
| 199 | Result( |
| 200 | num_tokens=num_tokens, |
| 201 | num_blocks=num_blocks, |
| 202 | backend=backend, |
| 203 | median_us=median_us, |
| 204 | p20_us=p20_us, |
| 205 | p80_us=p80_us, |
| 206 | ) |
| 207 | ) |
| 208 | row[backend] = median_us |
| 209 | print( |
| 210 | f"blocks={num_blocks} tokens={num_tokens:5d} " |
| 211 | f"triton={row['vllm_triton']:9.2f} us " |
| 212 | f"cuda={row['vllm_cuda']:9.2f} us", |
| 213 | flush=True, |
| 214 | ) |
| 215 | |
| 216 | if args.output is not None: |
| 217 | metadata = { |
| 218 | "device": torch.cuda.get_device_name(), |
| 219 | "capability": torch.cuda.get_device_capability(), |
| 220 | "torch": torch.__version__, |
| 221 | "cuda": torch.version.cuda, |
| 222 | "warmup_ms": args.warmup_ms, |
| 223 | "repeat_ms": args.repeat_ms, |
| 224 | } |
| 225 | args.output.write_text( |
| 226 | json.dumps( |
| 227 | {"metadata": metadata, "results": [asdict(item) for item in results]}, |
| 228 | indent=2, |
| 229 | ) |
| 230 | + "\n" |
| 231 | ) |
| 232 | print(f"wrote {args.output}") |
| 233 | |
| 234 | |
| 235 | if __name__ == "__main__": |
| 236 | main() |