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 12, 23:30 UTC
add support for --fully-sharded-loras in fused_moe (#28761)
Signed-off-by: gnovack <gnovack@amazon.com> Co-authored-by: Jee Jee Li <pandaleefree@gmail.com>
gnovack committed
Nov 19, 2025 at 00:32 UTC
d69062c67af46a2e624be92162e9db585eef329b
6 files changed
+274
-10
tests/lora/test_fused_moe_lora_kernel.py
+206
-2
@@ -1,13 +1,25 @@
1
# SPDX-License-Identifier: Apache-2.0
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+import os
4
import random
5
6
import pytest
7
import torch
8
9
+from tests.utils import multi_gpu_test
10
from vllm import _custom_ops as ops
11
+from vllm.distributed import (
12
+ init_distributed_environment,
13
+ initialize_model_parallel,
14
+ tensor_model_parallel_all_gather,
15
+ tensor_model_parallel_all_reduce,
16
+)
17
+from vllm.distributed.parallel_state import (
18
+ get_tensor_model_parallel_world_size,
19
+)
20
from vllm.lora.ops.triton_ops import fused_moe_lora
21
from vllm.platforms import current_platform
22
+from vllm.utils.network_utils import get_open_port
23
24
25
@pytest.fixture(autouse=True)
@@ -122,6 +134,8 @@ def use_fused_moe_lora_kernel(
134
max_loras,
135
num_experts,
136
block_size,
137
+ fully_sharded=False,
138
+ offset=0,
139
):
140
max_num_tokens_padded = topk_ids.numel() + num_experts * (block_size - 1)
141
max_num_tokens_padded = round_up(max_num_tokens_padded, block_size)
@@ -195,10 +209,10 @@ def use_fused_moe_lora_kernel(
209
config["NUM_STAGES"],
210
config["SPLIT_K"],
211
mul_routed_weight,
212
+ fully_sharded=fully_sharded,
213
+ offset=offset,
214
)
215
200
- return output
201
-
216
217
def use_torch(
218
hidden_states,
@@ -317,3 +331,193 @@ def test_fused_moe_lora_kernel(
331
)
332
333
torch.testing.assert_close(output, output2, atol=1e-1, rtol=1e-1)
334
+
335
+
336
+@multi_gpu_test(num_gpus=2)
337
+@pytest.mark.parametrize("num_tokens", [100])
338
+@pytest.mark.parametrize("top_k_num", [6])
339
+@pytest.mark.parametrize("num_experts", [64])
340
+@pytest.mark.parametrize("max_loras", [4])
341
+@pytest.mark.parametrize("N", [1408])
342
+@pytest.mark.parametrize("K", [2048])
343
+@pytest.mark.parametrize("max_lora_rank", [16, 32, 64])
344
+@pytest.mark.parametrize("block_size", [16])
345
+@pytest.mark.parametrize("dtype", DTYPES)
346
+@pytest.mark.parametrize("seed", SEED)
347
+@pytest.mark.parametrize("column_parallel", [True, False])
348
+def test_fused_moe_lora_kernel_fully_sharded(
349
+ num_tokens,
350
+ top_k_num,
351
+ num_experts,
352
+ max_loras,
353
+ N,
354
+ K,
355
+ max_lora_rank,
356
+ block_size,
357
+ dtype,
358
+ seed,
359
+ column_parallel,
360
+):
361
+ current_platform.seed_everything(seed)
362
+ # the number of randomly generated sentences.
363
+ num_sequences = 10
364
+ # generate data
365
+ topk_ids, topk_weights, token_lora_mapping = sample_data(
366
+ num_tokens, num_sequences, max_loras, num_experts, top_k_num
367
+ )
368
+
369
+ def run_torch_spawn(fn, nprocs):
370
+ torch.multiprocessing.spawn(
371
+ fn,
372
+ args=(
373
+ nprocs,
374
+ f"tcp://{os.getenv('LOCALHOST', 'localhost')}:{get_open_port()}",
375
+ dtype,
376
+ seed,
377
+ N,
378
+ K,
379
+ num_tokens,
380
+ topk_ids,
381
+ topk_weights,
382
+ token_lora_mapping,
383
+ max_lora_rank,
384
+ top_k_num,
385
+ max_loras,
386
+ num_experts,
387
+ block_size,
388
+ column_parallel,
389
+ ),
390
+ nprocs=nprocs,
391
+ )
392
+
393
+ run_torch_spawn(use_fused_moe_lora_kernel_tensor_parallel, nprocs=2)
394
+
395
+
396
+def use_fused_moe_lora_kernel_tensor_parallel(
397
+ local_rank,
398
+ world_size,
399
+ init_method,
400
+ dtype,
401
+ seed,
402
+ N,
403
+ K,
404
+ num_tokens,
405
+ topk_ids,
406
+ topk_weights,
407
+ token_lora_mapping,
408
+ max_lora_rank,
409
+ top_k_num,
410
+ max_loras,
411
+ num_experts,
412
+ block_size,
413
+ column_parallel,
414
+):
415
+ def _get_shard_slice(shard_size):
416
+ return slice(local_rank * shard_size, (local_rank + 1) * shard_size)
417
+
418
+ current_platform.seed_everything(seed)
419
+
420
+ device = torch.device(f"cuda:{local_rank}")
421
+ torch.cuda.set_device(device)
422
+ torch.set_default_device(device)
423
+ torch.set_default_dtype(dtype)
424
+
425
+ init_distributed_environment(
426
+ world_size=world_size,
427
+ rank=local_rank,
428
+ local_rank=local_rank,
429
+ distributed_init_method=init_method,
430
+ )
431
+ initialize_model_parallel(world_size, 1)
432
+ tp_size = get_tensor_model_parallel_world_size()
433
+
434
+ input_dim = K if column_parallel else N
435
+ output_dim = N if column_parallel else K
436
+
437
+ # init lora weights
438
+ lora_a = torch.rand(
439
+ (
440
+ max_loras,
441
+ num_experts,
442
+ max_lora_rank,
443
+ input_dim,
444
+ ),
445
+ dtype=dtype,
446
+ )
447
+ lora_b = torch.rand(
448
+ (
449
+ max_loras,
450
+ num_experts,
451
+ output_dim,
452
+ max_lora_rank,
453
+ ),
454
+ dtype=dtype,
455
+ )
456
+
457
+ hidden_states = torch.rand(
458
+ (
459
+ num_tokens,
460
+ input_dim,
461
+ ),
462
+ dtype=dtype,
463
+ )
464
+
465
+ output = torch.zeros((num_tokens, top_k_num, output_dim), dtype=dtype)
466
+ topk_ids = topk_ids.to(device)
467
+ topk_weights = topk_weights.to(device)
468
+ token_lora_mapping = token_lora_mapping.to(device)
469
+
470
+ ref_output = use_torch(
471
+ hidden_states,
472
+ token_lora_mapping,
473
+ topk_ids,
474
+ [lora_a],
475
+ [lora_b],
476
+ top_k_num,
477
+ )
478
+
479
+ if column_parallel:
480
+ # Column parallel (e.g. gate_up_proj): LoRA A is sliced along the rank dim,
481
+ # and Lora B is sliced along the output dim
482
+ lora_a_shard_size = max_lora_rank // tp_size
483
+ lora_a = lora_a[:, :, _get_shard_slice(lora_a_shard_size), :]
484
+ max_lora_rank = lora_a_shard_size
485
+ offset = 0
486
+
487
+ lora_b_shard_size = output_dim // tp_size
488
+ lora_b = lora_b[:, :, _get_shard_slice(lora_b_shard_size), :]
489
+ output = output[:, :, _get_shard_slice(lora_b_shard_size)].contiguous()
490
+ else:
491
+ # Row parallel (e.g. down proj): LoRA A is sliced along the input dim,
492
+ # and LoRA B is sliced along the output dim
493
+ lora_a_shard_size = input_dim // tp_size
494
+ lora_a = lora_a[:, :, :, _get_shard_slice(lora_a_shard_size)]
495
+ hidden_states = hidden_states[:, _get_shard_slice(lora_a_shard_size)]
496
+
497
+ lora_b_shard_size = output_dim // tp_size
498
+ lora_b = lora_b[:, :, _get_shard_slice(lora_b_shard_size), :]
499
+ offset = lora_b_shard_size * local_rank
500
+
501
+ use_fused_moe_lora_kernel(
502
+ topk_ids,
503
+ topk_weights,
504
+ token_lora_mapping,
505
+ max_lora_rank,
506
+ top_k_num,
507
+ [lora_a],
508
+ [lora_b],
509
+ hidden_states,
510
+ output,
511
+ max_loras,
512
+ num_experts,
513
+ block_size,
514
+ fully_sharded=True,
515
+ offset=offset,
516
+ )
517
+
518
+ if column_parallel:
519
+ output = tensor_model_parallel_all_gather(output)
520
+ else:
521
+ output = tensor_model_parallel_all_reduce(output)
522
+
523
+ torch.testing.assert_close(output, ref_output, atol=1e-1, rtol=1e-1)
tests/lora/test_olmoe_tp.py
+8
-2
@@ -2,6 +2,8 @@
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4
5
+import pytest
6
+
7
import vllm
8
from vllm.lora.request import LoRARequest
9
@@ -111,8 +113,9 @@ def test_olmoe_lora_mixed(olmoe_lora_files):
113
generate_and_test(llm, olmoe_lora_files, lora_id=[1, None, 3, None])
114
115
116
+@pytest.mark.parametrize("fully_sharded_loras", [False, True])
117
@multi_gpu_test(num_gpus=2)
115
-def test_olmoe_lora_tp2(olmoe_lora_files):
118
+def test_olmoe_lora_tp2(olmoe_lora_files, fully_sharded_loras):
119
llm = vllm.LLM(
120
MODEL_PATH,
121
max_model_len=1024,
@@ -122,14 +125,16 @@ def test_olmoe_lora_tp2(olmoe_lora_files):
125
trust_remote_code=True,
126
enable_chunked_prefill=True,
127
tensor_parallel_size=2,
128
+ fully_sharded_loras=fully_sharded_loras,
129
)
130
131
generate_and_test(llm, olmoe_lora_files, lora_id=1)
132
generate_and_test(llm, olmoe_lora_files, lora_id=2)
133
134
135
+@pytest.mark.parametrize("fully_sharded_loras", [False, True])
136
@multi_gpu_test(num_gpus=4)
132
-def test_olmoe_lora_tp4(olmoe_lora_files):
137
+def test_olmoe_lora_tp4(olmoe_lora_files, fully_sharded_loras):
138
llm = vllm.LLM(
139
MODEL_PATH,
140
max_model_len=1024,
@@ -139,6 +144,7 @@ def test_olmoe_lora_tp4(olmoe_lora_files):
144
trust_remote_code=True,
145
enable_chunked_prefill=True,
146
tensor_parallel_size=4,
147
+ fully_sharded_loras=fully_sharded_loras,
148
)
149
150
generate_and_test(llm, olmoe_lora_files, lora_id=1)
vllm/lora/layers/fused_moe.py
+32
-4
@@ -12,6 +12,7 @@ from vllm.distributed.parallel_state import (
12
get_tensor_model_parallel_rank,
13
get_tensor_model_parallel_world_size,
14
)
15
+from vllm.distributed.utils import divide
16
from vllm.lora.layers.base import BaseLayerWithLoRA
17
from vllm.lora.ops.triton_ops.utils import get_lora_op_configs
18
from vllm.model_executor.layers.fused_moe import FusedMoE
@@ -205,6 +206,7 @@ class FusedMoEWithLoRA(BaseLayerWithLoRA):
206
shrink_config, ## pass the shrink config
207
expand_config, ## pass the expand config
208
self.adapter_enabled,
209
+ fully_sharded=self.fully_sharded,
210
)
211
212
result = func(*args, **kwargs)
@@ -250,7 +252,10 @@ class FusedMoEWithLoRA(BaseLayerWithLoRA):
252
sorted_token_ids_lora = sorted_token_ids_lora.view(max_loras, -1)
253
intermediate_cache2 = moe_state_dict["intermediate_cache2"]
254
intermediate_cache3 = args[0]
253
- max_lora_rank = self.w1_lora_a_stacked.shape[-2]
255
+ max_lora_rank = self.w2_lora_a_stacked.shape[-2]
256
+
257
+ shard_size_w2 = divide(self.base_layer.hidden_size, self.tp_size)
258
+
259
self.punica_wrapper.add_lora_fused_moe(
260
intermediate_cache3,
261
intermediate_cache2,
@@ -266,6 +271,8 @@ class FusedMoEWithLoRA(BaseLayerWithLoRA):
271
expand_config, ## pass the expand config
272
self.adapter_enabled,
273
True,
274
+ fully_sharded=self.fully_sharded,
275
+ offset=shard_size_w2 * self.tp_rank if self.fully_sharded else 0,
276
)
277
278
result = func(*args, **kwargs)
@@ -294,6 +301,7 @@ class FusedMoEWithLoRA(BaseLayerWithLoRA):
301
model_config: PretrainedConfig | None = None,
302
) -> None:
303
"""Initializes lora matrices."""
304
+ self.fully_sharded = lora_config.fully_sharded_loras
305
306
self.adapter_enabled = torch.tensor(
307
[0] * (max_loras + 1), dtype=torch.int, device=self.device
@@ -303,7 +311,9 @@ class FusedMoEWithLoRA(BaseLayerWithLoRA):
311
(
312
max_loras,
313
self.base_layer.local_num_experts,
306
- lora_config.max_lora_rank,
314
+ lora_config.max_lora_rank
315
+ if not self.fully_sharded
316
+ else divide(lora_config.max_lora_rank, self.tp_size),
317
self.base_layer.hidden_size,
318
),
319
dtype=lora_config.lora_dtype,
@@ -334,7 +344,9 @@ class FusedMoEWithLoRA(BaseLayerWithLoRA):
344
(
345
max_loras,
346
self.base_layer.local_num_experts,
337
- self.base_layer.hidden_size,
347
+ self.base_layer.hidden_size
348
+ if not self.fully_sharded
349
+ else divide(self.base_layer.hidden_size, self.tp_size),
350
lora_config.max_lora_rank,
351
),
352
dtype=lora_config.lora_dtype,
@@ -345,7 +357,9 @@ class FusedMoEWithLoRA(BaseLayerWithLoRA):
357
(
358
max_loras,
359
self.base_layer.local_num_experts,
348
- lora_config.max_lora_rank,
360
+ lora_config.max_lora_rank
361
+ if not self.fully_sharded
362
+ else divide(lora_config.max_lora_rank, self.tp_size),
363
self.base_layer.hidden_size,
364
),
365
dtype=lora_config.lora_dtype,
@@ -419,6 +433,20 @@ class FusedMoEWithLoRA(BaseLayerWithLoRA):
433
w3_lora_b = w3_lora_b[start_idx:end_idx, :]
434
w2_lora_a = w2_lora_a[:, start_idx:end_idx]
435
436
+ if self.fully_sharded:
437
+ # Based on S-LoRA, we slice W1 and W3 A along the rank dim,
438
+ # and W2 B along the hidden_size dim.
439
+ w13_shard_size = self.w1_lora_a_stacked[index, eid].shape[0]
440
+ w13_start_idx = self.tp_rank * w13_shard_size
441
+ w13_end_idx = (self.tp_rank + 1) * w13_shard_size
442
+ w1_lora_a = w1_lora_a[w13_start_idx:w13_end_idx, :]
443
+ w3_lora_a = w3_lora_a[w13_start_idx:w13_end_idx, :]
444
+
445
+ w2_shard_size = self.w2_lora_b_stacked[index, eid].shape[0]
446
+ w2_start_idx = self.tp_rank * w2_shard_size
447
+ w2_end_idx = (self.tp_rank + 1) * w2_shard_size
448
+ w2_lora_b = w2_lora_b[w2_start_idx:w2_end_idx, :]
449
+
450
self.w1_lora_a_stacked[
451
index, eid, : w1_lora_a.shape[0], : w1_lora_a.shape[1]
452
].copy_(w1_lora_a, non_blocking=True)
vllm/lora/ops/triton_ops/fused_moe_lora_op.py
+22
-2
@@ -3,6 +3,10 @@
3
4
import torch
5
6
+from vllm.distributed import (
7
+ tensor_model_parallel_all_gather,
8
+ tensor_model_parallel_all_reduce,
9
+)
10
from vllm.triton_utils import tl, triton
11
from vllm.utils.torch_utils import direct_register_custom_op
12
@@ -311,6 +315,7 @@ def _fused_moe_lora_expand(
315
num_stages: int,
316
split_k: int,
317
mul_routed_weight: bool = False,
318
+ offset: int = 0,
319
) -> None:
320
b_ptr = _get_ptr(lora_b_stacked, device)
321
K = max_lora_rank
@@ -380,7 +385,7 @@ def _fused_moe_lora_expand(
385
**expand_config,
386
)
387
for i in range(num_slices):
383
- output[:, :, i * N : (i + 1) * N] += b_intermediate_cache1[i]
388
+ output[:, :, i * N + offset : (i + 1) * N + offset] += b_intermediate_cache1[i]
389
390
391
@torch.inference_mode()
@@ -416,6 +421,8 @@ def _fused_moe_lora(
421
expand_num_stages: int,
422
expand_split_k: int,
423
mul_routed_weight: bool = False,
424
+ fully_sharded: bool = False,
425
+ offset: int = 0,
426
) -> None:
427
assert len(lora_a_stacked) == len(lora_b_stacked) > 0
428
assert (
@@ -430,7 +437,6 @@ def _fused_moe_lora(
437
== expert_ids.shape[0]
438
== num_tokens_post_padded.shape[0]
439
)
433
- assert len(lora_b_stacked) * lora_b_stacked[0].shape[-2] == output.shape[-1]
440
assert output.shape[0] == topk_weights.shape[0]
441
assert top_k_num == topk_weights.shape[1]
442
device = qcurr_hidden_states.device
@@ -480,6 +486,19 @@ def _fused_moe_lora(
486
mul_routed_weight,
487
)
488
489
+ if fully_sharded:
490
+ if max_lora_rank == w1_lora_b_stacked.shape[-1]:
491
+ a_intermediate_cache1 = tensor_model_parallel_all_reduce(
492
+ a_intermediate_cache1
493
+ )
494
+ else:
495
+ a_intermediate_cache1 = tensor_model_parallel_all_gather(
496
+ a_intermediate_cache1
497
+ )
498
+
499
+ # reset max_lora_rank to the full rank after allgather
500
+ max_lora_rank = a_intermediate_cache1.shape[-1]
501
+
502
_fused_moe_lora_expand(
503
output,
504
a_intermediate_cache1,
@@ -510,6 +529,7 @@ def _fused_moe_lora(
529
expand_num_stages,
530
expand_split_k,
531
mul_routed_weight,
532
+ offset,
533
)
534
535
vllm/lora/punica_wrapper/punica_base.py
+2
@@ -483,6 +483,8 @@ class PunicaWrapperBase(PunicaWrapperABC):
483
expand_config,
484
adapter_enabled: torch.Tensor,
485
mul_routed_weight=False,
486
+ fully_sharded: bool = False,
487
+ offset: int = 0,
488
):
489
"""
490
Performs a fused forward computation for LoRA of
vllm/lora/punica_wrapper/punica_gpu.py
+4
@@ -375,6 +375,8 @@ class PunicaWrapperGPU(PunicaWrapperBase):
375
expand_config,
376
adapter_enabled: torch.Tensor,
377
mul_routed_weight=False,
378
+ fully_sharded: bool = False,
379
+ offset: int = 0,
380
):
381
"""
382
Performs a fused forward computation for LoRA of Mixture-of-Experts (MoE) layer.
@@ -408,4 +410,6 @@ class PunicaWrapperGPU(PunicaWrapperBase):
410
expand_config.get("NUM_STAGES", 3),
411
expand_config.get("SPLIT_K", 1),
412
mul_routed_weight,
413
+ fully_sharded,
414
+ offset,
415
)