main
py 1,465 lines 50.9 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 import argparse
5 import copy
6 import json
7 import pickle
8 import time
9 from collections.abc import Callable
10 from dataclasses import dataclass
11 from enum import Enum, auto
12 from itertools import product
13 from pathlib import Path
14 from typing import Any
15
16 import torch
17 import torch.utils.benchmark as TBenchmark
18 from torch.utils.benchmark import Measurement as TMeasurement
19 from utils import ArgPool, Bench, CudaGraphBenchParams
20 from weight_shapes import WEIGHT_SHAPES
21
22 from vllm.lora.ops.triton_ops.utils import get_lora_op_configs
23 from vllm.triton_utils import HAS_TRITON, triton
24
25 if HAS_TRITON:
26 from vllm.lora.ops.triton_ops import ( ## added fused_moe_lora
27 LoRAKernelMeta,
28 fused_moe_lora_expand,
29 fused_moe_lora_shrink,
30 lora_expand,
31 lora_shrink,
32 )
33 from vllm.lora.ops.triton_ops.fused_moe_lora_op import (
34 _LORA_PTR_DICT, ## added _LORA_PTR_DICT for fused_moe_lora
35 )
36 from vllm.lora.ops.triton_ops.utils import _LORA_A_PTR_DICT, _LORA_B_PTR_DICT
37 from vllm import _custom_ops as ops
38 from vllm.utils.argparse_utils import FlexibleArgumentParser
39 from vllm.utils.math_utils import round_up
40
41 DEFAULT_MODELS = list(WEIGHT_SHAPES.keys())
42 DEFAULT_TP_SIZES = [1]
43 DEFAULT_BATCH_SIZES = [
44 1,
45 16,
46 32,
47 64,
48 128,
49 192,
50 256,
51 320,
52 384,
53 448,
54 512,
55 640,
56 768,
57 896,
58 1024,
59 2048,
60 3072,
61 4096,
62 5120,
63 6144,
64 7168,
65 8192,
66 ]
67 DEFAULT_HIDDEN_SIZES = [1024, 2048, 4096, 8192, 16384]
68 DEFAULT_LORA_RANKS = [16]
69 DEFAULT_NUM_LORAS = [1, 2, 3, 4]
70 DEFAULT_SORT_BY_LORA_IDS = [False, True]
71 DEFAULT_SEQ_LENGTHS = [1]
72 DEFAULT_EXPAND_FN_ADD_INPUTS = [True, False]
73 DEFAULT_TOP_K_NUMS = [1] # Added for MoE LoRA top_k
74 DEFAULT_NUM_EXPERTS = [8] # Added for MoE LoRA num_experts
75
76
77 # Utilities
78 def dtype_to_str(dtype: torch.dtype):
79 if dtype == torch.float16:
80 return "f16"
81 if dtype == torch.bfloat16:
82 return "bf16"
83 if dtype == torch.float32:
84 return "f32"
85 raise ValueError(f"Unsupported dtype {dtype}")
86
87
88 def make_rand_lora_weight_tensor(
89 k: int, n: int, num_loras: int, dtype: torch.dtype, device: str = "cuda"
90 ) -> torch.Tensor:
91 # LoRA weights column major
92 return torch.rand((num_loras, n, k), dtype=dtype).to(device)
93
94
95 def make_rand_tensors(
96 a_shape: tuple[int, ...],
97 b_shape: tuple[int, ...],
98 c_shape: tuple[int, ...],
99 a_dtype: torch.dtype,
100 b_dtype: torch.dtype,
101 c_dtype: torch.dtype,
102 num_slices: int,
103 device: str = "cuda",
104 ) -> tuple[torch.Tensor, list[torch.Tensor], torch.Tensor]:
105 """Make LoRA input/output matrices."""
106 A = torch.rand(a_shape, dtype=a_dtype).to(device)
107
108 # LoRA weights column major
109 Bs = [torch.rand(b_shape, dtype=b_dtype).to(device) for _ in range(num_slices)]
110
111 C = torch.zeros(c_shape, dtype=c_dtype).to(device)
112 return A, Bs, C
113
114
115 def make_prompt_lora_mapping(
116 num_prompts: int, num_active_loras: int, sort_by_lora_id: bool, device: str
117 ) -> torch.Tensor:
118 """All prompts are mapped to a LoRA ID in range [0, num_active_loras).
119 where 0 refers to first lora, 1 refers to second lora and so on.
120 """
121 assert num_active_loras > 0
122
123 if not sort_by_lora_id:
124 return torch.randint(0, num_active_loras, (num_prompts,), dtype=torch.long)
125
126 # Divide LoRAs equally and in order.
127 part_size = num_prompts // num_active_loras
128 part_size = max(part_size, 1)
129
130 lora_id = 0
131 prompt_lora_mapping = []
132 while len(prompt_lora_mapping) < num_prompts:
133 prompt_lora_mapping.extend([lora_id] * part_size)
134 lora_id = lora_id + 1 if lora_id + 1 < num_active_loras else lora_id
135 return torch.tensor(
136 prompt_lora_mapping[:num_prompts], dtype=torch.long, device=device
137 )
138
139
140 def make_token_lora_mapping(
141 num_tokens: int,
142 num_prompts: int,
143 prompt_lora_mapping: torch.Tensor,
144 seq_len_tensor: torch.Tensor,
145 device: str,
146 ):
147 """Make token_lora_mapping from prompt_lora_mapping and seq_lens_tensor."""
148 assert prompt_lora_mapping.shape[0] == num_prompts
149
150 # token to lora index mapping
151 token_lora_mapping = [0] * num_tokens
152 current_offset = 0
153 for b_id in range(num_prompts):
154 lora_index = prompt_lora_mapping[b_id].item()
155 s = current_offset
156 e = s + seq_len_tensor[b_id].item()
157 token_lora_mapping[s:e] = [lora_index] * (e - s)
158 current_offset += seq_len_tensor[b_id].item()
159
160 return torch.tensor(token_lora_mapping, dtype=torch.long, device=device)
161
162
163 def ref_group_gemm(
164 ref_out: torch.Tensor,
165 input: torch.Tensor,
166 lora_weights: list[torch.Tensor],
167 seq_lens_cpu: torch.Tensor,
168 prompt_lora_mapping_cpu: torch.Tensor,
169 scaling: float,
170 add_inputs: bool | None,
171 ):
172 """Torch group gemm reference implementation to test correctness of
173 benchmarking operations.
174 """
175 batches = seq_lens_cpu.size(0)
176 out_list = []
177 current_offset = 0
178 for lora_index, b_length in zip(range(batches), seq_lens_cpu):
179 x = input[current_offset : b_length + current_offset, :]
180 current_offset += b_length
181 w = lora_weights[prompt_lora_mapping_cpu[lora_index]]
182 result = torch.nn.functional.linear(x, w)
183 result *= scaling
184 out_list.append(result)
185
186 cat_result = torch.cat(out_list, dim=0)
187
188 if add_inputs:
189 ref_out += cat_result
190 else:
191 ref_out.copy_(cat_result)
192
193
194 class OpType(Enum):
195 """LoRA Ops to benchmark and its properties."""
196
197 LORA_SHRINK = auto()
198 LORA_EXPAND = auto()
199 ## Adding support for fused moe lora
200 FUSED_MOE_LORA_GATE_UP_SHRINK = auto() ## Gate/Up projection variant with shrink
201 FUSED_MOE_LORA_GATE_UP_EXPAND = auto() ## Gate/Up projection variant with expand
202 FUSED_MOE_LORA_DOWN_SHRINK = auto() ## Down projection variant with shrink
203 FUSED_MOE_LORA_DOWN_EXPAND = auto() ## Down projection variant with expand
204
205 @staticmethod
206 def from_str(s: str) -> "OpType":
207 if s.lower() == "lora_shrink":
208 return OpType.LORA_SHRINK
209 if s.lower() == "lora_expand":
210 return OpType.LORA_EXPAND
211 # Adding support for fused moe lora, both in gate_up and down
212 if s.lower() == "fused_moe_lora_gate_up_shrink": ## Gate/Up variant with shrink
213 return OpType.FUSED_MOE_LORA_GATE_UP_SHRINK
214 if s.lower() == "fused_moe_lora_gate_up_expand": ## Gate/Up variant with expand
215 return OpType.FUSED_MOE_LORA_GATE_UP_EXPAND
216 if s.lower() == "fused_moe_lora_down_shrink": ## Down variant with shrink
217 return OpType.FUSED_MOE_LORA_DOWN_SHRINK
218 if s.lower() == "fused_moe_lora_down_expand": ## Down variant with expand
219 return OpType.FUSED_MOE_LORA_DOWN_EXPAND
220 raise ValueError(f"Unrecognized str {s} to convert to OpType")
221
222 def is_shrink_fn(self) -> bool:
223 return self in [OpType.LORA_SHRINK]
224
225 def is_expand_fn(self) -> bool:
226 return self in [OpType.LORA_EXPAND]
227
228 def is_fused_moe_lora_fn(self) -> bool: ## adding for fused MoE LoRA
229 return self in [
230 OpType.FUSED_MOE_LORA_GATE_UP_SHRINK,
231 OpType.FUSED_MOE_LORA_DOWN_SHRINK,
232 OpType.FUSED_MOE_LORA_GATE_UP_EXPAND,
233 OpType.FUSED_MOE_LORA_DOWN_EXPAND,
234 ]
235
236 def is_fused_moe_lora_gate_up_fn(
237 self,
238 ) -> bool: ## adding for fused MoE LoRA Gate/Up
239 return self in [
240 OpType.FUSED_MOE_LORA_GATE_UP_SHRINK,
241 OpType.FUSED_MOE_LORA_GATE_UP_EXPAND,
242 ]
243
244 def is_fused_moe_lora_down_fn(self) -> bool: ## adding for fused MoE LoRA Down
245 return self in [
246 OpType.FUSED_MOE_LORA_DOWN_SHRINK,
247 OpType.FUSED_MOE_LORA_DOWN_EXPAND,
248 ]
249
250 def is_fused_moe_lora_shrink_fn(self) -> bool:
251 return self in [
252 OpType.FUSED_MOE_LORA_GATE_UP_SHRINK,
253 OpType.FUSED_MOE_LORA_DOWN_SHRINK,
254 ]
255
256 def is_fused_moe_lora_expand_fn(self) -> bool:
257 return self in [
258 OpType.FUSED_MOE_LORA_GATE_UP_EXPAND,
259 OpType.FUSED_MOE_LORA_DOWN_EXPAND,
260 ]
261
262 def num_slices(self) -> list[int]:
263 if self.is_fused_moe_lora_gate_up_fn():
264 return [2]
265 elif self.is_fused_moe_lora_down_fn():
266 return [1]
267 return [1, 2, 3]
268
269 def mkn(
270 self, batch_size: int, seq_length: int, hidden_size: int, lora_rank: int
271 ) -> tuple[int, int, int]:
272 num_tokens = batch_size * seq_length
273 if self.is_shrink_fn() or self.is_fused_moe_lora_fn():
274 m = num_tokens
275 k = hidden_size
276 n = lora_rank
277 elif self.is_expand_fn():
278 m = num_tokens
279 k = lora_rank
280 n = hidden_size
281 return m, k, n
282
283 def matmul_dtypes(
284 self, op_dtype: torch.dtype
285 ) -> tuple[torch.dtype, torch.dtype, torch.dtype]:
286 """Return a type, b type and c type for A x B = C."""
287 if self.is_shrink_fn():
288 return op_dtype, op_dtype, torch.float32
289 elif self.is_expand_fn():
290 return torch.float32, op_dtype, op_dtype
291 else:
292 assert self.is_fused_moe_lora_fn()
293 return op_dtype, op_dtype, op_dtype
294
295 def matmul_shapes_fused_moe_lora(
296 self,
297 m: int,
298 n: int,
299 k: int,
300 num_loras: int,
301 num_slices: int,
302 top_k_num: int,
303 num_experts: int,
304 ) -> tuple[tuple[int], tuple[int], tuple[int], tuple[int]]:
305 if self.is_fused_moe_lora_shrink_fn():
306 input_shape = (
307 (m * top_k_num, n)
308 if self in [OpType.FUSED_MOE_LORA_DOWN_SHRINK]
309 else (m, n)
310 )
311 output_shape = (num_slices, m, top_k_num, k)
312 weight_shape = (num_loras, num_experts, k, n)
313 else:
314 assert self.is_fused_moe_lora_expand_fn()
315 input_shape = (num_slices, m, top_k_num, k)
316 output_shape = (m, top_k_num, n * num_slices)
317 weight_shape = (num_loras, num_experts, n, k)
318 return (input_shape, weight_shape, output_shape)
319
320 def matmul_shapes(
321 self,
322 batch_size: int,
323 seq_length: int,
324 hidden_size: int,
325 lora_rank: int,
326 num_loras: int,
327 num_slices: int,
328 top_k_num: int | None = None,
329 num_experts: int | None = None,
330 ) -> tuple[tuple[int, ...], tuple[int, ...], tuple[int, ...]]:
331 """Given num_slices, return the shapes of the A, B, and C matrices
332 in A x B = C, for the op_type
333 """
334 m, k, n = self.mkn(batch_size, seq_length, hidden_size, lora_rank)
335
336 b_shape = (num_loras, n, k) # col-major
337 if self in [OpType.LORA_SHRINK]:
338 # LoRA shrink kernels support num_slices inherently in the kernel.
339 return ((m, k), b_shape, (num_slices, m, n))
340 if self in [OpType.LORA_EXPAND]:
341 # LoRA expand kernels support num_slices inherently in the kernel
342 return ((num_slices, m, k), b_shape, (m, n * num_slices))
343 if self.is_fused_moe_lora_fn():
344 return self.matmul_shapes_fused_moe_lora(
345 m,
346 k,
347 n,
348 num_loras,
349 num_slices,
350 top_k_num,
351 num_experts,
352 )
353 raise ValueError(f"Unrecognized op_type {self}")
354
355 def bench_fn(self) -> Callable:
356 if self == OpType.LORA_SHRINK:
357 return lora_shrink
358 if self == OpType.LORA_EXPAND:
359 return lora_expand
360 if self in [
361 OpType.FUSED_MOE_LORA_GATE_UP_SHRINK,
362 OpType.FUSED_MOE_LORA_DOWN_SHRINK,
363 ]:
364 return fused_moe_lora_shrink
365 if self in [
366 OpType.FUSED_MOE_LORA_GATE_UP_EXPAND,
367 OpType.FUSED_MOE_LORA_DOWN_EXPAND,
368 ]:
369 return fused_moe_lora_expand
370
371 raise ValueError(f"Unrecognized optype {self}")
372
373 def run_ref_group_gemm(
374 self,
375 output: torch.Tensor,
376 input: torch.Tensor,
377 lora_weights: list[torch.Tensor],
378 **kwargs,
379 ) -> Callable:
380 """Each benchmark operation expects the input, lora_weights and outputs
381 in a slightly different format. Refer to self.matmul_shapes().
382 run_ref_group_gemm accounts for those differences in executing a
383 reference group gemm for correctness testing.
384 """
385 w_dtype = lora_weights[0].dtype
386 num_slices = len(lora_weights)
387 if self in [OpType.LORA_SHRINK]:
388 for slice_idx in range(num_slices):
389 ref_group_gemm(
390 ref_out=output[slice_idx, :],
391 input=input,
392 lora_weights=lora_weights[slice_idx],
393 **kwargs,
394 )
395 elif self in [OpType.LORA_EXPAND]:
396 hidden_size = lora_weights[0].shape[1]
397 for slice_idx in range(num_slices):
398 slice_offset = slice_idx * hidden_size
399 ref_group_gemm(
400 ref_out=output[:, slice_offset : slice_offset + hidden_size],
401 input=input[slice_idx].clone().to(dtype=w_dtype),
402 lora_weights=lora_weights[slice_idx],
403 **kwargs,
404 )
405 else:
406 raise ValueError(f"Unrecognized optype {self}")
407
408
409 @dataclass
410 class BenchmarkContext:
411 """LoRA benchmark context."""
412
413 batch_size: int
414 hidden_size: int
415 num_loras: int
416 num_active_loras: int
417 lora_rank: int
418 sort_by_lora_id: bool
419 dtype: torch.dtype
420 seq_length: int | None = None
421 num_experts: int | None = None # num_experts for MoE based ops
422 top_k_num: int | None = None # top_k for MoE based ops
423 num_slices: int | None = None # num_slices for slice based ops
424
425 def with_seq_length(self, seq_length: int) -> "BenchmarkContext":
426 ctx = copy.copy(self)
427 ctx.seq_length = seq_length
428 return ctx
429
430 def with_num_slices(self, num_slices: int) -> "BenchmarkContext":
431 ctx = copy.copy(self)
432 ctx.num_slices = num_slices
433 return ctx
434
435 def bench_label(self) -> str:
436 return f"lora-{self.dtype}"
437
438 def bench_sublabel(self, op_type: OpType) -> str:
439 m, k, n = op_type.mkn(
440 self.batch_size, self.seq_length, self.hidden_size, self.lora_rank
441 )
442 desc = {
443 "bs": self.batch_size,
444 "sl": self.seq_length,
445 "m": m,
446 "k": k,
447 "n": n,
448 "num_loras": self.num_loras,
449 "sort_by_lora": self.sort_by_lora_id,
450 "num_slices": self.num_slices,
451 }
452 return json.dumps(desc)
453
454
455 @dataclass
456 class BenchmarkTensors:
457 """Input/Output tensors used for benchmarks."""
458
459 # matmul tensors
460 input: torch.Tensor
461 lora_weights_lst: list[torch.Tensor]
462 output: torch.Tensor
463 # LoRA kernel metadata
464 lora_kernel_meta: LoRAKernelMeta
465 # Metadata tensors used in testing correctness
466 seq_lens: torch.Tensor
467 prompt_lora_mapping: torch.Tensor
468
469 def io_types(self) -> str:
470 return (
471 f"{dtype_to_str(self.input.dtype)}x"
472 f"{dtype_to_str(self.lora_weights_lst[0].dtype)}=>"
473 f"{dtype_to_str(self.output.dtype)}"
474 )
475
476 def get_num_tokens(self, size: int, top_k_num: int, op_type: OpType):
477 return (
478 size * top_k_num if op_type in [OpType.FUSED_MOE_LORA_DOWN_SHRINK] else size
479 )
480
481 @staticmethod
482 def make(
483 ctx: BenchmarkContext, op_type: OpType, device: str = "cuda"
484 ) -> "BenchmarkTensors":
485 # Make input / output matmul tensors.
486 a_shape, b_shape, c_shape = op_type.matmul_shapes(
487 ctx.batch_size,
488 ctx.seq_length,
489 ctx.hidden_size,
490 ctx.lora_rank,
491 ctx.num_loras,
492 ctx.num_slices,
493 ctx.top_k_num,
494 ctx.num_experts,
495 )
496 a_type, b_type, c_type = op_type.matmul_dtypes(ctx.dtype)
497 input_tensor, lora_weights, output_tensor = make_rand_tensors(
498 a_shape, b_shape, c_shape, a_type, b_type, c_type, num_slices=ctx.num_slices
499 )
500
501 # Make metadata tensors.
502 # Keep the metadata tensors in the CPU for further processing if needed.
503 # The tensors get moved to the GPU before benchmarking.
504 assert ctx.num_active_loras <= ctx.num_loras
505 total_tokens = ctx.batch_size * ctx.seq_length
506
507 # Make metadata tensors involved in correctness testing.
508 # Prepare seq lens tensor
509 seq_len_tensor = torch.randint(
510 ctx.seq_length, ctx.seq_length + 1, (ctx.batch_size,)
511 )
512 assert total_tokens == seq_len_tensor.sum()
513 # Prepare prompt lora indices tensor
514 prompt_lora_indices_tensor = make_prompt_lora_mapping(
515 ctx.batch_size, ctx.num_active_loras, ctx.sort_by_lora_id, "cpu"
516 )
517
518 # Make LoRAKernelMeta
519 token_lora_indices_tensor = make_token_lora_mapping(
520 total_tokens,
521 ctx.batch_size,
522 prompt_lora_indices_tensor,
523 seq_len_tensor,
524 "cpu",
525 )
526 lora_kernel_meta = LoRAKernelMeta.make(
527 max_loras=ctx.num_loras,
528 max_num_tokens=token_lora_indices_tensor.size(0),
529 device="cpu",
530 )
531 lora_kernel_meta.prepare_tensors(token_lora_mapping=token_lora_indices_tensor)
532
533 return BenchmarkTensors(
534 input_tensor,
535 lora_weights,
536 output_tensor,
537 lora_kernel_meta,
538 seq_len_tensor,
539 prompt_lora_indices_tensor,
540 )
541
542 def sanity_check(self, ctx: BenchmarkContext, op_type: OpType) -> None:
543 """Fails asserts when non-conformality is detected."""
544 num_tokens = (
545 self.input.shape[1]
546 if op_type.is_fused_moe_lora_expand_fn()
547 else self.input.shape[-2]
548 )
549 # check metadata tensors
550 ## In down shrink case, each token is repeated top_k_num times
551 assert num_tokens == self.get_num_tokens(
552 torch.sum(self.seq_lens), ctx.top_k_num, op_type
553 ), f"Expected {num_tokens} tokens, but got {torch.sum(self.seq_lens)}"
554 num_seqs = self.seq_lens.shape[0]
555 # assert self.seq_start_loc.shape[0] == num_seqs
556 ## In down shrink case, each prompt corresponds to top_k_num sequences
557 assert self.prompt_lora_mapping.shape[0] == num_seqs
558 assert self.get_num_tokens(
559 self.lora_kernel_meta.token_lora_mapping.shape[0], ctx.top_k_num, op_type
560 )
561
562 def to_device(self, device: str):
563 """Transfer tensors to device if the tensors aren't already on the device."""
564
565 def to_device(tensor: torch.Tensor):
566 if tensor.device != device:
567 tensor = tensor.to(device=device)
568 return tensor
569
570 self.input = to_device(self.input)
571 self.output = to_device(self.output)
572 self.seq_lens = to_device(self.seq_lens)
573 self.prompt_lora_mapping = to_device(self.prompt_lora_mapping)
574 for i in range(len(self.lora_weights_lst)):
575 self.lora_weights_lst[i] = to_device(self.lora_weights_lst[i])
576
577 # LoRA meta
578 for field_name in LoRAKernelMeta.__dataclass_fields__:
579 field = getattr(self.lora_kernel_meta, field_name)
580 assert isinstance(field, torch.Tensor)
581 setattr(
582 self.lora_kernel_meta,
583 field_name,
584 to_device(field) if field_name != "no_lora_flag_cpu" else field,
585 )
586
587 def metadata(self, ctx: BenchmarkContext, op_type: OpType) -> tuple[int, int, int]:
588 """Return num_seqs, num_tokens and max_seq_len."""
589 num_seqs = self.seq_lens.shape[0]
590 num_tokens = self.get_num_tokens(
591 self.lora_kernel_meta.token_lora_mapping.shape[0], ctx.top_k_num, op_type
592 )
593 max_seq_len = torch.max(self.seq_lens).item()
594 num_slices = len(self.lora_weights_lst)
595 return num_seqs, num_tokens, max_seq_len, num_slices
596
597 def fused_moe_lora_data_prepare(
598 self,
599 block_size: int,
600 token_lora_mapping: torch.Tensor,
601 ctx: BenchmarkContext,
602 ):
603 def moe_lora_align_block_size(
604 topk_ids: torch.Tensor,
605 token_lora_mapping: torch.Tensor,
606 block_size: int,
607 num_experts: int,
608 max_loras: int,
609 expert_map: torch.Tensor | None = None,
610 pad_sorted_ids: bool = False,
611 ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
612 """Aligns tokens and experts into block-sized chunks for LoRA-based
613 mixture-of-experts (MoE) execution.
614 """
615 max_num_tokens_padded = topk_ids.numel() + num_experts * (block_size - 1)
616 if pad_sorted_ids:
617 max_num_tokens_padded = round_up(max_num_tokens_padded, block_size)
618 sorted_ids = torch.empty(
619 (max_loras * max_num_tokens_padded,),
620 dtype=torch.int32,
621 device=topk_ids.device,
622 )
623 max_num_m_blocks = triton.cdiv(max_num_tokens_padded, block_size)
624 # Expert ids must be set default to -1 to prevent a blank block
625 expert_ids = torch.empty(
626 (max_loras * max_num_m_blocks,),
627 dtype=torch.int32,
628 device=topk_ids.device,
629 )
630 num_tokens_post_pad = torch.empty(
631 (max_loras), dtype=torch.int32, device=topk_ids.device
632 )
633
634 ops.moe_lora_align_block_size(
635 topk_ids,
636 token_lora_mapping,
637 num_experts,
638 block_size,
639 max_loras,
640 max_num_tokens_padded,
641 max_num_m_blocks,
642 sorted_ids,
643 expert_ids,
644 num_tokens_post_pad,
645 )
646 if expert_map is not None:
647 expert_ids = expert_map[expert_ids]
648
649 return sorted_ids, expert_ids, num_tokens_post_pad
650
651 num_tokens = ctx.batch_size
652 curr_topk_ids = torch.randint(
653 0,
654 ctx.num_experts,
655 (num_tokens, ctx.top_k_num),
656 device="cuda",
657 dtype=torch.int32,
658 )
659 topk_weights = torch.randint(
660 0,
661 ctx.num_experts,
662 (num_tokens, ctx.top_k_num),
663 device="cuda",
664 dtype=torch.int32,
665 )
666
667 (sorted_token_ids_lora, expert_ids_lora, num_tokens_post_padded_lora) = (
668 moe_lora_align_block_size(
669 topk_ids=curr_topk_ids,
670 token_lora_mapping=token_lora_mapping,
671 block_size=block_size,
672 num_experts=ctx.num_experts,
673 max_loras=ctx.num_loras,
674 )
675 )
676
677 sorted_token_ids = sorted_token_ids_lora.view(ctx.num_loras, -1)
678 expert_ids = expert_ids_lora.view(ctx.num_loras, -1)
679 num_tokens_post_padded = num_tokens_post_padded_lora
680 return (topk_weights, sorted_token_ids, expert_ids, num_tokens_post_padded)
681
682 def as_lora_shrink_kwargs(
683 self, ctx: BenchmarkContext, op_type: OpType
684 ) -> dict[str, Any]:
685 self.sanity_check(ctx, op_type)
686 self.to_device(self.input.device)
687
688 _, num_tokens, _, num_slices = self.metadata(ctx, op_type)
689
690 # Sanity check matrix shapes.
691 i_shape, lw_shape, o_shape = (
692 self.input.shape,
693 self.lora_weights_lst[0].shape,
694 self.output.shape,
695 )
696 # Expected input shape [num_tokens, hidden_size]
697 assert len(i_shape) == 2
698 assert i_shape[0] == num_tokens
699 hidden_size = i_shape[1]
700 # Expected lora weight shape [num_loras, lora_rank, hidden_size]
701 assert len(lw_shape) == 3
702 assert lw_shape[2] == hidden_size
703 lora_rank = lw_shape[1]
704 # Expected output shape [num_slices, num_tokens, lora_rank]
705 assert len(o_shape) == 3
706 assert o_shape == (num_slices, num_tokens, lora_rank)
707
708 return {
709 "inputs": self.input,
710 "lora_a_weights": self.lora_weights_lst,
711 "output_tensor": self.output,
712 "token_lora_mapping": self.lora_kernel_meta.token_lora_mapping,
713 "token_indices_sorted_by_lora_ids": (
714 self.lora_kernel_meta.token_indices_sorted_by_lora_ids
715 ),
716 "num_tokens_per_lora": self.lora_kernel_meta.num_tokens_per_lora,
717 "lora_token_start_loc": self.lora_kernel_meta.lora_token_start_loc,
718 "lora_ids": self.lora_kernel_meta.active_lora_ids,
719 "scaling": 1.0,
720 "no_lora_flag_cpu": self.lora_kernel_meta.no_lora_flag_cpu,
721 }
722
723 def as_lora_expand_kwargs(
724 self, ctx: BenchmarkContext, op_type: OpType, add_inputs: bool
725 ) -> dict[str, Any]:
726 self.sanity_check(ctx, op_type)
727 self.to_device(self.input.device)
728
729 _, num_tokens, _, num_slices = self.metadata(ctx, op_type)
730
731 # Sanity check matrix shapes.
732 i_shape, lw_shape, o_shape = (
733 self.input.shape,
734 self.lora_weights_lst[0].shape,
735 self.output.shape,
736 )
737 # Expected input shape : [num_slices, num_tokens, lora_rank]
738 assert len(i_shape) == 3
739 assert i_shape[0] == num_slices
740 assert i_shape[1] == num_tokens
741 lora_rank = i_shape[2]
742 # Expected lora weight shape : [num_lora, hidden_size, lora_rank]
743 assert len(lw_shape) == 3
744 assert lw_shape[2] == lora_rank
745 hidden_size = lw_shape[1]
746 # Expected output shape : [num_tokens, hidden_size * num_slices]
747 assert len(o_shape) == 2
748 assert o_shape == (num_tokens, hidden_size * num_slices)
749
750 return {
751 "inputs": self.input,
752 "lora_b_weights": self.lora_weights_lst,
753 "output_tensor": self.output,
754 "token_lora_mapping": self.lora_kernel_meta.token_lora_mapping,
755 "token_indices_sorted_by_lora_ids": (
756 self.lora_kernel_meta.token_indices_sorted_by_lora_ids
757 ),
758 "num_tokens_per_lora": self.lora_kernel_meta.num_tokens_per_lora,
759 "lora_token_start_loc": self.lora_kernel_meta.lora_token_start_loc,
760 "lora_ids": self.lora_kernel_meta.active_lora_ids,
761 "offset_start": 0,
762 "add_inputs": add_inputs,
763 "no_lora_flag_cpu": self.lora_kernel_meta.no_lora_flag_cpu,
764 }
765
766 def as_fused_moe_lora_shrink_kwargs(
767 self, ctx: BenchmarkContext, op_type: OpType
768 ) -> dict[str, Any]:
769 self.sanity_check(ctx, op_type)
770 self.to_device(self.input.device)
771
772 _, num_tokens, _, num_slices = self.metadata(ctx, op_type)
773
774 # Sanity check matrix shapes.
775 i_shape, lw_shape, o_shape = (
776 self.input.shape,
777 self.lora_weights_lst[0].shape,
778 self.output.shape,
779 )
780 # Expected input shape : [num_tokens, hidden_size] for gate_up
781 # Expected input shape : [top_k_num * num_tokens, hidden_size] for down
782 assert len(i_shape) == 2
783 assert i_shape[0] == num_tokens
784 hidden_size = i_shape[1]
785 # Expected lora weight shape [max_lora, num_experts, lora_rank, hidden_size]
786 assert len(lw_shape) == 4
787 assert lw_shape[-1] == hidden_size
788 lora_rank = lw_shape[-2]
789 # Expected output shape : [num_slices, num_tokens, top_k_num, lora_rank]
790 assert len(o_shape) == 4
791 assert (
792 o_shape
793 == (num_slices, num_tokens // ctx.top_k_num, ctx.top_k_num, lora_rank)
794 if op_type in [OpType.FUSED_MOE_LORA_DOWN_SHRINK]
795 else o_shape == (num_slices, num_tokens, ctx.top_k_num, lora_rank)
796 )
797 kernel_config = get_lora_op_configs(
798 op_type.name.lower(),
799 max_loras=lw_shape[0],
800 batch=num_tokens,
801 hidden_size=hidden_size,
802 rank=lora_rank,
803 num_slices=num_slices,
804 add_inputs=False,
805 )
806
807 (topk_weights, sorted_token_ids, expert_ids, num_tokens_post_padded) = (
808 self.fused_moe_lora_data_prepare(
809 block_size=kernel_config["BLOCK_SIZE_M"],
810 token_lora_mapping=self.lora_kernel_meta.token_lora_mapping,
811 ctx=ctx,
812 )
813 )
814
815 return {
816 "qcurr_hidden_states": self.input,
817 "lora_a_stacked": self.lora_weights_lst,
818 "a_intermediate_cache1": self.output,
819 "topk_weights": topk_weights,
820 "sorted_token_ids": sorted_token_ids,
821 "expert_ids": expert_ids,
822 "num_tokens_post_padded": num_tokens_post_padded,
823 "token_lora_mapping": self.lora_kernel_meta.token_lora_mapping,
824 "top_k_num": ctx.top_k_num,
825 "device": self.input.device,
826 "N": lora_rank,
827 "M": topk_weights.shape[0],
828 "EM": sorted_token_ids.shape[1],
829 "K": self.input.shape[1],
830 "num_tokens": num_tokens,
831 "num_experts": ctx.num_experts,
832 "num_slices": num_slices,
833 "shrink_block_size_m": kernel_config["BLOCK_SIZE_M"],
834 "shrink_block_size_n": kernel_config["BLOCK_SIZE_N"],
835 "shrink_block_size_k": kernel_config["BLOCK_SIZE_K"],
836 "shrink_group_size_m": kernel_config["GROUP_SIZE_M"],
837 "shrink_num_warps": kernel_config["NUM_WARPS"],
838 "shrink_num_stages": kernel_config["NUM_STAGES"],
839 "shrink_split_k": kernel_config.get("SPLIT_K", 1),
840 "mul_routed_weight": op_type.is_fused_moe_lora_down_fn(),
841 }
842
843 def as_fused_moe_lora_expand_kwargs(
844 self, ctx: BenchmarkContext, op_type: OpType
845 ) -> dict[str, Any]:
846 self.sanity_check(ctx, op_type)
847 self.to_device(self.input.device)
848
849 _, num_tokens, _, num_slices = self.metadata(ctx, op_type)
850
851 # Sanity check matrix shapes.
852 i_shape, lw_shape, o_shape = (
853 self.input.shape,
854 self.lora_weights_lst[0].shape,
855 self.output.shape,
856 )
857
858 # Expected input shape : [num_slices, num_tokens, top_k_num, lora_rank]
859 assert len(i_shape) == 4
860 assert i_shape[0] == num_slices
861 assert i_shape[1] == num_tokens
862 lora_rank = i_shape[-1]
863 # Expected lora weight shape : [num_loras, num_experts, hidden_size, lora_rank]
864 assert len(lw_shape) == 4
865 assert lw_shape[-1] == lora_rank
866 hidden_size = lw_shape[-2]
867 # Expected output shape : [num_tokens, top_k_num, hidden_size * num_slices]
868 assert len(o_shape) == 3
869 assert o_shape == (num_tokens, ctx.top_k_num, hidden_size * num_slices)
870
871 kernel_config = get_lora_op_configs(
872 op_type.name.lower(),
873 max_loras=lw_shape[0],
874 batch=num_tokens,
875 hidden_size=hidden_size,
876 rank=lora_rank,
877 num_slices=num_slices,
878 add_inputs=False,
879 )
880
881 (topk_weights, sorted_token_ids, expert_ids, num_tokens_post_padded) = (
882 self.fused_moe_lora_data_prepare(
883 block_size=kernel_config["BLOCK_SIZE_M"],
884 token_lora_mapping=self.lora_kernel_meta.token_lora_mapping,
885 ctx=ctx,
886 )
887 )
888
889 return {
890 "a_intermediate_cache1": self.input,
891 "lora_b_stacked": self.lora_weights_lst,
892 "output": self.output,
893 "topk_weights": topk_weights,
894 "sorted_token_ids": sorted_token_ids,
895 "expert_ids": expert_ids,
896 "num_tokens_post_padded": num_tokens_post_padded,
897 "token_lora_mapping": self.lora_kernel_meta.token_lora_mapping,
898 "top_k_num": ctx.top_k_num,
899 "device": self.input.device,
900 "N": lora_rank,
901 "M": topk_weights.shape[0],
902 "EM": sorted_token_ids.shape[1],
903 "K": self.input.shape[1],
904 "num_tokens": num_tokens,
905 "num_experts": ctx.num_experts,
906 "num_slices": num_slices,
907 "max_lora_rank": lora_rank,
908 "w1_output_dim_size": lw_shape[2],
909 "expand_block_size_m": kernel_config["BLOCK_SIZE_M"],
910 "expand_block_size_n": kernel_config["BLOCK_SIZE_N"],
911 "expand_block_size_k": kernel_config["BLOCK_SIZE_K"],
912 "expand_group_size_m": kernel_config["GROUP_SIZE_M"],
913 "expand_num_warps": kernel_config["NUM_WARPS"],
914 "expand_num_stages": kernel_config["NUM_STAGES"],
915 "expand_split_k": kernel_config.get("SPLIT_K", 1),
916 "mul_routed_weight": op_type.is_fused_moe_lora_down_fn(),
917 }
918
919 def bench_fn_kwargs(
920 self, ctx: BenchmarkContext, op_type: OpType, add_inputs: bool | None = None
921 ) -> dict[str, Any]:
922 if op_type.is_shrink_fn() or op_type.is_fused_moe_lora_fn():
923 assert add_inputs is None
924 else:
925 assert add_inputs is not None
926
927 if op_type == OpType.LORA_SHRINK:
928 return self.as_lora_shrink_kwargs(ctx, op_type)
929 if op_type == OpType.LORA_EXPAND:
930 return self.as_lora_expand_kwargs(ctx, op_type, add_inputs)
931 if op_type.is_fused_moe_lora_shrink_fn():
932 return self.as_fused_moe_lora_shrink_kwargs(ctx, op_type)
933 if op_type.is_fused_moe_lora_expand_fn():
934 return self.as_fused_moe_lora_expand_kwargs(ctx, op_type)
935 raise ValueError(f"Unrecognized optype {self}")
936
937 def test_correctness(
938 self, op_type: OpType, expand_fn_add_inputs: bool | None
939 ) -> bool:
940 """Test correctness of op_type implementation against a grouped gemm
941 reference implementation.
942 """
943 seq_lens_cpu = self.seq_lens.to(device="cpu")
944 prompt_lora_mapping_cpu = self.prompt_lora_mapping.to(device="cpu")
945 ref_output = self.output.clone()
946
947 self.output.zero_()
948 op_type.bench_fn()(**self.bench_fn_kwargs(op_type, expand_fn_add_inputs))
949
950 op_type.run_ref_group_gemm(
951 ref_output,
952 self.input,
953 self.lora_weights_lst,
954 seq_lens_cpu=seq_lens_cpu,
955 prompt_lora_mapping_cpu=prompt_lora_mapping_cpu,
956 scaling=1.0,
957 add_inputs=expand_fn_add_inputs,
958 )
959
960 rtol, atol = {
961 torch.float16: (6e-2, 6e-2),
962 torch.bfloat16: (6e-2, 6e-2),
963 torch.float32: (1e-2, 1e-2),
964 }[self.output.dtype]
965
966 return torch.allclose(ref_output, self.output, rtol=rtol, atol=atol)
967
968
969 def bench_optype(
970 ctx: BenchmarkContext,
971 arg_pool_size: int,
972 op_type: OpType,
973 cuda_graph_nops: int | None = None,
974 expand_fn_add_inputs: bool | None = None,
975 test_correctness: bool = False,
976 ) -> TMeasurement:
977 assert arg_pool_size >= 1
978 if op_type.is_shrink_fn() or op_type.is_fused_moe_lora_fn():
979 assert expand_fn_add_inputs is None
980 else:
981 assert expand_fn_add_inputs is not None
982
983 # BenchmarkContext -> BenchmarkTensors
984 bench_tensors: list[BenchmarkTensors] = [
985 BenchmarkTensors.make(ctx, op_type) for _ in range(arg_pool_size)
986 ]
987 for bt in bench_tensors:
988 bt.sanity_check(ctx, op_type)
989
990 # Test correctness of our implementation.
991 if test_correctness:
992 assert op_type in [OpType.LORA_SHRINK, OpType.LORA_EXPAND], (
993 f"Correctness testing is not supported for {op_type.name}."
994 )
995 assert all(
996 [
997 bt.test_correctness(ctx, op_type, expand_fn_add_inputs)
998 for bt in bench_tensors
999 ]
1000 )
1001
1002 # BenchmarkTensors -> dict (kwargs)
1003 kwargs_list = [
1004 bt.bench_fn_kwargs(ctx, op_type, add_inputs=expand_fn_add_inputs)
1005 for bt in bench_tensors
1006 ]
1007
1008 # Clear LoRA optimization hash-maps.
1009 _LORA_A_PTR_DICT.clear()
1010 _LORA_B_PTR_DICT.clear()
1011 _LORA_PTR_DICT.clear()
1012 # Run bench function so that _LORA_A_PTR_DICT and _LORA_B_PTR_DICT are set up
1013 for kwargs in kwargs_list:
1014 op_type.bench_fn()(**kwargs)
1015 torch.accelerator.synchronize()
1016
1017 # Merge into a single kwargs and qualify arguments as ArgPool
1018 kwargs = {k: ArgPool([]) for k in kwargs_list[0]}
1019 for _kwargs in kwargs_list:
1020 for k, v in _kwargs.items():
1021 kwargs[k].values.append(v)
1022
1023 describe_args = (
1024 f"add_inputs={expand_fn_add_inputs}" if expand_fn_add_inputs is not None else ""
1025 )
1026 description = f"{op_type.name}({describe_args}) ({bench_tensors[0].io_types()})"
1027
1028 cuda_graph_params = None
1029 if cuda_graph_nops:
1030 cuda_graph_params = CudaGraphBenchParams(cuda_graph_nops)
1031 timer = None
1032 with Bench(
1033 cuda_graph_params,
1034 ctx.bench_label(),
1035 ctx.bench_sublabel(op_type),
1036 description,
1037 op_type.bench_fn(),
1038 **kwargs,
1039 ) as bench:
1040 timer = bench.run()
1041 return timer
1042
1043
1044 def bench_torch_mm(
1045 ctx: BenchmarkContext,
1046 arg_pool_size: int,
1047 op_type: OpType,
1048 cuda_graph_nops: int | None = None,
1049 ) -> TMeasurement:
1050 """Benchmark basic torch.mm as a roofline.
1051
1052 When all the input tokens have the same LoRA ID, the LoRA kernels are just
1053 a matmul. This torch.mm benchmark serves as a roofline for that case.
1054
1055 input op_type is used in determining the m, k, n dimensions for the matmul.
1056 """
1057 batch_size, hidden_size, lora_rank, seq_length, dtype = (
1058 ctx.batch_size,
1059 ctx.hidden_size,
1060 ctx.lora_rank,
1061 ctx.seq_length,
1062 ctx.dtype,
1063 )
1064
1065 m, k, n = op_type.mkn(batch_size, seq_length, hidden_size, lora_rank)
1066 # For a fairer comparison.
1067 n = n * ctx.num_slices
1068
1069 # Get matmul input and output tensors for A x B = C
1070 As, Bs, Cs = [], [], []
1071 for _ in range(arg_pool_size):
1072 As.append(torch.rand((m, k), dtype=dtype).to("cuda"))
1073 Bs.append(torch.rand((n, k), dtype=dtype).to("cuda").t())
1074 Cs.append(torch.rand((m, n), dtype=dtype).to("cuda"))
1075
1076 # Make torch.mm kwargs
1077 mm_kwargs = {"input": ArgPool(As), "mat2": ArgPool(Bs), "out": ArgPool(Cs)}
1078
1079 description = (
1080 f"single-lora roofline using torch.mm ({dtype_to_str(dtype)}"
1081 f"x{dtype_to_str(dtype)}"
1082 f"=>{dtype_to_str(dtype)})"
1083 )
1084 cuda_graph_params = None
1085 if cuda_graph_nops:
1086 cuda_graph_params = CudaGraphBenchParams(cuda_graph_nops)
1087 with Bench(
1088 cuda_graph_params,
1089 ctx.bench_label(),
1090 ctx.bench_sublabel(op_type),
1091 description,
1092 torch.mm,
1093 **mm_kwargs,
1094 ) as bench:
1095 return bench.run()
1096
1097
1098 # runner
1099 def use_cuda_graph_recommendation() -> str:
1100 return """
1101 Triton kernels have a significant launch overhead with
1102 launched directly via python. This overhead is more noticeable
1103 for small the problem sizes. For these cases, it is recommended
1104 to use the script with `--cuda-graph-nops N` to benchmark N
1105 consecutive invocations of the benchmarking operations from
1106 inside a CUDA Graph. Note that the returned measurement is for N
1107 invocations of the operation.
1108 """
1109
1110
1111 def print_timers(timers: list[TMeasurement], args: argparse.Namespace | None = None):
1112 compare = TBenchmark.Compare(timers)
1113 compare.print()
1114
1115 if args and args.cuda_graph_nops:
1116 print(
1117 f"Note : The timings reported above is for {args.cuda_graph_nops} "
1118 "consecutive invocations of the benchmarking functions. "
1119 f"Please divide by {args.cuda_graph_nops} for single invocation "
1120 "timings."
1121 )
1122
1123 print(
1124 "Note on Comparison with torch.mm : The torch.mm numbers are "
1125 "benchmark numbers of a simple matmul emulating the single lora "
1126 "case. It is provided as a roofline for comparing our LoRA Kernel "
1127 "implementations. It is expected that the LoRA kernels will be "
1128 "slower than torch.mm in cases where num_loras is big. But for "
1129 "small num_loras the goal should be to match the torch.mm numbers."
1130 )
1131
1132
1133 def run(args: argparse.Namespace, bench_ctxs: list[BenchmarkContext]):
1134 if args.cuda_graph_nops is not None:
1135 assert args.cuda_graph_nops > 0
1136 print(f"Benchmarking {args.cuda_graph_nops} invocations inside a CUDA Graph")
1137 else:
1138 print(f"CUDA Graphs not enabled.\n{use_cuda_graph_recommendation()}")
1139
1140 timers = []
1141 for bench_ctx in bench_ctxs:
1142 for seq_len in args.seq_lengths:
1143 bench_ops: list[OpType] = args.op_types
1144 seq_len_timers = []
1145 for bench_op in bench_ops:
1146 for num_slices in bench_op.num_slices():
1147 _ctx = bench_ctx.with_seq_length(seq_len).with_num_slices(
1148 num_slices
1149 )
1150 # Benchmark torch.mm as a roofline
1151 seq_len_timers.append(
1152 bench_torch_mm(
1153 _ctx, args.arg_pool_size, bench_op, args.cuda_graph_nops
1154 )
1155 )
1156
1157 # Benchmark bench_op
1158 expand_fn_add_inputs = (
1159 [None]
1160 if bench_op.is_shrink_fn() or bench_op.is_fused_moe_lora_fn()
1161 else args.expand_fn_add_inputs
1162 )
1163 for add_input_arg in expand_fn_add_inputs:
1164 seq_len_timers.append(
1165 bench_optype(
1166 _ctx,
1167 args.arg_pool_size,
1168 bench_op,
1169 args.cuda_graph_nops,
1170 add_input_arg,
1171 args.test_correctness,
1172 )
1173 )
1174
1175 print_timers(seq_len_timers)
1176 timers.extend(seq_len_timers)
1177
1178 # Result stdout dump
1179 print("== All Results ====")
1180 print_timers(timers, args)
1181
1182 if args.output_directory:
1183 # Result file dump
1184 od = Path(args.output_directory)
1185 if not od.exists():
1186 od.mkdir()
1187
1188 timestamp = int(time.time())
1189 pkl_file = od / f"lora_bench-{timestamp}.pkl"
1190 print(f"Writing benchmarks to {pkl_file}")
1191 with open(pkl_file, "wb") as f:
1192 pickle.dump(timers, f)
1193
1194
1195 def as_benchmark_contexts(
1196 hidden_sizes: list[int], lora_ranks: list[int], args: argparse.Namespace
1197 ) -> list[BenchmarkContext]:
1198 ctxs: list[BenchmarkContext] = []
1199 for (
1200 batch_size,
1201 hidden_size,
1202 lora_rank,
1203 num_loras,
1204 sort_by_lora_id,
1205 top_k_num,
1206 num_experts,
1207 ) in product( # noqa
1208 args.batch_sizes,
1209 list(hidden_sizes),
1210 lora_ranks,
1211 args.num_loras,
1212 args.sort_by_lora_id,
1213 args.top_k_nums,
1214 args.num_experts,
1215 ):
1216 ctxs.append(
1217 BenchmarkContext(
1218 batch_size=batch_size,
1219 hidden_size=hidden_size,
1220 lora_rank=lora_rank,
1221 num_loras=num_loras,
1222 num_active_loras=args.num_active_loras
1223 if args.num_active_loras
1224 else num_loras,
1225 # To be filled based on the OpType to benchmark
1226 seq_length=None,
1227 sort_by_lora_id=sort_by_lora_id,
1228 dtype=args.dtype,
1229 top_k_num=top_k_num,
1230 num_experts=num_experts,
1231 # To be filled based on the OpType to benchmark
1232 num_slices=None,
1233 )
1234 )
1235
1236 return ctxs
1237
1238
1239 def run_list_bench(args: argparse.Namespace):
1240 print(args)
1241
1242 print(
1243 "List bench :\n"
1244 f" Hidden Sizes {args.hidden_sizes}"
1245 f" LoRA Ranks {args.lora_ranks}"
1246 )
1247
1248 # Get all benchmarking contexts
1249 bench_contexts: list[BenchmarkContext] = as_benchmark_contexts(
1250 hidden_sizes=args.hidden_sizes, lora_ranks=args.lora_ranks, args=args
1251 )
1252
1253 run(args, bench_contexts)
1254
1255
1256 def run_range_bench(args: argparse.Namespace):
1257 print(args)
1258
1259 hidden_sizes = list(
1260 range(
1261 args.hidden_sizes_start,
1262 args.hidden_sizes_end + 1,
1263 args.hidden_sizes_increment,
1264 )
1265 )
1266 lora_ranks = list(
1267 range(args.lora_ranks_start, args.lora_ranks_end + 1, args.lora_ranks_increment)
1268 )
1269
1270 print(f"Range bench :\n Hidden Sizes {hidden_sizes} LoRA Ranks {lora_ranks}")
1271
1272 # Get all benchmarking contexts
1273 bench_contexts: list[BenchmarkContext] = as_benchmark_contexts(
1274 hidden_sizes=hidden_sizes, lora_ranks=lora_ranks, args=args
1275 )
1276
1277 run(args, bench_contexts)
1278
1279
1280 def run_model_bench(args: argparse.Namespace):
1281 print(args)
1282
1283 def hidden_sizes_from_model(model: str, tp_size: int) -> set[int]:
1284 hidden_sizes = set()
1285 for KN, tp_split_dim in WEIGHT_SHAPES[model]:
1286 KN[tp_split_dim] = KN[tp_split_dim] // tp_size
1287 hidden_sizes.add(KN[1])
1288 return hidden_sizes
1289
1290 # Get all hidden sizes
1291 hidden_sizes: set[int] = set()
1292 for model_name, tp_size in product(args.models, args.tp_sizes):
1293 hidden_sizes = hidden_sizes.union(hidden_sizes_from_model(model_name, tp_size))
1294
1295 print(f"Model bench :\n Hidden Sizes {hidden_sizes} LoRA Ranks {args.lora_ranks}")
1296
1297 # Get all benchmarking contexts
1298 bench_contexts: list[BenchmarkContext] = as_benchmark_contexts(
1299 hidden_sizes=hidden_sizes, lora_ranks=args.lora_ranks, args=args
1300 )
1301
1302 run(args, bench_contexts)
1303
1304
1305 if __name__ == "__main__":
1306
1307 def to_torch_dtype(dt):
1308 if dt == "torch.float16":
1309 return torch.float16
1310 if dt == "torch.bfloat16":
1311 return torch.bfloat16
1312 raise ValueError("unsupported dtype")
1313
1314 def get_bool(s: str) -> bool:
1315 return s.lower() in ["true", "1"]
1316
1317 def add_common_command_args(p: argparse.ArgumentParser):
1318 p.add_argument(
1319 "--dtype",
1320 type=to_torch_dtype,
1321 required=True,
1322 help="Available options are ['torch.float16', 'torch.bfloat16']",
1323 )
1324
1325 p.add_argument(
1326 "--arg-pool-size",
1327 type=int,
1328 default=32,
1329 help="Run profiles with a pool of input/output/meta tensors instead"
1330 "of simply reusing the same tensors for all runs. A bigger arg-pool"
1331 "mitigates hardware caching effects during benchmarking.",
1332 )
1333
1334 p.add_argument(
1335 "--cuda-graph-nops",
1336 type=int,
1337 help=(
1338 "when set profiling is done using cudagraph, "
1339 "with the given number of operations in a graph."
1340 "Note that the measurement returned is the time "
1341 "taken for N consecutive executions of the benchmarking "
1342 "functions, where N is the value of this argument."
1343 ),
1344 )
1345 p.add_argument("--num-loras", nargs="+", type=int, default=DEFAULT_NUM_LORAS)
1346 p.add_argument(
1347 "--num-active-loras",
1348 type=int,
1349 default=None,
1350 help="Active LoRAs. When None, all LoRAs are active",
1351 )
1352 p.add_argument(
1353 "--sort-by-lora-id",
1354 nargs="+",
1355 type=get_bool,
1356 default=DEFAULT_SORT_BY_LORA_IDS,
1357 )
1358 p.add_argument(
1359 "--op-types", nargs="+", type=OpType.from_str, default=list(OpType)
1360 )
1361 p.add_argument(
1362 "--seq-lengths", nargs="+", type=int, default=DEFAULT_SEQ_LENGTHS
1363 )
1364 p.add_argument(
1365 "--batch-sizes", nargs="+", type=int, default=DEFAULT_BATCH_SIZES
1366 )
1367 p.add_argument(
1368 "--expand-fn-add-inputs",
1369 nargs="+",
1370 type=get_bool,
1371 default=DEFAULT_EXPAND_FN_ADD_INPUTS,
1372 )
1373 p.add_argument(
1374 "-o",
1375 "--output-directory",
1376 type=str,
1377 help=(
1378 "Output directory to store a the list of benchmarking"
1379 "TMeasurement objects as a pickle file"
1380 ),
1381 )
1382
1383 p.add_argument(
1384 "--test-correctness",
1385 action="store_true",
1386 help=(
1387 "When enabled, the benchmarking functions are tested"
1388 "for correctness before the actual benchmarking"
1389 ),
1390 )
1391
1392 p.add_argument(
1393 "--top-k-nums",
1394 nargs="+",
1395 type=int,
1396 default=DEFAULT_TOP_K_NUMS,
1397 help="Top-K values for MoE LoRA operations",
1398 )
1399
1400 p.add_argument(
1401 "--num-experts",
1402 nargs="+",
1403 type=int,
1404 default=DEFAULT_NUM_EXPERTS,
1405 help="Number of experts for MoE LoRA operations",
1406 )
1407
1408 parser = FlexibleArgumentParser(
1409 description=f"""
1410 Benchmark LoRA kernels:
1411 {use_cuda_graph_recommendation()}
1412
1413 list_bench example:
1414 python3 benchmarks/kernels/benchmark_lora.py list_bench --arg-pool-size 32 --batch-sizes 1 16 32 --dtype torch.float16 --hidden-sizes 2048 --lora-ranks 16 --num-loras 1 4 --op-types lora_shrink lora_expand --seq-lengths 1 16 --sort-by-lora-id 1 --cuda-graph-nops 32
1415
1416 model_bench example:
1417 python3 benchmarks/kernels/benchmark_lora.py model_bench --models meta-llama/Llama-3-8b --arg-pool-size 32 --batch-sizes 1 16 32 --dtype torch.float16 --lora-ranks 16 --num-loras 1 4 --op-types lora_shrink lora_expand --seq-lengths 1 16 --sort-by-lora-id 1 --cuda-graph-nops 32
1418
1419 range_bench example:
1420 python3 benchmarks/kernels/benchmark_lora.py range_bench --arg-pool-size 32 --batch-sizes 1 16 32 --dtype torch.float16 --num-loras 1 4 --op-types lora_shrink lora_expand --seq-lengths 1 16 --sort-by-lora-id 1 --cuda-graph-nops 32 --hidden-sizes-start 1024 --hidden-sizes-end 4096 --hidden-sizes-increment 1024 --lora-ranks-start 8 --lora-ranks-end 24 --lora-ranks-increment 8
1421 """, # noqa: E501
1422 formatter_class=argparse.RawTextHelpFormatter,
1423 )
1424
1425 subparsers = parser.add_subparsers(dest="cmd", required=True)
1426
1427 list_parser = subparsers.add_parser("list_bench")
1428 list_parser.add_argument(
1429 "--hidden-sizes", nargs="+", type=int, default=DEFAULT_HIDDEN_SIZES
1430 )
1431 list_parser.add_argument(
1432 "--lora-ranks", nargs="+", type=int, default=DEFAULT_LORA_RANKS
1433 )
1434 add_common_command_args(list_parser)
1435 list_parser.set_defaults(func=run_list_bench)
1436
1437 range_parser = subparsers.add_parser("range_bench")
1438 range_parser.add_argument("--hidden-sizes-start", type=int, required=True)
1439 range_parser.add_argument("--hidden-sizes-end", type=int, required=True)
1440 range_parser.add_argument("--hidden-sizes-increment", type=int, required=True)
1441 range_parser.add_argument("--lora-ranks-start", type=int, required=True)
1442 range_parser.add_argument("--lora-ranks-end", type=int, required=True)
1443 range_parser.add_argument("--lora-ranks-increment", type=int, required=True)
1444 add_common_command_args(range_parser)
1445 range_parser.set_defaults(func=run_range_bench)
1446
1447 model_parser = subparsers.add_parser("model_bench")
1448 model_parser.add_argument(
1449 "--models",
1450 nargs="+",
1451 type=str,
1452 default=DEFAULT_MODELS,
1453 choices=WEIGHT_SHAPES.keys(),
1454 )
1455 model_parser.add_argument(
1456 "--tp-sizes", nargs="+", type=int, default=DEFAULT_TP_SIZES
1457 )
1458 model_parser.add_argument(
1459 "--lora-ranks", nargs="+", type=int, default=DEFAULT_LORA_RANKS
1460 )
1461 add_common_command_args(model_parser)
1462 model_parser.set_defaults(func=run_model_bench)
1463
1464 args = parser.parse_args()
1465 args.func(args)