main
py 316 lines 9.94 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 import functools
5 import time
6
7 import numpy as np
8 import torch
9
10 from vllm._custom_ops import (
11 cpu_attention_with_kv_cache,
12 cpu_attn_get_scheduler_metadata,
13 cpu_attn_reshape_and_cache,
14 )
15 from vllm.utils.argparse_utils import FlexibleArgumentParser
16 from vllm.utils.torch_utils import (
17 STR_DTYPE_TO_TORCH_DTYPE,
18 is_quantized_kv_cache,
19 set_random_seed,
20 )
21 from vllm.v1.attention.backends.cpu_attn import CPUAttentionBackend, _get_attn_isa
22
23 # Enable AMX tile data registers so isolated runs don't rely on other operations
24 # to trigger oneDNN's _init_amx() first (which can cause hangs otherwise).
25 if torch.cpu._is_amx_tile_supported():
26 torch.cpu._init_amx()
27
28 KV_CACHE_DTYPE_CHOICES = ["auto", "fp8_e4m3", "fp8_e5m2"]
29
30
31 def get_attn_isa(
32 block_size: int | None = None,
33 dtype: torch.dtype | None = None,
34 head_size: int | None = None,
35 kv_cache_dtype: str = "auto",
36 ):
37 # Delegate to _get_attn_isa so the fallback path applies the same arch
38 # gating (e.g. RISC-V RVV is only chosen when the build's hardcoded
39 # VLEN=128 kernel is actually present; on VLEN=256 / scalar hosts it
40 # correctly falls through to vec/vec16).
41 return _get_attn_isa(
42 dtype if dtype is not None else torch.bfloat16,
43 block_size if block_size else 32,
44 head_size=head_size,
45 kv_cache_dtype=kv_cache_dtype if kv_cache_dtype != "auto" else None,
46 )
47
48
49 # rand number generation takes too much time, cache rand tensors
50 @functools.lru_cache(maxsize=128, typed=False)
51 def tensor_cache(
52 elem_num: int,
53 dtype: torch.dtype,
54 ) -> torch.Tensor:
55 tensor = torch.randn(elem_num, dtype=dtype)
56 return tensor
57
58
59 @torch.inference_mode()
60 def main(
61 seq_lens: list[tuple[int, int]],
62 num_heads: tuple[int, int],
63 head_size: int,
64 sliding_window: int = None,
65 dtype: torch.dtype = torch.bfloat16,
66 block_size: int = 128,
67 num_blocks: int = 4096,
68 use_sink: bool = False,
69 enable_kv_split: bool = False,
70 isa: str | None = None,
71 kv_cache_dtype: str = "auto",
72 seed: int = 0,
73 iters: int = 20,
74 ) -> None:
75 set_random_seed(seed)
76 num_seqs = len(seq_lens)
77 query_lens = [x[0] for x in seq_lens]
78 kv_lens = [x[1] for x in seq_lens]
79 num_query_heads = num_heads[0]
80 num_kv_heads = num_heads[1]
81 assert num_query_heads % num_kv_heads == 0
82 max_kv_len = max(kv_lens)
83 window_size = (sliding_window - 1, 0) if sliding_window is not None else (-1, -1)
84 scale = head_size**-0.5
85 token_num = sum(query_lens)
86
87 # Resolve kv_cache_dtype: "auto" means same as compute dtype (no quantization)
88 effective_kv_cache_dtype = kv_cache_dtype if kv_cache_dtype != "auto" else None
89 is_fp8_kv = (
90 is_quantized_kv_cache(effective_kv_cache_dtype)
91 if effective_kv_cache_dtype
92 else False
93 )
94 # FP8 KV cache is stored as uint8 (byte-level view of fp8 values)
95 kv_cache_torch_dtype = torch.uint8 if is_fp8_kv else dtype
96
97 if isa is None:
98 isa = get_attn_isa(block_size, dtype, kv_cache_dtype)
99
100 s_aux = (
101 15 * torch.rand((num_query_heads,), dtype=torch.bfloat16) if use_sink else None
102 )
103
104 query = tensor_cache(
105 elem_num=token_num * num_query_heads * head_size,
106 dtype=dtype,
107 )
108 query = query.view(
109 token_num,
110 num_query_heads,
111 head_size,
112 )
113
114 key_value = tensor_cache(
115 elem_num=2 * num_blocks * num_kv_heads * block_size * head_size,
116 dtype=dtype,
117 )
118 key_value = key_value.view(
119 2,
120 num_blocks,
121 block_size,
122 num_kv_heads,
123 head_size,
124 )
125 key_cache, value_cache = key_value.unbind(0)
126
127 # KV cache for CPU attention; FP8 variants store quantized values as uint8
128 packed_key_cache = torch.empty(
129 num_blocks, num_kv_heads, block_size, head_size, dtype=kv_cache_torch_dtype
130 )
131 packed_value_cache = torch.empty_like(packed_key_cache)
132
133 cu_query_lens = torch.tensor([0] + query_lens, dtype=torch.int32).cumsum(
134 dim=0, dtype=torch.int32
135 )
136 kv_lens_tensor = torch.tensor(kv_lens, dtype=torch.int32)
137 max_num_blocks_per_seq = (max_kv_len + block_size - 1) // block_size
138 block_tables = torch.randint(
139 0, num_blocks, (num_seqs, max_num_blocks_per_seq), dtype=torch.int32
140 )
141
142 # use reshape_and_cache to pack key_cache and value_cache
143 slot_mapping = torch.arange(0, num_blocks * block_size, dtype=torch.int64)
144 cpu_attn_reshape_and_cache(
145 key=key_cache.view(-1, num_kv_heads, head_size),
146 value=value_cache.view(-1, num_kv_heads, head_size),
147 key_cache=packed_key_cache,
148 value_cache=packed_value_cache,
149 slot_mapping=slot_mapping,
150 isa=isa,
151 kv_cache_dtype=kv_cache_dtype,
152 )
153
154 metadata = cpu_attn_get_scheduler_metadata(
155 num_reqs=num_seqs,
156 num_heads=num_query_heads,
157 num_kv_heads=num_kv_heads,
158 head_dim=head_size,
159 seq_lens=kv_lens_tensor,
160 dtype=dtype,
161 query_start_loc=cu_query_lens,
162 causal=True,
163 sliding_window_size=sliding_window if sliding_window is not None else -1,
164 isa=isa,
165 enable_kv_split=enable_kv_split,
166 )
167
168 out_with_split = torch.empty_like(query)
169
170 def run_benchmark(iters: int) -> list[float]:
171 times = []
172 for _ in range(iters):
173 start_time = time.perf_counter_ns()
174 cpu_attention_with_kv_cache(
175 query=query,
176 key_cache=packed_key_cache,
177 value_cache=packed_value_cache,
178 output=out_with_split,
179 query_start_loc=cu_query_lens,
180 seq_lens=kv_lens_tensor,
181 scale=scale,
182 causal=True,
183 alibi_slopes=None,
184 sliding_window=window_size if sliding_window is not None else -1,
185 block_table=block_tables,
186 softcap=0,
187 scheduler_metadata=metadata,
188 s_aux=s_aux,
189 kv_cache_dtype=kv_cache_dtype,
190 )
191 end_time = time.perf_counter_ns()
192 times.append((end_time - start_time) / 1e6)
193 return times
194
195 # warmup
196 run_benchmark(5)
197 # benchmark
198 times = run_benchmark(iters)
199
200 time_min = min(times)
201 time_max = max(times)
202 time_mean = np.mean(times)
203 time_std = np.std(times)
204
205 print("\tmin (ms) = ", time_min)
206 print("\tmax (ms) = ", time_max)
207 print("\tmean (ms) = ", time_mean)
208 print("\tstd = ", time_std)
209 print("\tmedian (ms) = ", np.median(times))
210
211
212 def generate_seq_lens(
213 batch_size: int,
214 q_len_min: int,
215 q_len_max: int,
216 kv_len_min: int,
217 kv_len_max: int,
218 seed: int = 0,
219 ) -> list[tuple[int, int]]:
220 assert 1 <= q_len_min <= q_len_max
221 assert 1 <= kv_len_min <= kv_len_max
222 assert kv_len_max >= q_len_min
223
224 g = torch.Generator(device="cpu").manual_seed(seed)
225
226 def rint(lo: int, hi: int) -> int:
227 return torch.randint(lo, hi + 1, (1,), generator=g).item()
228
229 seq_lens: list[tuple[int, int]] = []
230 for _ in range(batch_size):
231 # ensure q <= kv
232 kv = rint(max(kv_len_min, q_len_min), kv_len_max)
233 q = rint(q_len_min, min(q_len_max, kv))
234 seq_lens.append((q, kv))
235
236 return seq_lens
237
238
239 if __name__ == "__main__":
240 parser = FlexibleArgumentParser(description="Benchmark the paged attention kernel.")
241 parser.add_argument("--batch-size", type=int, default=64)
242 parser.add_argument("--q-len-min", type=int, default=512)
243 parser.add_argument("--q-len-max", type=int, default=512)
244 parser.add_argument("--kv-len-min", type=int, default=512)
245 parser.add_argument("--kv-len-max", type=int, default=512)
246 parser.add_argument("--num-blocks", type=int, default=4096)
247
248 parser.add_argument("--sliding-window", type=int, default=None)
249 parser.add_argument("--num-query-heads", type=int, default=32)
250 parser.add_argument("--num-kv-heads", type=int, default=8)
251 parser.add_argument(
252 "--head-size",
253 type=int,
254 choices=CPUAttentionBackend.get_supported_head_sizes(),
255 default=128,
256 )
257 parser.add_argument("--enable-kv-split", action="store_true")
258 parser.add_argument("--block-size", type=int, choices=[32, 64, 128], default=128)
259 parser.add_argument(
260 "--dtype", type=str, choices=["half", "bfloat16", "float"], default="bfloat16"
261 )
262 parser.add_argument("--use-sink", action="store_true")
263 parser.add_argument(
264 "--isa",
265 type=str,
266 choices=["vec", "neon", "amx", "amx_fp8", "vec16", "rvv"],
267 default=None,
268 )
269 parser.add_argument(
270 "--kv-cache-dtype",
271 type=str,
272 choices=KV_CACHE_DTYPE_CHOICES,
273 default="auto",
274 help="KV cache dtype: 'auto' uses compute dtype (bfloat16), "
275 "'fp8_e4m3'/'fp8_e5m2' enables FP8 quantized KV cache "
276 "(requires AMX-FP8 capable hardware for amx_fp8 ISA).",
277 )
278 parser.add_argument("--seed", type=int, default=0)
279 parser.add_argument("--iters", type=int, default=20)
280
281 args = parser.parse_args()
282 print(args)
283
284 seq_lens = generate_seq_lens(
285 args.batch_size,
286 args.q_len_min,
287 args.q_len_max,
288 args.kv_len_min,
289 args.kv_len_max,
290 args.seed,
291 )
292
293 print("batch (query len, kv len) = ", seq_lens)
294
295 main(
296 seq_lens=seq_lens,
297 num_heads=(args.num_query_heads, args.num_kv_heads),
298 head_size=args.head_size,
299 sliding_window=args.sliding_window,
300 dtype=STR_DTYPE_TO_TORCH_DTYPE[args.dtype],
301 block_size=args.block_size,
302 num_blocks=args.num_blocks,
303 use_sink=args.use_sink,
304 enable_kv_split=args.enable_kv_split,
305 isa=args.isa
306 if args.isa is not None
307 else get_attn_isa(
308 args.block_size,
309 STR_DTYPE_TO_TORCH_DTYPE[args.dtype],
310 args.head_size,
311 args.kv_cache_dtype,
312 ),
313 kv_cache_dtype=args.kv_cache_dtype,
314 seed=args.seed,
315 iters=args.iters,
316 )