@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / 727c41f3fd
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 10, 22:45 UTC

[MoE Refactor][10/N] Cleanup Fp8 Process Weights After Loading (#31169)

Signed-off-by: Robert Shaw <robshaw@redhat.com> Co-authored-by: Robert Shaw <robshaw@redhat.com>

Robert Shaw committed Dec 27, 2025 at 15:22 UTC 727c41f3fd7c4fb183b18eb1a41a6d5be8a42c17
1 file changed +150 -194
vllm/model_executor/layers/quantization/fp8.py
+150 -194
@@ -6,7 +6,6 @@ from typing import TYPE_CHECKING, Any, Optional
6
7 import torch
8 from torch.nn import Module
9 -from torch.nn.parameter import Parameter
9 from torch.utils._python_dispatch import TorchDispatchMode
10
11 import vllm.envs as envs
@@ -728,6 +727,9 @@ class Fp8MoEMethod(FusedMoEMethodBase):
727 self.quant_config = quant_config
728 self.weight_block_size = self.quant_config.weight_block_size
729 self.block_quant: bool = self.weight_block_size is not None
730 + self.weight_scale_name = (
731 + "weight_scale_inv" if self.block_quant else "weight_scale"
732 + )
733 self.fp8_backend = get_fp8_moe_backend(
734 self.block_quant, layer.moe_parallel_config, self.moe.is_lora_enabled
735 )
@@ -832,38 +834,28 @@ class Fp8MoEMethod(FusedMoEMethodBase):
834
835 # WEIGHT_SCALES
836 if not self.block_quant:
835 - # Allocate 2 scales for w1 and w3 respectively.
836 - # They will be combined to a single scale after weight loading.
837 - w13_weight_scale = torch.nn.Parameter(
838 - torch.ones(num_experts, 2, dtype=torch.float32), requires_grad=False
839 - )
840 - w2_weight_scale = torch.nn.Parameter(
841 - torch.ones(num_experts, dtype=torch.float32), requires_grad=False
842 - )
843 - layer.register_parameter("w13_weight_scale", w13_weight_scale)
844 - layer.register_parameter("w2_weight_scale", w2_weight_scale)
837 + # For per-tensor quant, the scales are per expert and weight.
838 + w13_scale_data = torch.ones(num_experts, 2, dtype=torch.float32)
839 + w2_scale_data = torch.ones(num_experts, dtype=torch.float32)
840 else:
846 - w13_weight_scale = torch.nn.Parameter(
847 - torch.ones(
848 - num_experts,
849 - 2 * ((intermediate_size_per_partition + block_n - 1) // block_n),
850 - (hidden_size + block_k - 1) // block_k,
851 - dtype=torch.float32,
852 - ),
853 - requires_grad=False,
841 + # For block quant, the scales are per block (typically 128x128).
842 + w13_scale_data = torch.ones(
843 + num_experts,
844 + 2 * ((intermediate_size_per_partition + block_n - 1) // block_n),
845 + (hidden_size + block_k - 1) // block_k,
846 + dtype=torch.float32,
847 )
855 - w2_weight_scale = torch.nn.Parameter(
856 - torch.ones(
857 - num_experts,
858 - (hidden_size + block_n - 1) // block_n,
859 - (intermediate_size_per_partition + block_k - 1) // block_k,
860 - dtype=torch.float32,
861 - ),
862 - requires_grad=False,
848 + w2_scale_data = torch.ones(
849 + num_experts,
850 + (hidden_size + block_n - 1) // block_n,
851 + (intermediate_size_per_partition + block_k - 1) // block_k,
852 + dtype=torch.float32,
853 )
864 - layer.register_parameter("w13_weight_scale_inv", w13_weight_scale)
865 - layer.register_parameter("w2_weight_scale_inv", w2_weight_scale)
866 - assert self.quant_config.activation_scheme == "dynamic"
854 + w13_weight_scale = torch.nn.Parameter(w13_scale_data, requires_grad=False)
855 + w2_weight_scale = torch.nn.Parameter(w2_scale_data, requires_grad=False)
856 + # Note: name is weight_scale for tensor, weight_scale_inv for block.
857 + layer.register_parameter(f"w13_{self.weight_scale_name}", w13_weight_scale)
858 + layer.register_parameter(f"w2_{self.weight_scale_name}", w2_weight_scale)
859
860 # Add the quantization method used (per tensor/grouped/channel)
861 # to ensure the weight scales are loaded in properly
@@ -877,6 +869,7 @@ class Fp8MoEMethod(FusedMoEMethodBase):
869
870 # INPUT_SCALES
871 if self.quant_config.activation_scheme == "static":
872 + assert not self.block_quant
873 w13_input_scale = torch.nn.Parameter(
874 torch.ones(num_experts, dtype=torch.float32), requires_grad=False
875 )
@@ -893,158 +886,60 @@ class Fp8MoEMethod(FusedMoEMethodBase):
886 layer.w13_input_scale = None
887 layer.w2_input_scale = None
888
896 - def process_weights_after_loading(self, layer: Module) -> None:
897 - if getattr(layer, "_already_called_process_weights_after_loading", False):
898 - return
899 -
900 - # TODO (rob): refactor block quant into separate class.
901 - if self.block_quant:
902 - assert self.quant_config.activation_scheme == "dynamic"
903 - if current_platform.is_fp8_fnuz():
904 - w13_weight, w13_weight_scale_inv, w13_input_scale = (
905 - normalize_e4m3fn_to_e4m3fnuz(
906 - layer.w13_weight,
907 - layer.w13_weight_scale_inv,
908 - layer.w13_input_scale,
909 - )
910 - )
911 - w2_weight, w2_weight_scale_inv, w2_input_scale = (
912 - normalize_e4m3fn_to_e4m3fnuz(
913 - layer.w2_weight, layer.w2_weight_scale_inv, layer.w2_input_scale
914 - )
915 - )
916 - elif self.flashinfer_moe_backend is not None:
917 - # NOTE: weights have to be swapped since the activation is
918 - # applied on different half for flashinfer vs vllm
919 - w13_weight = swap_w13_to_w31(layer.w13_weight.data)
920 - w13_weight_scale_inv = swap_w13_to_w31(layer.w13_weight_scale_inv.data)
921 - w2_weight = layer.w2_weight.data
922 - w2_weight_scale_inv = layer.w2_weight_scale_inv.data
889 + def _convert_weights_to_kernel_format(
890 + self,
891 + layer: Module,
892 + w13_weight: torch.Tensor,
893 + w2_weight: torch.Tensor,
894 + w13_weight_scale: torch.Tensor,
895 + w2_weight_scale: torch.Tensor,
896 + ) -> None:
897 + if self.fp8_backend == Fp8MoeBackend.DEEPGEMM:
898 + assert self.block_quant
899 + w13_weight, w13_weight_scale = deepgemm_post_process_fp8_weight_block(
900 + wq=w13_weight,
901 + ws=w13_weight_scale,
902 + quant_block_shape=tuple(layer.weight_block_size),
903 + use_e8m0=is_deep_gemm_e8m0_used(),
904 + )
905 + w2_weight, w2_weight_scale = deepgemm_post_process_fp8_weight_block(
906 + wq=w2_weight,
907 + ws=w2_weight_scale,
908 + quant_block_shape=tuple(layer.weight_block_size),
909 + use_e8m0=is_deep_gemm_e8m0_used(),
910 + )
911 + elif self.fp8_backend == Fp8MoeBackend.AITER:
912 + w13_weight, w2_weight = rocm_aiter_ops.shuffle_weights(
913 + w13_weight, w2_weight
914 + )
915 + elif self.fp8_backend in [
916 + Fp8MoeBackend.FLASHINFER_CUTLASS,
917 + Fp8MoeBackend.FLASHINFER_TRTLLM,
918 + ]:
919 + w13_weight = swap_w13_to_w31(w13_weight)
920 + if self.block_quant:
921 + w13_weight_scale = swap_w13_to_w31(w13_weight_scale)
922 else:
924 - w13_weight = layer.w13_weight.data
925 - w13_weight_scale_inv = layer.w13_weight_scale_inv.data
926 - w2_weight = layer.w2_weight
927 - w2_weight_scale_inv = layer.w2_weight_scale_inv
928 -
929 - # torch.compile() cannot use Parameter subclasses.
930 - replace_parameter(layer, "w13_weight", w13_weight)
931 - replace_parameter(layer, "w13_weight_scale_inv", w13_weight_scale_inv)
932 - replace_parameter(layer, "w2_weight", w2_weight)
933 - replace_parameter(layer, "w2_weight_scale_inv", w2_weight_scale_inv)
934 - if self.fp8_backend == Fp8MoeBackend.AITER:
935 - # reshaping weights is required for aiter moe kernel.
936 - shuffled_w13, shuffled_w2 = rocm_aiter_ops.shuffle_weights(
937 - layer.w13_weight.data, layer.w2_weight.data
938 - )
939 -
940 - replace_parameter(layer, "w13_weight", shuffled_w13)
941 - replace_parameter(layer, "w2_weight", shuffled_w2)
942 -
943 - # DeepGemm scales need to be transposed and aligned. We try to do
944 - # it ahead of time for performance reasons.
945 - if self.fp8_backend == Fp8MoeBackend.DEEPGEMM:
946 - dg_w13_weight, dg_w13_weight_scale_inv = (
947 - deepgemm_post_process_fp8_weight_block(
948 - wq=layer.w13_weight.data,
949 - ws=layer.w13_weight_scale_inv.data,
950 - quant_block_shape=tuple(layer.weight_block_size),
951 - use_e8m0=is_deep_gemm_e8m0_used(),
952 - )
953 - )
954 - dg_w2_weight, dg_w2_weight_scale_inv = (
955 - deepgemm_post_process_fp8_weight_block(
956 - wq=layer.w2_weight.data,
957 - ws=layer.w2_weight_scale_inv.data,
958 - quant_block_shape=tuple(layer.weight_block_size),
959 - use_e8m0=is_deep_gemm_e8m0_used(),
960 - )
961 - )
962 - layer.w13_weight = Parameter(dg_w13_weight, requires_grad=False)
963 - layer.w13_weight_scale_inv = Parameter(
964 - dg_w13_weight_scale_inv, requires_grad=False
965 - )
966 - layer.w2_weight = Parameter(dg_w2_weight, requires_grad=False)
967 - layer.w2_weight_scale_inv = Parameter(
968 - dg_w2_weight_scale_inv, requires_grad=False
969 - )
970 - else:
971 - # Fp8 moe kernels require a single activation scale.
972 - # We take the max of all the scales in case they differ.
973 - if self.quant_config.activation_scheme == "static":
974 - if layer.w13_input_scale is None or layer.w2_input_scale is None:
975 - raise ValueError(
976 - "QuantConfig has static quantization, but found "
977 - "activation scales are None."
978 - )
979 - if not all_close_1d(layer.w13_input_scale) or not all_close_1d(
980 - layer.w2_input_scale
981 - ):
982 - logger.warning_once(
983 - "Found input_scales that are not equal for "
984 - "fp8 MoE layer. Using the maximum across experts "
985 - "for each layer."
986 - )
987 - replace_parameter(layer, "w13_input_scale", layer.w13_input_scale.max())
988 - replace_parameter(layer, "w2_input_scale", layer.w2_input_scale.max())
989 - if current_platform.is_fp8_fnuz():
990 - # Normalize the weights and scales
991 - w13_weight, w13_weight_scale, w13_input_scale = (
992 - normalize_e4m3fn_to_e4m3fnuz(
993 - layer.w13_weight, layer.w13_weight_scale, layer.w13_input_scale
994 - )
995 - )
996 - w2_weight, w2_weight_scale, w2_input_scale = (
997 - normalize_e4m3fn_to_e4m3fnuz(
998 - layer.w2_weight, layer.w2_weight_scale, layer.w2_input_scale
999 - )
1000 - )
1001 - # Reset the parameter
1002 - replace_parameter(layer, "w13_weight", w13_weight)
1003 - replace_parameter(layer, "w13_weight_scale", w13_weight_scale)
1004 - if w13_input_scale is not None:
1005 - replace_parameter(layer, "w13_input_scale", w13_input_scale)
1006 - replace_parameter(layer, "w2_weight", w2_weight)
1007 - replace_parameter(layer, "w2_weight_scale", w2_weight_scale)
1008 - if w2_input_scale is not None:
1009 - replace_parameter(layer, "w2_input_scale", w2_input_scale)
1010 -
1011 - # Fp8 moe kernel needs single weight scale for w13 per expert.
1012 - # We take the max then dequant and requant each expert.
1013 - assert layer.w13_weight_scale is not None
1014 - shard_size = layer.intermediate_size_per_partition
1015 - max_w13_scales = layer.w13_weight_scale.max(dim=1).values
1016 - for expert_id in range(layer.local_num_experts):
1017 - start = 0
1018 - for shard_id in range(2):
1019 - dq_weight = per_tensor_dequantize(
1020 - layer.w13_weight[expert_id][start : start + shard_size, :],
1021 - layer.w13_weight_scale[expert_id][shard_id],
1022 - )
1023 - layer.w13_weight[expert_id][start : start + shard_size, :], _ = (
1024 - ops.scaled_fp8_quant(dq_weight, max_w13_scales[expert_id])
1025 - )
1026 - start += shard_size
1027 -
1028 - if self.fp8_backend == Fp8MoeBackend.AITER:
1029 - shuffled_w13, shuffled_w2 = rocm_aiter_ops.shuffle_weights(
1030 - layer.w13_weight, layer.w2_weight
1031 - )
1032 -
1033 - replace_parameter(layer, "w13_weight", shuffled_w13)
1034 - replace_parameter(layer, "w2_weight", shuffled_w2)
1035 -
1036 - replace_parameter(layer, "w13_weight_scale", max_w13_scales)
1037 -
1038 - if self.flashinfer_moe_backend is not None:
1039 - # NOTE: weights have to be swapped since the activation is
1040 - # applied on different half for flashinfer vs vllm
1041 - assert not self.block_quant
923 + # TODO(rob): this function is a hack that renames the scaling
924 + # factors in the Module. This is a hack we should clean up.
925 register_moe_scaling_factors(layer)
1043 - w13_weight = swap_w13_to_w31(layer.w13_weight.data)
1044 - if self.flashinfer_moe_backend == FlashinferMoeBackend.TENSORRT_LLM:
926 + if self.fp8_backend == Fp8MoeBackend.FLASHINFER_TRTLLM:
927 rotate_flashinfer_fp8_moe_weights(w13_weight, w2_weight)
1046 - layer.w13_weight.data = w13_weight.data
928 + elif self.fp8_backend == Fp8MoeBackend.AITER:
929 + w13_weight, w2_weight = rocm_aiter_ops.shuffle_weights(
930 + w13_weight, w2_weight
931 + )
932
933 + # Replace parameters with updated versions. Note that this helper
934 + # function ensures the replacement is compatible with RL weight reloads.
935 + replace_parameter(layer, "w13_weight", w13_weight)
936 + replace_parameter(layer, "w2_weight", w2_weight)
937 + replace_parameter(layer, f"w13_{self.weight_scale_name}", w13_weight_scale)
938 + replace_parameter(layer, f"w2_{self.weight_scale_name}", w2_weight_scale)
939 +
940 + # TODO(rob): we do this after replace_parameter() because
941 + # prepare_moe_fp8_layer_for_marlin uses on the layer's params
942 + # directly. We will refactor this in a follow up PR.
943 if self.fp8_backend == Fp8MoeBackend.MARLIN:
944 prepare_moe_fp8_layer_for_marlin(
945 layer, False, input_dtype=self.marlin_input_dtype
@@ -1053,6 +948,8 @@ class Fp8MoEMethod(FusedMoEMethodBase):
948 del layer.w13_input_scale
949 del layer.w2_input_scale
950
951 + def _setup_kernel(self, layer: Module) -> None:
952 + """Setup Modular Kernel for TP Case"""
953 # NOTE(rob): this is a WIP refactor. We are first migrating
954 # all of the kernels in the TP case to use mk. Once this is
955 # done, then we will initialzie the TP case and DP/EP case
@@ -1134,6 +1031,71 @@ class Fp8MoEMethod(FusedMoEMethodBase):
1031 )
1032 self.use_inplace = True
1033
1034 + def process_weights_after_loading(self, layer: Module) -> None:
1035 + if getattr(layer, "_already_called_process_weights_after_loading", False):
1036 + return
1037 +
1038 + # Allow for accessing weights and scales in standard way.
1039 + w13_weight = layer.w13_weight
1040 + w2_weight = layer.w2_weight
1041 + w13_weight_scale = getattr(layer, f"w13_{self.weight_scale_name}")
1042 + w2_weight_scale = getattr(layer, f"w2_{self.weight_scale_name}")
1043 +
1044 + # MI300x and MI325x use FNUZ format for FP8. Convert if needed.
1045 + if current_platform.is_fp8_fnuz():
1046 + w13_weight, w13_weight_scale, layer.w13_input_scale = (
1047 + normalize_e4m3fn_to_e4m3fnuz(
1048 + w13_weight, w13_weight_scale, layer.w13_input_scale
1049 + )
1050 + )
1051 + w2_weight, w2_weight_scale, layer.w2_input_scale = (
1052 + normalize_e4m3fn_to_e4m3fnuz(
1053 + w2_weight, w2_weight_scale, layer.w2_input_scale
1054 + )
1055 + )
1056 +
1057 + # Per tensor kernels require single activation scale. Use the max.
1058 + if self.quant_config.activation_scheme == "static":
1059 + assert not self.block_quant
1060 + assert layer.w13_input_scale is not None
1061 + assert layer.w2_input_scale is not None
1062 + if not all_close_1d(layer.w13_input_scale) or not all_close_1d(
1063 + layer.w2_input_scale
1064 + ):
1065 + logger.warning_once(
1066 + "Found input_scales that are not equal for "
1067 + "fp8 MoE layer. Using the maximum across experts "
1068 + "for each layer."
1069 + )
1070 + replace_parameter(layer, "w13_input_scale", layer.w13_input_scale.max())
1071 + replace_parameter(layer, "w2_input_scale", layer.w2_input_scale.max())
1072 +
1073 + # Per tensor kernels require single weight scale for w13 per expert, but
1074 + # on disk there is a scale for w1 and w3. Use the max to requantize.
1075 + if not self.block_quant:
1076 + shard_size = layer.intermediate_size_per_partition
1077 + max_w13_scales = w13_weight_scale.max(dim=1).values
1078 + for expert_id in range(layer.local_num_experts):
1079 + start = 0
1080 + for shard_id in range(2):
1081 + dq_weight = per_tensor_dequantize(
1082 + w13_weight[expert_id][start : start + shard_size, :],
1083 + w13_weight_scale[expert_id][shard_id],
1084 + )
1085 + w13_weight[expert_id][start : start + shard_size, :], _ = (
1086 + ops.scaled_fp8_quant(dq_weight, max_w13_scales[expert_id])
1087 + )
1088 + start += shard_size
1089 + w13_weight_scale = max_w13_scales
1090 +
1091 + # Shuffle weights into the runtime format.
1092 + self._convert_weights_to_kernel_format(
1093 + layer, w13_weight, w2_weight, w13_weight_scale, w2_weight_scale
1094 + )
1095 +
1096 + # Setup modular kernel for TP case.
1097 + self._setup_kernel(layer)
1098 +
1099 def maybe_make_prepare_finalize(
1100 self,
1101 routing_tables: tuple[torch.Tensor, torch.Tensor, torch.Tensor] | None = None,
@@ -1453,32 +1415,26 @@ class Fp8OnlineMoEMethod(Fp8MoEMethod):
1415
1416 # If checkpoint is fp16, quantize in place.
1417 fp8_dtype = current_platform.fp8_dtype()
1456 - w13_weight = torch.empty_like(layer.w13_weight.data, dtype=fp8_dtype)
1457 - w2_weight = torch.empty_like(layer.w2_weight.data, dtype=fp8_dtype)
1418 + w13_weight = torch.empty_like(layer.w13_weight, dtype=fp8_dtype)
1419 + w2_weight = torch.empty_like(layer.w2_weight, dtype=fp8_dtype)
1420
1421 for expert in range(layer.local_num_experts):
1422 w13_weight[expert, :, :], layer.w13_weight_scale[expert] = (
1461 - ops.scaled_fp8_quant(layer.w13_weight.data[expert, :, :])
1423 + ops.scaled_fp8_quant(layer.w13_weight[expert, :, :])
1424 )
1425 w2_weight[expert, :, :], layer.w2_weight_scale[expert] = (
1464 - ops.scaled_fp8_quant(layer.w2_weight.data[expert, :, :])
1426 + ops.scaled_fp8_quant(layer.w2_weight[expert, :, :])
1427 )
1428 replace_parameter(layer, "w13_weight", w13_weight)
1429 replace_parameter(layer, "w2_weight", w2_weight)
1430
1469 - # Reshuffle weights for AITER if needed.
1470 - if self.fp8_backend == Fp8MoeBackend.AITER:
1471 - shuffled_w13, shuffled_w2 = rocm_aiter_ops.shuffle_weights(
1472 - layer.w13_weight, layer.w2_weight
1473 - )
1474 - replace_parameter(layer, "w13_weight", shuffled_w13)
1475 - replace_parameter(layer, "w2_weight", shuffled_w2)
1431 + # Shuffle weights into the runtime format.
1432 + self._convert_weights_to_kernel_format(
1433 + layer, w13_weight, w2_weight, layer.w13_weight_scale, layer.w2_weight_scale
1434 + )
1435
1477 - # Rushuffle weights for MARLIN if needed.
1478 - elif self.fp8_backend == Fp8MoeBackend.MARLIN:
1479 - prepare_moe_fp8_layer_for_marlin(
1480 - layer, False, input_dtype=self.marlin_input_dtype
1481 - )
1436 + # Setup modular kernel for TP case.
1437 + self._setup_kernel(layer)
1438
1439
1440 class Fp8KVCacheMethod(BaseKVCacheMethod):