@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / 294c805f1d
Fork 0

A high-throughput and memory-efficient inference and serving engine for LLMs

Mirrored from https://github.com/tduncan-lum/Nvidia-Optimization-Stack---VLLM.git

Synced Nov 13, 20:45 UTC

Early exit for MoE LoRA kernels (#27131)

Signed-off-by: gnovack <gnovack@amazon.com> Co-authored-by: Jee Jee Li <pandaleefree@gmail.com>

gnovack committed Nov 3, 2025 at 04:22 UTC 294c805f1df9ddf62c2290989710da9d48ab4973
11 files changed +123 -34
csrc/moe/moe_lora_align_sum_kernels.cu
+16 -11
@@ -28,11 +28,16 @@ __global__ void moe_lora_align_sum_kernel(
28 int64_t block_size, int num_experts, int max_loras, size_t numel,
29 int max_num_tokens_padded, int max_num_m_blocks,
30 int32_t* __restrict__ sorted_token_ids, int32_t* __restrict__ expert_ids,
31 - int topk_num, int32_t* total_tokens_post_pad) {
31 + int topk_num, int32_t* total_tokens_post_pad, int32_t* adapter_enabled,
32 + int32_t* lora_ids) {
33 const size_t tokens_per_thread = div_ceil(numel, blockDim.x);
34 const size_t start_idx = threadIdx.x * tokens_per_thread;
35
35 - int lora_id = blockIdx.x;
36 + int lora_idx = blockIdx.x;
37 + int lora_id = lora_ids[lora_idx];
38 + if (lora_id == -1 || adapter_enabled[lora_id] == 0) {
39 + return;
40 + }
41 extern __shared__ int32_t shared_mem[];
42 int32_t* cumsum = shared_mem;
43 token_cnts_t* tokens_cnts = (token_cnts_t*)(shared_mem + num_experts + 1);
@@ -121,14 +126,13 @@ __global__ void moe_lora_align_sum_kernel(
126 }
127 }
128
124 -void moe_lora_align_block_size(torch::Tensor topk_ids,
125 - torch::Tensor token_lora_mapping,
126 - int64_t num_experts, int64_t block_size,
127 - int64_t max_loras, int64_t max_num_tokens_padded,
128 - int64_t max_num_m_blocks,
129 - torch::Tensor sorted_token_ids,
130 - torch::Tensor expert_ids,
131 - torch::Tensor num_tokens_post_pad) {
129 +void moe_lora_align_block_size(
130 + torch::Tensor topk_ids, torch::Tensor token_lora_mapping,
131 + int64_t num_experts, int64_t block_size, int64_t max_loras,
132 + int64_t max_num_tokens_padded, int64_t max_num_m_blocks,
133 + torch::Tensor sorted_token_ids, torch::Tensor expert_ids,
134 + torch::Tensor num_tokens_post_pad, torch::Tensor adapter_enabled,
135 + torch::Tensor lora_ids) {
136 const int topk_num = topk_ids.size(1);
137
138 TORCH_CHECK(block_size > 0, "block_size should be greater than 0. ");
@@ -164,6 +168,7 @@ void moe_lora_align_block_size(torch::Tensor topk_ids,
168 max_loras, topk_ids.numel(), max_num_tokens_padded,
169 max_num_m_blocks, sorted_token_ids.data_ptr<int32_t>(),
170 expert_ids.data_ptr<int32_t>(), topk_num,
167 - num_tokens_post_pad.data_ptr<int32_t>());
171 + num_tokens_post_pad.data_ptr<int32_t>(),
172 + adapter_enabled.data_ptr<int32_t>(), lora_ids.data_ptr<int32_t>());
173 });
174 }
\ No newline at end of file
csrc/moe/moe_ops.h
+7 -8
@@ -20,14 +20,13 @@ void batched_moe_align_block_size(int64_t max_tokens_per_batch,
20 torch::Tensor expert_ids,
21 torch::Tensor num_tokens_post_pad);
22
23 -void moe_lora_align_block_size(torch::Tensor topk_ids,
24 - torch::Tensor token_lora_mapping,
25 - int64_t num_experts, int64_t block_size,
26 - int64_t max_loras, int64_t max_num_tokens_padded,
27 - int64_t max_num_m_blocks,
28 - torch::Tensor sorted_token_ids,
29 - torch::Tensor expert_ids,
30 - torch::Tensor num_tokens_post_pad);
23 +void moe_lora_align_block_size(
24 + torch::Tensor topk_ids, torch::Tensor token_lora_mapping,
25 + int64_t num_experts, int64_t block_size, int64_t max_loras,
26 + int64_t max_num_tokens_padded, int64_t max_num_m_blocks,
27 + torch::Tensor sorted_token_ids, torch::Tensor expert_ids,
28 + torch::Tensor num_tokens_post_pad, torch::Tensor adapter_enabled,
29 + torch::Tensor lora_ids);
30 #ifndef USE_ROCM
31 torch::Tensor moe_wna16_gemm(torch::Tensor input, torch::Tensor output,
32 torch::Tensor b_qweight, torch::Tensor b_scales,
csrc/moe/torch_bindings.cpp
+3 -1
@@ -44,7 +44,9 @@ TORCH_LIBRARY_EXPAND(TORCH_EXTENSION_NAME, m) {
44 " int max_num_m_blocks, "
45 " Tensor !sorted_token_ids,"
46 " Tensor !experts_ids,"
47 - " Tensor !num_tokens_post_pad) -> () ");
47 + " Tensor !num_tokens_post_pad,"
48 + " Tensor !adapter_enabled,"
49 + " Tensor !lora_ids) -> () ");
50 m.impl("moe_lora_align_block_size", torch::kCUDA, &moe_lora_align_block_size);
51
52 #ifndef USE_ROCM
tests/lora/test_fused_moe_lora_kernel.py
+6
@@ -134,6 +134,8 @@ def use_fused_moe_lora_kernel(
134 )
135 expert_ids = torch.empty((max_loras * max_num_m_blocks,), dtype=torch.int32)
136 num_tokens_post_padded = torch.empty((max_loras,), dtype=torch.int32)
137 + adapter_enabled = torch.ones(max_loras + 1, dtype=torch.int32)
138 + lora_ids = torch.arange(max_loras + 2, dtype=torch.int32)
139
140 # call kernel
141 ops.moe_lora_align_block_size(
@@ -147,6 +149,8 @@ def use_fused_moe_lora_kernel(
149 sorted_token_ids,
150 expert_ids,
151 num_tokens_post_padded,
152 + adapter_enabled,
153 + lora_ids,
154 )
155
156 config = {
@@ -172,6 +176,8 @@ def use_fused_moe_lora_kernel(
176 num_tokens_post_padded,
177 max_lora_rank,
178 top_k_num,
179 + lora_ids,
180 + adapter_enabled,
181 config["BLOCK_SIZE_M"],
182 config["BLOCK_SIZE_N"],
183 config["BLOCK_SIZE_K"],
tests/lora/test_moe_lora_align_sum.py
+4
@@ -60,6 +60,8 @@ def test_moe_lora_align_block_size(
60 (max_loras * max_num_m_blocks,), num_experts, dtype=torch.int32, device="cuda"
61 )
62 num_tokens_post_pad = torch.zeros((max_loras,), dtype=torch.int32, device="cuda")
63 + adapter_enabled = torch.ones((max_loras + 1,), dtype=torch.int32, device="cuda")
64 + lora_ids = torch.arange(max_loras + 2, dtype=torch.int32, device="cuda")
65
66 # call kernel
67 ops.moe_lora_align_block_size(
@@ -73,6 +75,8 @@ def test_moe_lora_align_block_size(
75 sorted_token_ids,
76 expert_ids,
77 num_tokens_post_pad,
78 + adapter_enabled,
79 + lora_ids,
80 )
81
82 # verify values
tests/lora/test_olmoe_tp.py
+43 -7
@@ -1,6 +1,7 @@
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 +
5 import vllm
6 from vllm.lora.request import LoRARequest
7
@@ -28,8 +29,17 @@ EXPECTED_LORA_OUTPUT = [
29 "SELECT poll_source FROM candidate GROUP BY poll_source ORDER BY count(*) DESC LIMIT 1", # noqa: E501
30 ]
31
32 +EXPECTED_BASE_MODEL_OUTPUT = [
33 + "SELECT COUNT(Candidate_ID) FROM candidate",
34 + "SELECT COUNT(Candidate_ID) FROM candidate",
35 + "SELECT Candidate_ID, COUNT(*) as Total_Candidates\nFROM candidate\nINNER JOIN people ON candidate.People_ID = people.People_ID", # noqa: E501
36 + "SELECT Candidate_ID, Poll_Source FROM candidate WHERE People_ID IN (SELECT People_ID FROM people) ORDER BY COUNT(*) DESC LIMIT 1", # noqa: E501
37 +]
38 +
39
32 -def generate_and_test(llm: vllm.LLM, lora_path: str, lora_id: int) -> None:
40 +def generate_and_test(
41 + llm: vllm.LLM, lora_path: str, lora_id: list[int | None] | int | None
42 +) -> None:
43 prompts = [
44 PROMPT_TEMPLATE.format(context="How many candidates are there?"),
45 PROMPT_TEMPLATE.format(context="Count the number of candidates."),
@@ -40,12 +50,18 @@ def generate_and_test(llm: vllm.LLM, lora_path: str, lora_id: int) -> None:
50 context="Return the poll resource associated with the most candidates."
51 ),
52 ]
53 +
54 + lora_request = None
55 + if isinstance(lora_id, int):
56 + lora_request = LoRARequest(str(lora_id), lora_id, lora_path)
57 + elif isinstance(lora_id, list):
58 + lora_request = [
59 + LoRARequest(str(i), i, lora_path) if i is not None else None
60 + for i in lora_id
61 + ]
62 +
63 sampling_params = vllm.SamplingParams(temperature=0, max_tokens=64)
44 - outputs = llm.generate(
45 - prompts,
46 - sampling_params,
47 - lora_request=LoRARequest(str(lora_id), lora_id, lora_path) if lora_id else None,
48 - )
64 + outputs = llm.generate(prompts, sampling_params, lora_request=lora_request)
65 # Print the outputs.
66 generated_texts: list[str] = []
67 for output in outputs:
@@ -55,7 +71,13 @@ def generate_and_test(llm: vllm.LLM, lora_path: str, lora_id: int) -> None:
71 print(f"Prompt: {prompt!r}, Generated text: {generated_text!r}")
72
73 for i in range(len(EXPECTED_LORA_OUTPUT)):
58 - assert generated_texts[i].startswith(EXPECTED_LORA_OUTPUT[i])
74 + req_lora_id = lora_id[i] if isinstance(lora_id, list) else lora_id
75 + expected_output = (
76 + EXPECTED_LORA_OUTPUT[i]
77 + if req_lora_id is not None
78 + else EXPECTED_BASE_MODEL_OUTPUT[i]
79 + )
80 + assert generated_texts[i].startswith(expected_output)
81
82
83 def test_olmoe_lora(olmoe_lora_files):
@@ -75,6 +97,20 @@ def test_olmoe_lora(olmoe_lora_files):
97 generate_and_test(llm, olmoe_lora_files, lora_id=2)
98
99
100 +def test_olmoe_lora_mixed(olmoe_lora_files):
101 + llm = vllm.LLM(
102 + MODEL_PATH,
103 + max_model_len=1024,
104 + enable_lora=True,
105 + max_loras=4,
106 + enforce_eager=True,
107 + trust_remote_code=True,
108 + enable_chunked_prefill=True,
109 + )
110 +
111 + generate_and_test(llm, olmoe_lora_files, lora_id=[1, None, 3, None])
112 +
113 +
114 @multi_gpu_test(num_gpus=2)
115 def test_olmoe_lora_tp2(olmoe_lora_files):
116 llm = vllm.LLM(
vllm/_custom_ops.py
+4
@@ -1823,6 +1823,8 @@ def moe_lora_align_block_size(
1823 sorted_token_ids: torch.Tensor,
1824 experts_ids: torch.Tensor,
1825 num_tokens_post_pad: torch.Tensor,
1826 + adapter_enabled: torch.Tensor,
1827 + lora_ids: torch.Tensor,
1828 ) -> None:
1829 torch.ops._moe_C.moe_lora_align_block_size(
1830 topk_ids,
@@ -1835,6 +1837,8 @@ def moe_lora_align_block_size(
1837 sorted_token_ids,
1838 experts_ids,
1839 num_tokens_post_pad,
1840 + adapter_enabled,
1841 + lora_ids,
1842 )
1843
1844
vllm/lora/layers/fused_moe.py
+10 -1
@@ -111,6 +111,7 @@ class FusedMoEWithLoRA(BaseLayerWithLoRA):
111 config["BLOCK_SIZE_M"],
112 self.base_layer.local_num_experts,
113 max_loras,
114 + self.adapter_enabled,
115 expert_map,
116 )
117
@@ -138,6 +139,7 @@ class FusedMoEWithLoRA(BaseLayerWithLoRA):
139 max_lora_rank,
140 top_k,
141 config,
142 + self.adapter_enabled,
143 )
144
145 result = func(*args, **kwargs)
@@ -196,6 +198,7 @@ class FusedMoEWithLoRA(BaseLayerWithLoRA):
198 max_lora_rank,
199 top_k,
200 config,
201 + self.adapter_enabled,
202 True,
203 )
204
@@ -227,6 +230,10 @@ class FusedMoEWithLoRA(BaseLayerWithLoRA):
230 ) -> None:
231 """Initializes lora matrices."""
232
233 + self.adapter_enabled = torch.tensor(
234 + [0] * (max_loras + 1), dtype=torch.int, device=self.device
235 + )
236 +
237 self.w1_lora_a_stacked = torch.zeros(
238 (
239 max_loras,
@@ -313,6 +320,7 @@ class FusedMoEWithLoRA(BaseLayerWithLoRA):
320 self.w3_lora_b_stacked[index] = 0
321 self.w2_lora_a_stacked[index] = 0
322 self.w2_lora_b_stacked[index] = 0
323 + self.adapter_enabled[index] = 0
324
325 def set_lora(
326 self,
@@ -322,8 +330,9 @@ class FusedMoEWithLoRA(BaseLayerWithLoRA):
330 embeddings_tensor: torch.Tensor | None,
331 bias: torch.Tensor | None = None,
332 ):
325 - self.reset_lora(index)
333 """Overwrites lora tensors at index."""
334 + self.reset_lora(index)
335 + self.adapter_enabled[index] = 1
336 for eid in range(len(lora_a) // 3):
337 w1_lora_a = lora_a[eid * 3]
338 w2_lora_a = lora_a[eid * 3 + 1]
vllm/lora/ops/triton_ops/fused_moe_lora_op.py
+20 -5
@@ -54,6 +54,8 @@ def _fused_moe_lora_kernel(
54 EM,
55 num_valid_tokens,
56 num_experts,
57 + lora_ids,
58 + adapter_enabled,
59 # The stride variables represent how much to increase the ptr by when
60 # moving by 1 element in a particular dimension. E.g. `stride_am` is
61 # how much to increase `a_ptr` by to get the element one row down
@@ -84,6 +86,11 @@ def _fused_moe_lora_kernel(
86 pid = tl.program_id(axis=0)
87 slice_id = tl.program_id(axis=1)
88 lora_idx = tl.program_id(axis=2)
89 + lora_id = tl.load(lora_ids + lora_idx)
90 + moe_enabled = tl.load(adapter_enabled + lora_id)
91 + if lora_id == -1 or moe_enabled == 0:
92 + # Early exit for the no-lora case.
93 + return
94 max_loras = tl.num_programs(axis=2)
95 grid_k = tl.cdiv(K, BLOCK_SIZE_K * SPLIT_K)
96
@@ -100,12 +107,12 @@ def _fused_moe_lora_kernel(
107 pid_m = first_pid_m + ((pid_m_n % num_pid_in_group) % group_size_m)
108 pid_n = (pid_m_n % num_pid_in_group) // group_size_m
109
103 - num_tokens_post_padded = tl.load(num_tokens_post_padded_ptr + lora_idx)
110 + num_tokens_post_padded = tl.load(num_tokens_post_padded_ptr + lora_id)
111 if pid_m * BLOCK_SIZE_M >= num_tokens_post_padded:
112 return
113
114 # get the expert_id to process curr shard
108 - ind = lora_idx * stride_el + pid_m
115 + ind = lora_id * stride_el + pid_m
116 expert_id = tl.load(expert_ids_ptr + ind, ind < max_loras * stride_el, -1)
117 if expert_id == -1:
118 return
@@ -119,7 +126,7 @@ def _fused_moe_lora_kernel(
126 offs_k = pid_sk * BLOCK_SIZE_K + tl.arange(0, BLOCK_SIZE_K)
127
128 offs_token_id = pid_m * BLOCK_SIZE_M + tl.arange(0, BLOCK_SIZE_M).to(tl.int64)
122 - token_ind = stride_tl * lora_idx + offs_token_id
129 + token_ind = stride_tl * lora_id + offs_token_id
130 offs_token = tl.load(
131 sorted_token_ids_ptr + token_ind, token_ind < max_loras * stride_tl, 0
132 )
@@ -132,7 +139,7 @@ def _fused_moe_lora_kernel(
139
140 b_ptrs = (
141 cur_b_ptr
135 - + lora_idx * stride_bl
142 + + lora_id * stride_bl
143 + expert_id * stride_be
144 + offs_k[:, None] * stride_bk
145 + offs_bn[None, :] * stride_bn
@@ -184,6 +191,8 @@ def _fused_moe_lora(
191 num_tokens_post_padded: torch.Tensor, # (max_loras, )
192 max_lora_rank: int,
193 top_k_num: int,
194 + lora_ids: torch.Tensor,
195 + adapter_enabled: torch.Tensor,
196 block_size_m: int,
197 block_size_n: int,
198 block_size_k: int,
@@ -234,7 +243,7 @@ def _fused_moe_lora(
243 num_tokens = M * top_k_num
244 w1_output_dim_size = w1_lora_b_stacked.shape[2]
245
237 - lora_intermediate_cache1 = torch.empty(
246 + lora_intermediate_cache1 = torch.zeros(
247 (num_slices * M * top_k_num * (max_lora_rank + w1_output_dim_size)),
248 dtype=output.dtype,
249 device=device,
@@ -272,6 +281,8 @@ def _fused_moe_lora(
281 EM,
282 num_tokens,
283 num_experts,
284 + lora_ids,
285 + adapter_enabled,
286 qcurr_hidden_states.stride(0),
287 qcurr_hidden_states.stride(1),
288 w1_lora_a_stacked.stride(0),
@@ -319,6 +330,8 @@ def _fused_moe_lora(
330 EM,
331 num_tokens,
332 num_experts,
333 + lora_ids,
334 + adapter_enabled,
335 a_intermediate_cache1.stride(0),
336 a_intermediate_cache1.stride(1),
337 w1_lora_b_stacked.stride(0),
@@ -352,6 +365,8 @@ def _fused_moe_lora_fake(
365 num_tokens_post_padded: torch.Tensor,
366 max_lora_rank: int,
367 top_k_num: int,
368 + lora_ids: torch.Tensor,
369 + adapter_enabled: torch.Tensor,
370 block_size_m: int,
371 block_size_n: int,
372 block_size_k: int,
vllm/lora/punica_wrapper/punica_base.py
+2
@@ -456,6 +456,7 @@ class PunicaWrapperBase(PunicaWrapperABC):
456 block_size: int,
457 num_experts: int,
458 max_loras: int,
459 + adapter_enabled: torch.Tensor,
460 expert_map: torch.Tensor | None = None,
461 pad_sorted_ids: bool = False,
462 ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
@@ -479,6 +480,7 @@ class PunicaWrapperBase(PunicaWrapperABC):
480 max_lora_rank: int,
481 top_k_num: int,
482 config,
483 + adapter_enabled: torch.Tensor,
484 mul_routed_weight=False,
485 ):
486 """
vllm/lora/punica_wrapper/punica_gpu.py
+8 -1
@@ -305,6 +305,7 @@ class PunicaWrapperGPU(PunicaWrapperBase):
305 block_size: int,
306 num_experts: int,
307 max_loras: int,
308 + adapter_enabled: torch.Tensor,
309 expert_map: torch.Tensor | None = None,
310 pad_sorted_ids: bool = False,
311 ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
@@ -331,7 +332,7 @@ class PunicaWrapperGPU(PunicaWrapperBase):
332 (max_loras), dtype=torch.int32, device=topk_ids.device
333 )
334
334 - (token_lora_mapping, _, _, _, _, _) = self.token_mapping_meta.meta_args(
335 + (token_lora_mapping, _, _, _, lora_ids, _) = self.token_mapping_meta.meta_args(
336 num_tokens
337 )
338
@@ -346,6 +347,8 @@ class PunicaWrapperGPU(PunicaWrapperBase):
347 sorted_ids,
348 expert_ids,
349 num_tokens_post_pad,
350 + adapter_enabled,
351 + lora_ids,
352 )
353 if expert_map is not None:
354 expert_ids = expert_map[expert_ids]
@@ -365,11 +368,13 @@ class PunicaWrapperGPU(PunicaWrapperBase):
368 max_lora_rank: int,
369 top_k_num: int,
370 config,
371 + adapter_enabled: torch.Tensor,
372 mul_routed_weight=False,
373 ):
374 """
375 Performs a fused forward computation for LoRA of Mixture-of-Experts (MoE) layer.
376 """
377 + (_, _, _, _, lora_ids, _) = self.token_mapping_meta.meta_args(x.size(0))
378 fused_moe_lora(
379 y,
380 x,
@@ -381,6 +386,8 @@ class PunicaWrapperGPU(PunicaWrapperBase):
386 num_tokens_post_padded,
387 max_lora_rank,
388 top_k_num,
389 + lora_ids,
390 + adapter_enabled,
391 config["BLOCK_SIZE_M"],
392 config["BLOCK_SIZE_N"],
393 config["BLOCK_SIZE_K"],