[Bugfix][MoE] Convert FlashInfer BF16 weights in place (#54699)
Signed-off-by: Yuchen Wang <93700456+yuchenwang3@users.noreply.github.com> Signed-off-by: Dakai An <dakaian108@gmail.com> Co-authored-by: Dakai An <dakaian108@gmail.com> Co-authored-by: OpenAI Codex <codex@openai.com> Co-authored-by: aoshen02 <aoshen@inferact.ai>
Yuchen Wang committed
Sep 17, 2026 at 07:00 UTC
78fdf4efb86dffde1b0f5733207aed038c3530f8
9 files changed
+667
-68
tests/kernels/moe/test_flashinfer.py
+258
@@ -1,5 +1,6 @@
1
# SPDX-License-Identifier: Apache-2.0
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+import multiprocessing
4
from dataclasses import dataclass
5
6
import pytest
@@ -433,6 +434,263 @@ def test_convert_moe_weights_to_flashinfer_trtllm_block_layout(
434
435
assert w13_converted.shape[0] == num_experts
436
assert w2_converted.shape[0] == num_experts
437
+ assert w13_converted.data_ptr() == w13.data_ptr()
438
+ assert w2_converted.data_ptr() == w2.data_ptr()
439
+
440
+
441
+@pytest.mark.parametrize("is_gated_act_gemm", [True, False])
442
+def test_convert_moe_weights_to_flashinfer_trtllm_block_layout_values(
443
+ is_gated_act_gemm,
444
+):
445
+ from flashinfer.fused_moe.core import (
446
+ _maybe_get_cached_w3_w1_permute_indices,
447
+ get_w2_permute_indices_with_cache,
448
+ )
449
+
450
+ from vllm.model_executor.layers.quantization.utils.flashinfer_utils import (
451
+ convert_moe_weights_to_flashinfer_trtllm_block_layout,
452
+ )
453
+
454
+ num_experts, intermediate, hidden = 2, 256, 256
455
+ w13_multiplier = 2 if is_gated_act_gemm else 1
456
+ w13 = torch.randn(
457
+ (num_experts, w13_multiplier * intermediate, hidden),
458
+ dtype=torch.bfloat16,
459
+ device="cuda",
460
+ )
461
+ w2 = torch.randn(
462
+ (num_experts, hidden, intermediate),
463
+ dtype=torch.bfloat16,
464
+ device="cuda",
465
+ )
466
+
467
+ def _reference_block_layout(
468
+ weight: torch.Tensor,
469
+ is_w13: bool,
470
+ cache: dict[torch.Size, torch.Tensor],
471
+ ) -> torch.Tensor:
472
+ outputs = []
473
+ for expert in weight:
474
+ expert_uint8 = expert.view(torch.uint8)
475
+ if is_w13:
476
+ indices = _maybe_get_cached_w3_w1_permute_indices(
477
+ cache,
478
+ expert_uint8,
479
+ 128,
480
+ is_gated_act_gemm=is_gated_act_gemm,
481
+ )
482
+ if is_gated_act_gemm:
483
+ indices = (indices + expert_uint8.shape[0] // 2) % (
484
+ expert_uint8.shape[0]
485
+ )
486
+ else:
487
+ indices = get_w2_permute_indices_with_cache(
488
+ cache,
489
+ expert_uint8,
490
+ 128,
491
+ )
492
+ rows, cols = expert_uint8.shape
493
+ blocks = expert_uint8.view(rows, cols // 128, 128).permute(1, 0, 2)
494
+ outputs.append(torch.index_select(blocks, 1, indices.to(weight.device)))
495
+ return torch.stack(outputs).view(torch.bfloat16)
496
+
497
+ reference_cache: dict[torch.Size, torch.Tensor] = {}
498
+ expected_w13 = _reference_block_layout(w13, is_w13=True, cache=reference_cache)
499
+ expected_w2 = _reference_block_layout(w2, is_w13=False, cache=reference_cache)
500
+ w13_ptr = w13.data_ptr()
501
+ w2_ptr = w2.data_ptr()
502
+
503
+ actual_w13, actual_w2 = convert_moe_weights_to_flashinfer_trtllm_block_layout(
504
+ {},
505
+ w13,
506
+ w2,
507
+ is_gated_act_gemm=is_gated_act_gemm,
508
+ )
509
+
510
+ assert actual_w13.data_ptr() == w13_ptr
511
+ assert actual_w2.data_ptr() == w2_ptr
512
+ assert torch.equal(actual_w13, expected_w13)
513
+ assert torch.equal(actual_w2, expected_w2)
514
+
515
+
516
+def _make_unquantized_flashinfer_test_layer(
517
+ monkeypatch, intermediate, is_gated, *, device="cuda"
518
+):
519
+ from vllm.model_executor.layers.fused_moe.oracle.unquantized import (
520
+ UnquantizedMoeBackend,
521
+ )
522
+ from vllm.model_executor.layers.fused_moe.unquantized_fused_moe_method import (
523
+ UnquantizedFusedMoEMethod,
524
+ )
525
+
526
+ moe_config = make_dummy_moe_config(
527
+ num_experts=2,
528
+ hidden_dim=256,
529
+ intermediate_size=intermediate,
530
+ activation=MoEActivation.SILU if is_gated else MoEActivation.RELU2_NO_MUL,
531
+ )
532
+ moe_config.intermediate_size_per_partition = (intermediate + 127) // 128 * 128
533
+ method = object.__new__(UnquantizedFusedMoEMethod)
534
+ method.moe = moe_config
535
+ method.unquantized_backend = UnquantizedMoeBackend.FLASHINFER_TRTLLM
536
+ method.moe_kernel = None
537
+ monkeypatch.setattr(
538
+ method,
539
+ "_init_moe_kernel",
540
+ lambda _: setattr(method, "moe_kernel", object()),
541
+ )
542
+
543
+ layer = torch.nn.Module()
544
+ layer.moe_config = moe_config
545
+ with torch.device(device):
546
+ method.create_weights(
547
+ layer,
548
+ num_experts=2,
549
+ hidden_size=256,
550
+ intermediate_size_per_partition=moe_config.intermediate_size_per_partition,
551
+ params_dtype=torch.bfloat16,
552
+ )
553
+ return method, layer
554
+
555
+
556
+@pytest.mark.parametrize("intermediate", [192, 256])
557
+@pytest.mark.parametrize("is_gated", [True, False])
558
+def test_unquantized_flashinfer_trtllm_weights_can_be_reprocessed(
559
+ monkeypatch, intermediate, is_gated
560
+):
561
+ """Each raw reload clears padding left dirty by prior in-place conversion."""
562
+ from vllm.model_executor.layers.quantization.utils.flashinfer_utils import (
563
+ convert_moe_weights_to_flashinfer_trtllm_block_layout,
564
+ )
565
+
566
+ method, layer = _make_unquantized_flashinfer_test_layer(
567
+ monkeypatch, intermediate, is_gated
568
+ )
569
+ w13_shape, w2_shape = layer.w13_weight.shape, layer.w2_weight.shape
570
+ padded = w2_shape[-1]
571
+ w13_ptr = layer.w13_weight.data_ptr()
572
+ w2_ptr = layer.w2_weight.data_ptr()
573
+
574
+ for _ in range(2):
575
+ reloaded_w13 = torch.randn_like(layer.w13_weight)
576
+ reloaded_w2 = torch.randn_like(layer.w2_weight)
577
+ reloaded_w13[:, intermediate:padded].zero_()
578
+ if is_gated:
579
+ reloaded_w13[:, padded + intermediate :].zero_()
580
+ reloaded_w2[:, :, intermediate:].zero_()
581
+ expected_w13, expected_w2 = (
582
+ convert_moe_weights_to_flashinfer_trtllm_block_layout(
583
+ {},
584
+ reloaded_w13.clone(),
585
+ reloaded_w2.clone(),
586
+ is_gated_act_gemm=is_gated,
587
+ )
588
+ )
589
+
590
+ # The loader writes only logical checkpoint slices, not padding.
591
+ layer.w13_weight.fill_(float("nan"))
592
+ layer.w2_weight.fill_(float("nan"))
593
+ layer.w13_weight[:, :intermediate].copy_(reloaded_w13[:, :intermediate])
594
+ if is_gated:
595
+ layer.w13_weight[:, padded : padded + intermediate].copy_(
596
+ reloaded_w13[:, padded : padded + intermediate]
597
+ )
598
+ layer.w2_weight[:, :, :intermediate].copy_(reloaded_w2[:, :, :intermediate])
599
+ method.process_weights_after_loading(layer)
600
+
601
+ assert layer.w13_weight.shape == w13_shape
602
+ assert layer.w2_weight.shape == w2_shape
603
+ assert layer.w13_weight.data_ptr() == w13_ptr
604
+ assert layer.w2_weight.data_ptr() == w2_ptr
605
+ kernel_w13, kernel_w2 = method._kernel_weights(layer)
606
+ assert torch.equal(kernel_w13, expected_w13)
607
+ assert torch.equal(kernel_w2, expected_w2)
608
+
609
+
610
+def _check_flashinfer_ipc_weights(entries, expected, is_gated, mode, device_index):
611
+ from vllm.model_executor.model_loader.weight_cache.ipc_loader import IpcModelLoader
612
+ from vllm.model_executor.utils import weights_already_processed
613
+
614
+ torch.accelerator.set_device_index(device_index)
615
+ with pytest.MonkeyPatch.context() as monkeypatch:
616
+ method, layer = _make_unquantized_flashinfer_test_layer(
617
+ monkeypatch, 192, is_gated, device="meta"
618
+ )
619
+ loader = object.__new__(IpcModelLoader)
620
+ loader.mode = mode
621
+ loader._apply_entries(layer, entries, {}, device_index)
622
+ pointers = (layer.w13_weight.data_ptr(), layer.w2_weight.data_ptr())
623
+
624
+ # Like ipc_cache: a fresh method, only tensor metadata, no _setup_kernel.
625
+ with weights_already_processed():
626
+ method.process_weights_after_loading(layer)
627
+ assert method.moe_kernel is not None
628
+ actual = method._kernel_weights(layer)
629
+ for weight, reference, pointer in zip(actual, expected, pointers):
630
+ assert weight.data_ptr() == pointer
631
+ assert torch.equal(weight.cpu(), reference)
632
+ # Cross-process addresses need not match; writes prove sharing/isolation.
633
+ with torch.no_grad():
634
+ for weight in actual:
635
+ weight.fill_(1.0)
636
+ torch.accelerator.synchronize()
637
+
638
+
639
+@pytest.mark.parametrize("is_gated", [True, False])
640
+@pytest.mark.parametrize("cache_ndim", [3, 4])
641
+@pytest.mark.parametrize("mode", ["copy", "zero_copy"])
642
+def test_unquantized_flashinfer_trtllm_cached_weights_need_no_method_state(
643
+ monkeypatch, is_gated, cache_ndim, mode
644
+):
645
+ """A spawned IPC consumer restores views without transient method state."""
646
+ from vllm.model_executor.layers.quantization.utils.flashinfer_utils import (
647
+ convert_moe_weights_to_flashinfer_trtllm_block_layout,
648
+ )
649
+ from vllm.model_executor.model_loader.weight_cache.protocol import TensorEntry
650
+
651
+ method, layer = _make_unquantized_flashinfer_test_layer(
652
+ monkeypatch, 192, is_gated, device="meta"
653
+ )
654
+ packed = convert_moe_weights_to_flashinfer_trtllm_block_layout(
655
+ {},
656
+ torch.randn_like(layer.w13_weight, device="cuda"),
657
+ torch.randn_like(layer.w2_weight, device="cuda"),
658
+ is_gated_act_gemm=is_gated,
659
+ )
660
+ expected = [weight.cpu() for weight in packed]
661
+ entries = {}
662
+ for name, weight in zip(("w13_weight", "w2_weight"), packed):
663
+ cached = weight.view_as(getattr(layer, name)) if cache_ndim == 3 else weight
664
+ entries[name] = TensorEntry.from_tensor(cached, kind="param")
665
+ consumer = multiprocessing.get_context("spawn").Process(
666
+ target=_check_flashinfer_ipc_weights,
667
+ args=(
668
+ entries,
669
+ expected,
670
+ is_gated,
671
+ mode,
672
+ torch.accelerator.current_device_index(),
673
+ ),
674
+ )
675
+ consumer.start()
676
+ try:
677
+ # Keep producer allocations alive until the consumer releases its views.
678
+ consumer.join(timeout=180)
679
+ assert not consumer.is_alive(), "IPC consumer timed out"
680
+ assert consumer.exitcode == 0, "IPC consumer failed; see child traceback"
681
+ for source, reference in zip(packed, expected):
682
+ expected_source = (
683
+ torch.ones_like(reference) if mode == "zero_copy" else reference
684
+ )
685
+ assert torch.equal(source.cpu(), expected_source)
686
+ finally:
687
+ if consumer.is_alive():
688
+ consumer.terminate()
689
+ consumer.join(timeout=10)
690
+ if consumer.is_alive():
691
+ consumer.kill()
692
+ consumer.join(timeout=10)
693
+ consumer.close()
694
695
696
@pytest.mark.parametrize(
tests/kernels/moe/test_moe_weight_loading_padded.py
+126
@@ -12,7 +12,14 @@ correctly handles this mismatch.
12
import pytest
13
import torch
14
15
+from vllm.model_executor.layers.fused_moe.activation import MoEActivation
16
+from vllm.model_executor.layers.fused_moe.oracle.unquantized import (
17
+ UnquantizedMoeBackend,
18
+)
19
from vllm.model_executor.layers.fused_moe.routed_experts import RoutedExperts
20
+from vllm.model_executor.layers.fused_moe.unquantized_fused_moe_method import (
21
+ UnquantizedFusedMoEMethod,
22
+)
23
24
from .utils import make_dummy_moe_config
25
@@ -249,6 +256,57 @@ class TestNarrowExpertDataForPadding:
256
class TestWeightLoadingWithPaddedHiddenSize:
257
"""Integration-style tests that simulate padded weight loading."""
258
259
+ @pytest.mark.skipif(not torch.cuda.is_available(), reason="requires CUDA")
260
+ @pytest.mark.parametrize("dtype", [torch.bfloat16, torch.float32])
261
+ @pytest.mark.parametrize("tp_rank", [0, 1])
262
+ def test_load_w2_chunks_noncontiguous_cpu_source_to_padded_cuda(
263
+ self, dtype, tp_rank
264
+ ):
265
+ hidden = 4096
266
+ intermediate = 1024
267
+ loaded_weight = (
268
+ torch.arange(hidden * intermediate * 2)
269
+ .remainder(251)
270
+ .to(dtype)
271
+ .reshape(hidden, intermediate * 2)
272
+ )
273
+ tp_source = loaded_weight[
274
+ :, intermediate * tp_rank : intermediate * (tp_rank + 1)
275
+ ]
276
+ expert_data_full = torch.zeros(
277
+ hidden + 8, intermediate + 8, device="cuda", dtype=dtype
278
+ )
279
+ destination = expert_data_full[:hidden, :intermediate]
280
+
281
+ assert not tp_source.is_contiguous()
282
+ assert tp_source.nbytes > 1 << 20
283
+ assert not destination.is_contiguous()
284
+
285
+ experts = object.__new__(RoutedExperts)
286
+ torch.nn.Module.__init__(experts)
287
+ experts.moe_config = make_dummy_moe_config()
288
+ experts.moe_config.moe_parallel_config.tp_size = 2
289
+
290
+ torch.accelerator.synchronize()
291
+ allocated_before = torch.accelerator.memory_allocated()
292
+ torch.accelerator.reset_peak_memory_stats()
293
+ experts._load_w2(
294
+ expert_data=expert_data_full,
295
+ shard_dim=1,
296
+ loaded_weight=loaded_weight,
297
+ tp_rank=tp_rank,
298
+ )
299
+ torch.accelerator.synchronize()
300
+ peak_extra = torch.accelerator.max_memory_allocated() - allocated_before
301
+
302
+ # A single copy into the strided CUDA view allocates a full-shard
303
+ # temporary. Correct values alone would not catch that regression.
304
+ assert peak_extra < tp_source.nbytes
305
+
306
+ torch.testing.assert_close(destination.cpu(), tp_source)
307
+ assert torch.count_nonzero(expert_data_full[hidden:, :]) == 0
308
+ assert torch.count_nonzero(expert_data_full[:hidden, intermediate:]) == 0
309
+
310
def test_load_w2_with_padding(self):
311
"""Simulate loading w2 weights when hidden_size is padded."""
312
padded_hidden = 3072
@@ -364,6 +422,74 @@ class TestWeightLoadingWithPaddedHiddenSize:
422
)
423
424
425
+class TestUnquantizedTrtLlmPrePadding:
426
+ @staticmethod
427
+ def _make_method(
428
+ intermediate: int,
429
+ backend: UnquantizedMoeBackend = UnquantizedMoeBackend.FLASHINFER_TRTLLM,
430
+ ) -> UnquantizedFusedMoEMethod:
431
+ moe_config = make_dummy_moe_config(
432
+ num_experts=2,
433
+ hidden_dim=64,
434
+ intermediate_size=intermediate,
435
+ )
436
+ method = object.__new__(UnquantizedFusedMoEMethod)
437
+ method.moe = moe_config
438
+ method.unquantized_backend = backend
439
+ method.moe_kernel = None
440
+ return method
441
+
442
+ @pytest.mark.parametrize(
443
+ "backend,original,expected",
444
+ [
445
+ (UnquantizedMoeBackend.FLASHINFER_TRTLLM, 1344, 1408),
446
+ (UnquantizedMoeBackend.FLASHINFER_TRTLLM, 1408, 1408),
447
+ (UnquantizedMoeBackend.TRITON, 1344, 1344),
448
+ ],
449
+ )
450
+ def test_rounds_intermediate_before_weight_allocation(
451
+ self,
452
+ backend: UnquantizedMoeBackend,
453
+ original: int,
454
+ expected: int,
455
+ ):
456
+ method = self._make_method(original, backend)
457
+
458
+ hidden, intermediate = method.maybe_roundup_sizes(
459
+ hidden_size=64,
460
+ intermediate_size_per_partition=original,
461
+ act_dtype=torch.bfloat16,
462
+ moe_parallel_config=method.moe.moe_parallel_config,
463
+ )
464
+
465
+ assert hidden == 64
466
+ assert intermediate == expected
467
+
468
+ @pytest.mark.parametrize("is_act_and_mul", [True, False])
469
+ def test_create_weights_zeroes_preallocated_padding(self, is_act_and_mul: bool):
470
+ method = self._make_method(1344)
471
+ if not is_act_and_mul:
472
+ method.moe.activation = MoEActivation.RELU2_NO_MUL
473
+ method.moe.intermediate_size_per_partition = 1408
474
+ layer = torch.nn.Module()
475
+
476
+ method.create_weights(
477
+ layer=layer,
478
+ num_experts=2,
479
+ hidden_size=64,
480
+ intermediate_size_per_partition=1408,
481
+ params_dtype=torch.bfloat16,
482
+ )
483
+
484
+ up_mult = 2 if is_act_and_mul else 1
485
+ assert layer.w13_weight.shape == (2, up_mult * 1408, 64)
486
+ assert layer.w2_weight.shape == (2, 64, 1408)
487
+ assert torch.count_nonzero(layer.w13_weight[:, 1344:1408]) == 0
488
+ if is_act_and_mul:
489
+ assert torch.count_nonzero(layer.w13_weight[:, 2752:]) == 0
490
+ assert torch.count_nonzero(layer.w2_weight[:, :, 1344:]) == 0
491
+
492
+
493
class TestLoadWeightsExpertBias:
494
"""Some quantized exports (e.g. GPTQ, llm-compressor NVFP4) materialize
495
all-zero per-expert `.bias` tensors for models whose experts have no bias
tests/kernels/moe/test_trtllm_bf16_moe.py
+6
-2
@@ -75,6 +75,10 @@ def test_trtllm_bf16_moe_modular_no_graph(
75
a = torch.randn((m, k), device="cuda", dtype=dtype) / 10
76
w1 = torch.randn((e, 2 * n, k), device="cuda", dtype=dtype) / 10
77
w2 = torch.randn((e, k, n), device="cuda", dtype=dtype) / 10
78
+ # The FlashInfer conversion may rewrite unpadded input storage in place.
79
+ # Preserve the original layout for the independent torch reference.
80
+ reference_w1 = w1.clone()
81
+ reference_w2 = w2.clone()
82
score = torch.randn((m, e), device="cuda", dtype=dtype)
83
scores = torch.softmax(score, dim=-1, dtype=torch.float32)
84
topk_weights, topk_ids = torch.topk(scores, topk)
@@ -133,8 +137,8 @@ def test_trtllm_bf16_moe_modular_no_graph(
137
138
torch_output = torch_moe(
139
a,
136
- w1,
137
- w2,
140
+ reference_w1,
141
+ reference_w2,
142
score,
143
topk,
144
activation=MoEActivation.SILU,
tests/model_executor/model_loader/test_reload.py
+105
@@ -5,6 +5,7 @@ import importlib.machinery
5
import inspect
6
import sys
7
import types
8
+from types import SimpleNamespace
9
from unittest.mock import Mock
10
from weakref import WeakKeyDictionary, ref
11
@@ -467,6 +468,110 @@ def test_model_cleanup(dist_init, default_vllm_config):
468
assert len(mock_info_dict) == 0
469
470
471
+@pytest.mark.parametrize("is_gated", [False, True])
472
+@pytest.mark.parametrize("has_bias", [False, True])
473
+@pytest.mark.parametrize("tp_rank", [0, 1])
474
+@pytest.mark.parametrize("padded", [False, True])
475
+def test_padded_moe_reload_releases_each_layer(
476
+ monkeypatch, is_gated, has_bias, tp_rank, padded
477
+):
478
+ """Checkpoint-sized copies finish each layer without global finalization."""
479
+ from vllm.model_executor.layers.fused_moe.routed_experts import RoutedExperts
480
+ from vllm.model_executor.layers.fused_moe.unquantized_fused_moe_method import (
481
+ UnquantizedFusedMoEMethod,
482
+ )
483
+
484
+ hidden, intermediate, experts = 4, 3, 2
485
+ stored_hidden, stored_intermediate = (8, 8) if padded else (hidden, intermediate)
486
+ config = SimpleNamespace(
487
+ hidden_dim_unpadded=hidden,
488
+ intermediate_size_per_partition_unpadded=intermediate,
489
+ is_act_and_mul=is_gated,
490
+ has_bias=has_bias,
491
+ tp_rank=tp_rank,
492
+ moe_parallel_config=SimpleNamespace(tp_size=2),
493
+ )
494
+ model = torch.nn.ModuleList()
495
+ processed: list[torch.nn.Module] = []
496
+ for _ in range(2):
497
+ method = object.__new__(UnquantizedFusedMoEMethod)
498
+ torch.nn.Module.__init__(method)
499
+ method.moe = config
500
+ # The regression concerns streaming reload, not kernel conversion.
501
+ monkeypatch.setattr(method, "process_weights_after_loading", processed.append)
502
+ layer = object.__new__(RoutedExperts)
503
+ torch.nn.Module.__init__(layer)
504
+ layer.moe_config = config
505
+ layer.quant_config = None
506
+ layer.quant_method = method
507
+ layer.expert_map_manager = SimpleNamespace(map_global_to_local=lambda i: i)
508
+ layer._loaded_expert_biases = set()
509
+ method.create_weights(
510
+ layer,
511
+ experts,
512
+ stored_hidden,
513
+ stored_intermediate,
514
+ torch.float32,
515
+ weight_loader=layer.weight_loader,
516
+ )
517
+ model.append(layer)
518
+
519
+ record_metadata_for_reloading(model)
520
+ original_params = [dict(layer.named_parameters()) for layer in model]
521
+ shards = ["w1", "w3", "w2"] if is_gated else ["w1", "w2"]
522
+ for cycle in range(2):
523
+ initialize_layerwise_reload(model)
524
+ for layer_index, layer in enumerate(model):
525
+ info = reload_layerwise.get_layerwise_info(layer)
526
+ inputs = []
527
+ expected = {
528
+ name: torch.full_like(p, float("nan"))
529
+ for name, p in original_params[layer_index].items()
530
+ }
531
+ params = dict(layer.named_parameters())
532
+ calls = [
533
+ (e, s, b)
534
+ for e in range(experts)
535
+ for s in shards
536
+ for b in ([False, True] if has_bias else [False])
537
+ ]
538
+ for call_index, (expert, shard, bias) in enumerate(calls):
539
+ name = ("w2" if shard == "w2" else "w13") + (
540
+ "_bias" if bias else "_weight"
541
+ )
542
+ shape = (
543
+ ((hidden,) if bias else (hidden, 2 * intermediate))
544
+ if shard == "w2"
545
+ else ((2 * intermediate,) if bias else (2 * intermediate, hidden))
546
+ )
547
+ weight = torch.arange(
548
+ torch.Size(shape).numel(), dtype=torch.float32
549
+ ).reshape(shape)
550
+ weight = weight + 100 * (1 + call_index + cycle)
551
+ inputs.append(ref(weight))
552
+ # Direct checkpoint loading is the reference for deferred reload.
553
+ layer.weight_loader(expected[name], weight, name, shard, expert)
554
+ param = params[name]
555
+ param.weight_loader(param, weight, name, shard, expert)
556
+ del weight
557
+ if call_index != len(calls) - 1:
558
+ assert info.can_load(), (
559
+ "Layer processed before its final checkpoint shard"
560
+ )
561
+
562
+ assert not info.can_load(), (
563
+ "Padding must not defer the layer until finalization"
564
+ )
565
+ assert not info.loaded_weights
566
+ assert len(processed) == cycle * len(model) + layer_index + 1
567
+ assert all(source() is None for source in inputs)
568
+ for name, original in original_params[layer_index].items():
569
+ assert getattr(layer, name) is original
570
+ # Kernel-specific tests cover padding, which is not checkpoint data.
571
+ mask = torch.isfinite(expected[name])
572
+ assert torch.equal(original[mask], expected[name][mask])
573
+
574
+
575
def test_get_numel_loaded():
576
param = torch.empty(10, device="meta")
577
loaded_weight = torch.empty(10)
vllm/model_executor/layers/fused_moe/routed_experts.py
+18
@@ -27,6 +27,7 @@ from vllm.model_executor.layers.quantization.base_config import (
27
QuantizationConfig,
28
resolve_quant_method,
29
)
30
+from vllm.utils.math_utils import cdiv
31
32
if TYPE_CHECKING:
33
from vllm.model_executor.layers.fused_moe.runner.shared_experts import SharedExperts
@@ -566,6 +567,23 @@ class RoutedExperts(PluggableLayer):
567
hidden_dim=hidden_dim,
568
shard_dim=shard_dim,
569
)
570
+ if (
571
+ loaded_weight.device.type == "cpu"
572
+ and expert_data.device.type == "cuda"
573
+ and not expert_data.is_contiguous()
574
+ and expert_data.ndim == 2
575
+ ):
576
+ # A strided CPU-to-CUDA copy can allocate a full-shard CUDA
577
+ # temporary. Make the TP slice contiguous on CPU and copy rows
578
+ # in chunks to limit the temporary used for the padded view.
579
+ loaded_weight = loaded_weight.contiguous()
580
+ num_chunks = max(cdiv(loaded_weight.nbytes, 1 << 20), 1)
581
+ for dst, src in zip(
582
+ expert_data.chunk(num_chunks, dim=0),
583
+ loaded_weight.chunk(num_chunks, dim=0),
584
+ ):
585
+ dst.copy_(src)
586
+ return
587
expert_data.copy_(loaded_weight)
588
589
def _load_single_value(
vllm/model_executor/layers/fused_moe/unquantized_fused_moe_method.py
+102
-20
@@ -11,6 +11,7 @@ from vllm.logger import init_logger
11
from vllm.model_executor.custom_op import CustomOp
12
from vllm.model_executor.layers.fused_moe.config import (
13
FusedMoEConfig,
14
+ FusedMoEParallelConfig,
15
FusedMoEQuantConfig,
16
biased_moe_quant_config,
17
)
@@ -33,6 +34,7 @@ from vllm.model_executor.utils import (
34
set_weight_attrs,
35
)
36
from vllm.platforms import current_platform
37
+from vllm.utils.math_utils import round_up
38
39
if TYPE_CHECKING:
40
from vllm.model_executor.layers.fused_moe.routed_experts import RoutedExperts
@@ -68,22 +70,38 @@ class UnquantizedFusedMoEMethod(FusedMoEMethodBase, CustomOp):
70
params_dtype: torch.dtype,
71
**extra_weight_attrs,
72
):
73
+ unpadded_intermediate = self.moe.intermediate_size_per_partition_unpadded
74
+ assert unpadded_intermediate is not None
75
+ has_intermediate_padding = (
76
+ intermediate_size_per_partition > unpadded_intermediate
77
+ )
78
if self.moe.is_act_and_mul:
79
w13_up_dim = 2 * intermediate_size_per_partition
80
else:
81
w13_up_dim = intermediate_size_per_partition
82
+ unpadded_hidden = self.moe.hidden_dim_unpadded
83
+ assert unpadded_hidden is not None
84
+ unpadded_up = unpadded_intermediate * (2 if self.moe.is_act_and_mul else 1)
85
# Fused gate_up_proj (column parallel)
76
- w13_weight = torch.nn.Parameter(
77
- torch.empty(
78
- num_experts,
79
- w13_up_dim,
80
- hidden_size,
81
- dtype=params_dtype,
82
- ),
83
- requires_grad=False,
86
+ w13_data = torch.empty(
87
+ num_experts,
88
+ w13_up_dim,
89
+ hidden_size,
90
+ dtype=params_dtype,
91
)
92
+ if has_intermediate_padding:
93
+ # The checkpoint loader only writes the unpadded slices. Zero the
94
+ # tails now so no full-size padding copy is needed after loading.
95
+ w13_data[:, unpadded_intermediate:intermediate_size_per_partition].zero_()
96
+ if self.moe.is_act_and_mul:
97
+ w13_data[
98
+ :,
99
+ intermediate_size_per_partition + unpadded_intermediate :,
100
+ ].zero_()
101
+ w13_weight = torch.nn.Parameter(w13_data, requires_grad=False)
102
layer.register_parameter("w13_weight", w13_weight)
103
set_weight_attrs(w13_weight, extra_weight_attrs)
104
+ w13_weight.weight_loader_numel = num_experts * unpadded_up * unpadded_hidden
105
if self.moe.has_bias:
106
w13_bias = torch.nn.Parameter(
107
torch.zeros(num_experts, w13_up_dim, dtype=params_dtype),
@@ -91,18 +109,22 @@ class UnquantizedFusedMoEMethod(FusedMoEMethodBase, CustomOp):
109
)
110
layer.register_parameter("w13_bias", w13_bias)
111
set_weight_attrs(w13_bias, extra_weight_attrs)
112
+ w13_bias.weight_loader_numel = num_experts * unpadded_up
113
# down_proj (row parallel)
95
- w2_weight = torch.nn.Parameter(
96
- torch.empty(
97
- num_experts,
98
- hidden_size,
99
- intermediate_size_per_partition,
100
- dtype=params_dtype,
101
- ),
102
- requires_grad=False,
114
+ w2_data = torch.empty(
115
+ num_experts,
116
+ hidden_size,
117
+ intermediate_size_per_partition,
118
+ dtype=params_dtype,
119
)
120
+ if has_intermediate_padding:
121
+ w2_data[:, :, unpadded_intermediate:].zero_()
122
+ w2_weight = torch.nn.Parameter(w2_data, requires_grad=False)
123
layer.register_parameter("w2_weight", w2_weight)
124
set_weight_attrs(w2_weight, extra_weight_attrs)
125
+ w2_weight.weight_loader_numel = (
126
+ num_experts * unpadded_hidden * unpadded_intermediate
127
+ )
128
if self.moe.has_bias:
129
w2_bias = torch.nn.Parameter(
130
torch.zeros(num_experts, hidden_size, dtype=params_dtype),
@@ -110,6 +132,26 @@ class UnquantizedFusedMoEMethod(FusedMoEMethodBase, CustomOp):
132
)
133
layer.register_parameter("w2_bias", w2_bias)
134
set_weight_attrs(w2_bias, extra_weight_attrs)
135
+ w2_bias.weight_loader_numel = num_experts * unpadded_hidden
136
+
137
+ def maybe_roundup_sizes(
138
+ self,
139
+ hidden_size: int,
140
+ intermediate_size_per_partition: int,
141
+ act_dtype: torch.dtype,
142
+ moe_parallel_config: FusedMoEParallelConfig,
143
+ ) -> tuple[int, int]:
144
+ hidden_size, intermediate_size_per_partition = super().maybe_roundup_sizes(
145
+ hidden_size,
146
+ intermediate_size_per_partition,
147
+ act_dtype,
148
+ moe_parallel_config,
149
+ )
150
+ if self.unquantized_backend == UnquantizedMoeBackend.FLASHINFER_TRTLLM:
151
+ intermediate_size_per_partition = round_up(
152
+ intermediate_size_per_partition, 128
153
+ )
154
+ return hidden_size, intermediate_size_per_partition
155
156
def _maybe_pad_weight(self, weight: torch.Tensor) -> torch.Tensor:
157
# Pad the weight tensor. This is an optimization on ROCm platform, which
@@ -135,6 +177,21 @@ class UnquantizedFusedMoEMethod(FusedMoEMethodBase, CustomOp):
177
w13: torch.Tensor,
178
w2: torch.Tensor,
179
) -> None:
180
+ w13_parameter_shape = w13.shape
181
+ w2_parameter_shape = w2.shape
182
+
183
+ if self.unquantized_backend == UnquantizedMoeBackend.FLASHINFER_TRTLLM:
184
+ unpadded = self.moe.intermediate_size_per_partition_unpadded
185
+ assert unpadded is not None
186
+ intermediate = w2.shape[-1]
187
+ # Reloads overwrite only checkpoint slices; the previous in-place
188
+ # permutation can leave nonzero values in the raw padding slots.
189
+ if intermediate > unpadded:
190
+ w13[:, unpadded:intermediate].zero_()
191
+ if self.moe.is_act_and_mul:
192
+ w13[:, intermediate + unpadded :].zero_()
193
+ w2[:, :, unpadded:].zero_()
194
+
195
# Shuffle weights to runtime format.
196
w13_new, w2_new = convert_to_unquantized_kernel_format(
197
self.unquantized_backend,
@@ -142,6 +199,10 @@ class UnquantizedFusedMoEMethod(FusedMoEMethodBase, CustomOp):
199
w13_weight=w13,
200
w2_weight=w2,
201
)
202
+ if self.unquantized_backend == UnquantizedMoeBackend.FLASHINFER_TRTLLM:
203
+ w13_new = w13_new.view(w13_parameter_shape)
204
+ w2_new = w2_new.view(w2_parameter_shape)
205
+
206
# `moe_kernel` is initialized to None in FusedMoEMethodBase.__init__;
207
# On the first call we replace the parameter normally. On subsequent
208
# calls (e.g. RL weight updates that re-trigger
@@ -170,6 +231,25 @@ class UnquantizedFusedMoEMethod(FusedMoEMethodBase, CustomOp):
231
assert self.moe_kernel is not None
232
self.moe_kernel.fused_experts.process_weights_after_loading(layer)
233
234
+ def _kernel_weights(
235
+ self, layer: "RoutedExperts"
236
+ ) -> tuple[torch.Tensor, torch.Tensor]:
237
+ if self.unquantized_backend != UnquantizedMoeBackend.FLASHINFER_TRTLLM:
238
+ return layer.w13_weight, layer.w2_weight
239
+
240
+ # IPC caches restore tensor shapes/storage, not method attributes.
241
+ # BlockMajorK uses 128-byte blocks, i.e. 64 BF16 elements.
242
+ def block_view(weight: torch.Tensor) -> torch.Tensor:
243
+ if weight.ndim == 4:
244
+ return weight
245
+ experts, rows, cols = weight.shape
246
+ return weight.view(experts, cols // 64, rows, 64)
247
+
248
+ return (
249
+ block_view(layer.w13_weight),
250
+ block_view(layer.w2_weight),
251
+ )
252
+
253
def _init_moe_kernel(self, layer: "RoutedExperts") -> None:
254
"""Build the MoE kernel from the layer's current (shuffled) weights."""
255
self.moe_quant_config = self.get_fused_moe_quant_config(layer)
@@ -274,10 +354,11 @@ class UnquantizedFusedMoEMethod(FusedMoEMethodBase, CustomOp):
354
shared_experts_input: torch.Tensor | None,
355
) -> torch.Tensor:
356
assert self.moe_kernel is not None
357
+ w13, w2 = self._kernel_weights(layer)
358
return self.moe_kernel.apply(
359
hidden_states=x,
279
- w1=layer.w13_weight,
280
- w2=layer.w2_weight,
360
+ w1=w13,
361
+ w2=w2,
362
topk_weights=topk_weights,
363
topk_ids=topk_ids,
364
activation=layer.activation,
@@ -315,10 +396,11 @@ class UnquantizedFusedMoEMethod(FusedMoEMethodBase, CustomOp):
396
) -> torch.Tensor | UnfinalizedMoEOutput:
397
assert self.is_monolithic
398
assert self.moe_kernel is not None
399
+ w13, w2 = self._kernel_weights(layer)
400
return self.moe_kernel.apply_monolithic(
401
x,
320
- layer.w13_weight,
321
- layer.w2_weight,
402
+ w13,
403
+ w2,
404
router_logits,
405
activation=layer.activation,
406
global_num_experts=layer.global_num_experts,
vllm/model_executor/layers/quantization/utils/flashinfer_utils.py
+43
-43
@@ -112,8 +112,9 @@ def convert_moe_weights_to_flashinfer_trtllm_block_layout(
112
) -> tuple[torch.Tensor, torch.Tensor]:
113
"""Convert expert weights to FlashInfer's block layout.
114
115
- This reorders W13 and W2 into the expected epilogue-tiled block layout and
116
- returns the shuffled weight tensors.
115
+ This reorders W13 and W2 in place into the expected epilogue-tiled block
116
+ layout and returns views of the shuffled weight tensors. Using one expert
117
+ as scratch space avoids allocating another full copy of both weights.
118
"""
119
if w13_weight.dtype != torch.bfloat16 or w2_weight.dtype != torch.bfloat16:
120
raise ValueError(
@@ -147,51 +148,50 @@ def convert_moe_weights_to_flashinfer_trtllm_block_layout(
148
out=out,
149
)
150
150
- w13_rows, w13_cols = w13_weight[0].view(torch.uint8).shape
151
- w2_rows, w2_cols = w2_weight[0].view(torch.uint8).shape
152
- w13_weights_shuffled_tensor = torch.empty(
153
- (num_experts, w13_cols // block_k, w13_rows, block_k),
154
- dtype=torch.uint8,
155
- device=w13_weight.device,
156
- )
157
- w2_weights_shuffled_tensor = torch.empty(
158
- (num_experts, w2_cols // block_k, w2_rows, block_k),
159
- dtype=torch.uint8,
160
- device=w2_weight.device,
161
- )
162
-
163
- for i in range(num_experts):
164
- w13_expert_uint8 = w13_weight[i].view(torch.uint8)
165
-
166
- permute_indices = _maybe_get_cached_w3_w1_permute_indices(
167
- cache_permute_indices,
168
- w13_expert_uint8,
169
- epilogue_tile_m,
170
- is_gated_act_gemm=is_gated_act_gemm,
171
- )
172
- if is_gated_act_gemm:
173
- rows = w13_expert_uint8.shape[0]
174
- permute_indices = (permute_indices + rows // 2) % rows
175
- _copy_permuted_expert_to_block_layout(
176
- w13_weights_shuffled_tensor[i],
177
- w13_expert_uint8,
178
- permute_indices,
151
+ def _convert_weight_in_place(
152
+ weight: torch.Tensor,
153
+ is_w13: bool,
154
+ ) -> torch.Tensor:
155
+ rows, cols = weight[0].view(torch.uint8).shape
156
+ block_layout_shape = (num_experts, cols // block_k, rows, block_k)
157
+ expert_scratch = torch.empty(
158
+ block_layout_shape[1:],
159
+ dtype=torch.uint8,
160
+ device=weight.device,
161
)
162
181
- permute_indices = get_w2_permute_indices_with_cache(
182
- cache_permute_indices,
183
- w2_weight[i].view(torch.uint8),
184
- epilogue_tile_m,
185
- )
186
- _copy_permuted_expert_to_block_layout(
187
- w2_weights_shuffled_tensor[i],
188
- w2_weight[i].view(torch.uint8),
189
- permute_indices,
190
- )
163
+ for i in range(num_experts):
164
+ expert_uint8 = weight[i].view(torch.uint8)
165
+ if is_w13:
166
+ permute_indices = _maybe_get_cached_w3_w1_permute_indices(
167
+ cache_permute_indices,
168
+ expert_uint8,
169
+ epilogue_tile_m,
170
+ is_gated_act_gemm=is_gated_act_gemm,
171
+ )
172
+ if is_gated_act_gemm:
173
+ permute_indices = (
174
+ permute_indices + expert_uint8.shape[0] // 2
175
+ ) % expert_uint8.shape[0]
176
+ else:
177
+ permute_indices = get_w2_permute_indices_with_cache(
178
+ cache_permute_indices,
179
+ expert_uint8,
180
+ epilogue_tile_m,
181
+ )
182
+
183
+ _copy_permuted_expert_to_block_layout(
184
+ expert_scratch,
185
+ expert_uint8,
186
+ permute_indices,
187
+ )
188
+ expert_uint8.view(-1).copy_(expert_scratch.view(-1))
189
+
190
+ return weight.view(torch.uint8).view(block_layout_shape).view(torch.bfloat16)
191
192
return (
193
- w13_weights_shuffled_tensor.view(torch.bfloat16),
194
- w2_weights_shuffled_tensor.view(torch.bfloat16),
193
+ _convert_weight_in_place(w13_weight, is_w13=True),
194
+ _convert_weight_in_place(w2_weight, is_w13=False),
195
)
196
197
vllm/model_executor/model_loader/reload/meta.py
+2
-2
@@ -9,7 +9,7 @@ from torch.utils._python_dispatch import TorchDispatchMode
9
10
from .sanitize import restore_layer_refs, sanitize_layer_refs
11
from .types import LayerReloadingInfo, LayerTensors
12
-from .utils import get_layer_params_buffers, get_layer_tensors
12
+from .utils import get_layer_params_buffers, get_layer_tensors, get_tensor_load_numel
13
14
__all__ = [
15
"to_meta_tensor",
@@ -206,5 +206,5 @@ def get_numel_loaded(
206
numel = counter.copied_numel
207
param = args.arguments.get("param", None)
208
if isinstance(param, torch.Tensor):
209
- numel = min(numel, param.numel())
209
+ numel = min(numel, get_tensor_load_numel(param))
210
return numel, return_value
vllm/model_executor/model_loader/reload/utils.py
+7
-1
@@ -11,6 +11,7 @@ __all__ = [
11
"get_layer_tensors",
12
"get_layer_params_buffers",
13
"get_layer_size",
14
+ "get_tensor_load_numel",
15
"has_device_tensors",
16
"get_info_size",
17
]
@@ -30,6 +31,11 @@ def get_layer_params_buffers(layer: torch.nn.Module) -> LayerTensors:
31
)
32
33
34
+def get_tensor_load_numel(tensor: torch.Tensor) -> int:
35
+ """Count checkpoint elements, excluding padding declared by the weight creator."""
36
+ return getattr(tensor, "weight_loader_numel", tensor.numel())
37
+
38
+
39
def get_layer_size(layer: torch.nn.Module) -> int:
40
"""Calculate total number of elements across loadable tensors in a layer.
41
@@ -39,7 +45,7 @@ def get_layer_size(layer: torch.nn.Module) -> int:
45
from .meta import SKIP_LOAD_TENSORS
46
47
return sum(
42
- tensor.numel()
48
+ get_tensor_load_numel(tensor)
49
for name, tensor in get_layer_tensors(layer).items()
50
if name not in SKIP_LOAD_TENSORS
51
)