main
py 641 lines 15.8 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 from threading import Lock
4
5 import pytest
6 import torch
7
8 import vllm.lora.ops.triton_ops as triton_ops
9 from vllm.lora.ops.triton_ops import LoRAKernelMeta
10 from vllm.lora.ops.triton_ops.utils import _LORA_A_PTR_DICT, _LORA_B_PTR_DICT
11 from vllm.platforms import current_platform
12 from vllm.utils.torch_utils import set_random_seed
13
14 from .utils import PunicaTensors, assert_close, generate_data_for_nslices
15
16 DEVICE_TYPE = current_platform.device_type
17
18 # On XPU, oneDNN/oneMKL can return wrong results for these reference matmuls
19 # after many Triton kernel launches, so the reference stays on CPU there.
20 _REF_ON_CPU = current_platform.is_xpu()
21
22
23 def _to_ref_device(tensor: torch.Tensor) -> torch.Tensor:
24 return tensor.cpu() if _REF_ON_CPU else tensor
25
26
27 @pytest.fixture(autouse=True)
28 def reset_device(reset_default_device):
29 pass
30
31
32 @pytest.fixture(autouse=True)
33 def cleanup_fixture():
34 """Override conftest's cleanup_fixture— not needed for punica tests."""
35 yield
36
37
38 @pytest.fixture(autouse=True)
39 def dynamo_reset():
40 """Override conftest's dynamo_reset — not needed for punica tests."""
41 yield
42
43
44 def _bgmv_shrink(
45 inputs, lora_weight, output, seq_len_tensor, lora_indices, scaling=1.0
46 ):
47 """Memory-efficient shrink reference: per-LoRA matmul loop.
48 output[mask] = scaling * inputs[mask] @ weight.T"""
49 exploded = torch.repeat_interleave(lora_indices, seq_len_tensor)
50 for lid in exploded.unique():
51 if lid < 0:
52 continue
53 mask = exploded == lid
54 inp = inputs[mask].to(output.dtype)
55 w = lora_weight[lid].to(output.dtype)
56 output[mask] = scaling * (inp @ w.T)
57
58
59 def _bgmv_expand(
60 inputs,
61 lora_weight,
62 output,
63 seq_len_tensor,
64 lora_indices,
65 offset=0,
66 add_inputs=False,
67 ):
68 """Memory-efficient expand reference: per-LoRA matmul loop.
69 output[mask, offset:offset+n] (+)= inputs[mask] @ weight.T"""
70 exploded = torch.repeat_interleave(lora_indices, seq_len_tensor)
71 for lid in exploded.unique():
72 if lid < 0:
73 continue
74 mask = exploded == lid
75 inp = inputs[mask].to(output.dtype)
76 w = lora_weight[lid].to(output.dtype)
77 n = w.shape[0]
78 result = inp @ w.T
79 if add_inputs:
80 output[mask, offset : offset + n] += result
81 else:
82 output[mask, offset : offset + n] = result
83
84
85 # Utility shrink and expand operations used as reference implementations.
86 def sgmv_shrink_for_nslices(
87 nslices: int,
88 inputs_tensor: torch.Tensor,
89 lora_weights_lst: list[torch.Tensor],
90 out_tensor: torch.Tensor,
91 b_seq_start_loc: torch.Tensor,
92 seq_len_tensor: torch.Tensor,
93 prompt_lora_mapping: torch.Tensor,
94 batches: int,
95 max_seq_length: int,
96 num_tokens: int,
97 scaling: float,
98 ):
99 """Reference for sgmv_shrink using per-LoRA matmul loop."""
100 inputs = _to_ref_device(inputs_tensor)
101 seq_len = _to_ref_device(seq_len_tensor)
102 mapping = _to_ref_device(prompt_lora_mapping)
103 out = _to_ref_device(out_tensor)
104 for index in range(nslices):
105 _bgmv_shrink(
106 inputs,
107 _to_ref_device(lora_weights_lst[index]),
108 out[index],
109 seq_len,
110 mapping,
111 scaling=scaling,
112 )
113 if _REF_ON_CPU:
114 out_tensor.copy_(out)
115
116
117 def sgmv_expand_for_nslices(
118 nslices: int,
119 hidden_size: int,
120 inputs_tensor: torch.Tensor,
121 lora_weights_lst: list[torch.Tensor],
122 out_tensor: torch.Tensor,
123 b_seq_start_loc: torch.Tensor,
124 seq_len_tensor: torch.Tensor,
125 prompt_lora_mapping: torch.Tensor,
126 batches: int,
127 max_seq_length: int,
128 num_tokens: int,
129 add_inputs: bool,
130 ) -> None:
131 """Reference for sgmv_expand using per-LoRA matmul loop."""
132 seq_len = _to_ref_device(seq_len_tensor)
133 mapping = _to_ref_device(prompt_lora_mapping)
134 out = _to_ref_device(out_tensor)
135 for index in range(nslices):
136 _bgmv_expand(
137 _to_ref_device(inputs_tensor[index]),
138 _to_ref_device(lora_weights_lst[index]),
139 out,
140 seq_len,
141 mapping,
142 offset=hidden_size * index,
143 add_inputs=add_inputs,
144 )
145 if _REF_ON_CPU:
146 out_tensor.copy_(out)
147
148
149 _dict_lock = Lock()
150
151
152 def check_lora_shrink_kernel(
153 batches: int,
154 num_loras: int,
155 rank: int,
156 hidden_size: int,
157 nslices: int,
158 dtype: torch.dtype,
159 device: str,
160 seq_length: int,
161 scaling: float,
162 ):
163 """Compare outputs of torch_ops.sgmv_shrink and triton_ops.lora_shrink
164 kernels.
165 """
166 data: PunicaTensors = generate_data_for_nslices(
167 batches,
168 hidden_size,
169 num_loras,
170 rank,
171 seq_length,
172 nslices,
173 dtype,
174 "shrink",
175 device,
176 )
177 max_seq_length, token_nums = data.meta()
178
179 # Setup metadata information for SGMV and reference kernels
180 sgmv_meta_args = (
181 data.b_seq_start_loc,
182 data.seq_len_tensor,
183 data.prompt_lora_mapping,
184 batches,
185 max_seq_length,
186 token_nums,
187 )
188
189 # Setup metadata information for the LoRA kernel.
190 lora_meta = LoRAKernelMeta.make(
191 max_loras=num_loras,
192 max_num_tokens=token_nums,
193 device=DEVICE_TYPE,
194 )
195 lora_meta.prepare_tensors(data.token_lora_mapping)
196
197 ref_out_tensor = data.ref_out_tensor
198 out_tensor = data.our_out_tensor.clone()
199
200 # Preventing cache error pointer.
201 with _dict_lock:
202 # lora_shrink kernel
203 _LORA_A_PTR_DICT.clear()
204 triton_ops.lora_shrink(
205 data.inputs_tensor,
206 data.lora_weights,
207 out_tensor,
208 *lora_meta.meta_args(token_nums=token_nums, specialize_active_lora=False),
209 scaling,
210 )
211
212 # Reference
213 sgmv_shrink_for_nslices(
214 nslices,
215 data.inputs_tensor,
216 data.lora_weights,
217 ref_out_tensor,
218 *sgmv_meta_args,
219 scaling,
220 )
221
222 assert_close(out_tensor, ref_out_tensor)
223
224
225 def check_lora_expand_kernel(
226 batches: int,
227 num_loras: int,
228 rank: int,
229 hidden_size: int,
230 nslices: int,
231 dtype: torch.dtype,
232 device: str,
233 seq_length: int,
234 add_inputs: bool,
235 ):
236 """Compare outputs of torch_ops.sgmv_expand and triton_ops.lora_expand
237 kernels.
238 """
239 data: PunicaTensors = generate_data_for_nslices(
240 batches,
241 hidden_size,
242 num_loras,
243 rank,
244 seq_length,
245 nslices,
246 dtype,
247 "expand",
248 device,
249 )
250
251 max_seq_length, token_nums = data.meta()
252
253 # Setup metadata information for SGMV and reference kernels
254 sgmv_meta_args = (
255 data.b_seq_start_loc,
256 data.seq_len_tensor,
257 data.prompt_lora_mapping,
258 batches,
259 max_seq_length,
260 token_nums,
261 )
262
263 # Setup metadata information for the LoRA kernel.
264 lora_meta = LoRAKernelMeta.make(
265 max_loras=num_loras,
266 max_num_tokens=token_nums,
267 device=DEVICE_TYPE,
268 )
269 lora_meta.prepare_tensors(data.token_lora_mapping)
270
271 # Setup output tensors
272 ref_out_tensor = data.ref_out_tensor
273 out_tensor = data.our_out_tensor.clone()
274
275 with _dict_lock:
276 # lora_expand kernel
277 _LORA_B_PTR_DICT.clear()
278 triton_ops.lora_expand(
279 data.inputs_tensor,
280 data.lora_weights,
281 out_tensor,
282 *lora_meta.meta_args(token_nums=token_nums, specialize_active_lora=False),
283 offset_start=0,
284 add_inputs=add_inputs,
285 )
286
287 # Reference
288 sgmv_expand_for_nslices(
289 nslices,
290 hidden_size,
291 data.inputs_tensor,
292 data.lora_weights,
293 ref_out_tensor,
294 *sgmv_meta_args,
295 add_inputs=add_inputs,
296 )
297
298 assert_close(out_tensor, ref_out_tensor)
299
300
301 # Tests
302 # We test the punica kernels along 2 verticals mainly.
303 # 1. Variations in hidden_dim size
304 # 2. Variations in all other parameters like (batch_size, max_rank, num_loras
305 # etc.)
306
307 # We have collected the hidden_sizes included in the LoRA models
308 # currently supported by vLLM. It tests whether the corresponding Triton
309 # kernel can run normally when tensor parallelism is set to
310 # [1, 2, 4, 8, 16, 32, 64].
311 HIDDEN_SIZES = [
312 128,
313 256,
314 512,
315 896,
316 1024,
317 1152,
318 1216,
319 1280,
320 1536,
321 1664,
322 2048,
323 2240,
324 2304,
325 2368,
326 2432,
327 2560,
328 2752,
329 3072,
330 3328,
331 3456,
332 3584,
333 3712,
334 4096,
335 4480,
336 4608,
337 4736,
338 4864,
339 5120,
340 5504,
341 5632,
342 5888,
343 6144,
344 6400,
345 6848,
346 6912,
347 7168,
348 7424,
349 8192,
350 8960,
351 9216,
352 9472,
353 10240,
354 11008,
355 11264,
356 13824,
357 14336,
358 14784,
359 14848,
360 15360,
361 18944,
362 22016,
363 22528,
364 24576,
365 27392,
366 27648,
367 29568,
368 29696,
369 32000,
370 32256,
371 32512,
372 32768,
373 33024,
374 36864,
375 43264,
376 49152,
377 49408,
378 60544,
379 60672,
380 64000,
381 64256,
382 102400,
383 102656,
384 128000,
385 128256,
386 ]
387 # The size of TP
388 divisibility = [1, 2, 8, 16, 64]
389
390 all_hidden_size = []
391 for div in divisibility:
392 for hidden_size in HIDDEN_SIZES:
393 all_hidden_size.append(hidden_size // div)
394
395 HIDDEN_SIZES = list(set(all_hidden_size))
396
397 # Test params that focuses on hidden_size variation.
398 hs_test_params = {
399 "hidden_sizes": HIDDEN_SIZES,
400 "batches": [4],
401 "num_loras": [4],
402 "max_ranks": [32],
403 }
404
405 # General tests params that tests for variations in all dimensions
406 # except hidden_size.
407 test_params = {
408 "hidden_sizes": [2049],
409 "batches": [1, 4, 16, 32],
410 "num_loras": [1, 8, 32, 128],
411 "max_ranks": [1, 4, 8, 16, 32, 64, 128, 256],
412 }
413
414 DTYPES = [torch.float16, torch.bfloat16]
415 DEVICES = [f"{DEVICE_TYPE}:{0}"]
416 SEED = [0]
417
418
419 @pytest.mark.parametrize("batches", test_params["batches"])
420 @pytest.mark.parametrize("num_loras", test_params["num_loras"])
421 @pytest.mark.parametrize("rank", test_params["max_ranks"])
422 @pytest.mark.parametrize("hidden_size", test_params["hidden_sizes"])
423 @pytest.mark.parametrize("nslices", [1, 2, 3])
424 @pytest.mark.parametrize("dtype", DTYPES)
425 @pytest.mark.parametrize("device", DEVICES)
426 @pytest.mark.parametrize("seed", SEED)
427 @pytest.mark.parametrize("op_type", ["shrink", "expand"])
428 def test_kernels(
429 batches: int,
430 num_loras: int,
431 rank: int,
432 hidden_size: int,
433 nslices: int,
434 dtype: torch.dtype,
435 device: str,
436 seed: int,
437 op_type: str,
438 ):
439 """Tests LoRA kernels."""
440 torch.set_default_device(device)
441 torch.accelerator.set_device_index(device)
442 set_random_seed(seed)
443
444 if op_type == "shrink":
445 check_lora_shrink_kernel(
446 batches=batches,
447 num_loras=num_loras,
448 rank=rank,
449 hidden_size=hidden_size,
450 nslices=nslices,
451 dtype=dtype,
452 device=device,
453 seq_length=128,
454 scaling=0.5,
455 )
456 else:
457 check_lora_expand_kernel(
458 batches=batches,
459 num_loras=num_loras,
460 rank=rank,
461 hidden_size=hidden_size,
462 nslices=nslices,
463 dtype=dtype,
464 device=device,
465 seq_length=128,
466 add_inputs=True,
467 )
468
469
470 @pytest.mark.parametrize("batches", hs_test_params["batches"])
471 @pytest.mark.parametrize("num_loras", hs_test_params["num_loras"])
472 @pytest.mark.parametrize("rank", hs_test_params["max_ranks"])
473 @pytest.mark.parametrize("hidden_size", hs_test_params["hidden_sizes"])
474 @pytest.mark.parametrize("nslices", [1, 2, 3])
475 @pytest.mark.parametrize("dtype", DTYPES)
476 @pytest.mark.parametrize("device", DEVICES)
477 @pytest.mark.parametrize("seed", SEED)
478 @pytest.mark.parametrize("op_type", ["shrink", "expand"])
479 def test_kernels_hidden_size(
480 batches: int,
481 num_loras: int,
482 rank: int,
483 hidden_size: int,
484 nslices: int,
485 dtype: torch.dtype,
486 device: str,
487 seed: int,
488 op_type: str,
489 ):
490 """Tests SGMV and LoRA kernels."""
491 torch.set_default_device(device)
492 torch.accelerator.set_device_index(device)
493 set_random_seed(seed)
494
495 if op_type == "shrink":
496 check_lora_shrink_kernel(
497 batches=batches,
498 num_loras=num_loras,
499 rank=rank,
500 hidden_size=hidden_size,
501 nslices=nslices,
502 dtype=dtype,
503 device=device,
504 seq_length=128,
505 scaling=0.5,
506 )
507 else:
508 check_lora_expand_kernel(
509 batches=batches,
510 num_loras=num_loras,
511 rank=rank,
512 hidden_size=hidden_size,
513 nslices=nslices,
514 dtype=dtype,
515 device=device,
516 seq_length=128,
517 add_inputs=True,
518 )
519
520
521 @pytest.mark.parametrize("device", DEVICES)
522 def test_add_lora_fused_moe_early_exit(device):
523 """Ensures add_lora_fused_moe does not invoke the LoRA kernel or
524 modify the output tensor when no_lora_flag_cpu is True
525 """
526 from types import SimpleNamespace
527
528 from vllm.lora.punica_wrapper.punica_gpu import PunicaWrapperGPU
529
530 torch.set_default_device(device)
531 torch.accelerator.set_device_index(device)
532
533 max_loras, num_tokens = 4, 16
534 num_experts, top_k, max_lora_rank = 8, 2, 16
535 K, N = 256, 128
536
537 # build PunicaWrapperGPU with minimal lora_config mock
538 lora_config = SimpleNamespace(
539 max_loras=max_loras,
540 specialize_active_lora=False,
541 )
542 wrapper = PunicaWrapperGPU(
543 max_num_batched_tokens=num_tokens,
544 max_batches=num_tokens,
545 device=device,
546 lora_config=lora_config,
547 )
548
549 # simulate a prior LoRA batch so the internal mapping is
550 # populated with stale LoRA IDs
551 lora_mapping = torch.zeros(
552 num_tokens,
553 dtype=torch.int32,
554 device=device,
555 )
556 lora_mapping[:8] = 1
557 lora_mapping[8:] = 2
558 wrapper.token_mapping_meta.prepare_tensors(lora_mapping)
559
560 # simulate a base-model batch (all -1)
561 base_mapping = torch.full(
562 (num_tokens,),
563 -1,
564 dtype=torch.int32,
565 device=device,
566 )
567 wrapper.token_mapping_meta.prepare_tensors(base_mapping)
568
569 assert wrapper.token_mapping_meta.no_lora_flag_cpu[0].item() is True
570
571 # dummy tensors for add_lora_fused_moe
572 y = torch.rand(num_tokens, top_k, N, dtype=torch.bfloat16, device=device)
573 y_snapshot = y.clone()
574 x = torch.rand(num_tokens, K, dtype=torch.bfloat16, device=device)
575
576 lora_a_stacked = (
577 torch.rand(
578 max_loras,
579 num_experts,
580 max_lora_rank,
581 K,
582 dtype=torch.bfloat16,
583 device=device,
584 ),
585 )
586 lora_b_stacked = (
587 torch.rand(
588 max_loras,
589 num_experts,
590 N,
591 max_lora_rank,
592 dtype=torch.bfloat16,
593 device=device,
594 ),
595 )
596 topk_weights = torch.ones(
597 num_tokens,
598 top_k,
599 dtype=torch.float32,
600 device=device,
601 )
602 adapter_enabled = torch.ones(
603 max_loras + 1,
604 dtype=torch.int32,
605 device=device,
606 )
607 shrink_config = expand_config = {
608 "BLOCK_SIZE_M": 16,
609 "BLOCK_SIZE_N": 32,
610 "BLOCK_SIZE_K": 64,
611 "GROUP_SIZE_M": 1,
612 "NUM_WARPS": 4,
613 "NUM_STAGES": 3,
614 "SPLIT_K": 1,
615 }
616
617 # call add_lora_fused_moe - the early exit should prevent any
618 # modification to the output
619 wrapper.add_lora_fused_moe(
620 y=y,
621 x=x,
622 lora_a_stacked=lora_a_stacked,
623 lora_b_stacked=lora_b_stacked,
624 topk_weights=topk_weights,
625 sorted_token_ids=None,
626 expert_ids=torch.zeros(
627 num_tokens * top_k,
628 dtype=torch.int32,
629 device=device,
630 ),
631 num_tokens_post_padded=None,
632 max_lora_rank=max_lora_rank,
633 top_k_num=top_k,
634 shrink_config=shrink_config,
635 expand_config=expand_config,
636 adapter_enabled=adapter_enabled,
637 )
638
639 assert torch.equal(y, y_snapshot), (
640 "add_lora_fused_moe modified output tensor despite no_lora_flag_cpu=True"
641 )