Support kimi k3 nvfp4 checkpoint (#53132)

Signed-off-by: wzhao18 <wzhao18.sz@gmail.com>
Co-authored-by: OpenAI Codex <codex@openai.com>
This commit is contained in:
Wei Zhao
2026-08-21 05:59:44 +00:00
committed by GitHub
co-authored by OpenAI Codex
parent b389ac2946
commit f8e0602713
6 changed files with 151 additions and 35 deletions
+21 -2
View File
@@ -23,7 +23,7 @@ from tests.kernels.utils import torch_moe
from vllm import _custom_ops as ops
from vllm.config import ParallelConfig, VllmConfig, set_current_vllm_config
from vllm.model_executor.custom_op import CustomOp, op_registry
from vllm.model_executor.layers.activation import SiluAndMulWithClamp
from vllm.model_executor.layers.activation import SiluAndMulWithClamp, SituAndMul
from vllm.model_executor.layers.fused_moe import fused_topk
from vllm.model_executor.layers.fused_moe.activation import MoEActivation
from vllm.model_executor.layers.fused_moe.all2all_utils import (
@@ -63,6 +63,7 @@ MNK_FACTORS = [
_SWIGLU_LIMIT = 0.1
_LARGE_OUTPUT1_SCALE = 32768.0
_CLAMP_OP_NAME = "test_silu_and_mul_with_clamp"
_SITU_OP_NAME = "test_situ_and_mul"
# Test-only fixed-limit clamp. ``custom_op_name`` makes the class itself
# valid as an ``activation=`` argument to ``torch_moe`` (which only looks
@@ -78,12 +79,24 @@ if _CLAMP_OP_NAME not in op_registry:
super().__init__(_SWIGLU_LIMIT, compile_native=compile_native)
if _SITU_OP_NAME not in op_registry:
@CustomOp.register(_SITU_OP_NAME)
class _SituAndMulTest(SituAndMul):
custom_op_name = _SITU_OP_NAME
def __init__(self, *, compile_native: bool = True) -> None:
super().__init__(4.0, 25.0, compile_native=compile_native)
SILU_WITH_CLAMP = op_registry[_CLAMP_OP_NAME]
SITU = op_registry[_SITU_OP_NAME]
ACTIVATION_CASES = [
pytest.param(MoEActivation.SILU, MoEActivation.SILU, None, id="silu"),
pytest.param(MoEActivation.SILU, SILU_WITH_CLAMP, _SWIGLU_LIMIT, id="silu_clamp"),
pytest.param(MoEActivation.SITU, SITU, None, id="situ"),
pytest.param(
MoEActivation.RELU2_NO_MUL,
MoEActivation.RELU2_NO_MUL,
@@ -108,7 +121,7 @@ def test_trtllm_fp4_moe_no_graph(
topk: int,
dtype: torch.dtype,
activation: MoEActivation,
torch_activation: MoEActivation | type[SiluAndMulWithClamp],
torch_activation: MoEActivation | type[SiluAndMulWithClamp] | type[SituAndMul],
swiglu_limit: float | None,
workspace_init,
):
@@ -173,6 +186,10 @@ def test_trtllm_fp4_moe_no_graph(
in_dtype=dtype,
routing_method=RoutingMethodType.TopK,
max_num_tokens=next_power_of_2(m),
activation_situ_beta=4.0 if activation == MoEActivation.SITU else None,
activation_situ_linear_beta=(
25.0 if activation == MoEActivation.SITU else None
),
)
trtllm_inner = TrtLlmNvFp4ExpertsModular(
@@ -191,6 +208,8 @@ def test_trtllm_fp4_moe_no_graph(
fake_layer.w13_input_scale = torch.ones_like(quant_config.g1_alphas)
fake_layer.w2_input_scale = torch.ones_like(quant_config.g2_alphas)
trtllm_inner.process_weights_after_loading(fake_layer)
if activation == MoEActivation.SITU:
torch.testing.assert_close(trtllm_inner.g1_scale_c, quant_config.a2_gscale)
trtllm_experts = mk.FusedMoEKernel(
maybe_make_prepare_finalize(
+59
View File
@@ -23,6 +23,7 @@ from vllm.model_executor.layers.linear import UnquantizedLinearMethod
from vllm.model_executor.layers.quantization.modelopt import (
ModelOptFp8Config,
ModelOptFp8LinearMethod,
ModelOptFp8PbWoLinearMethod,
ModelOptMixedPrecisionConfig,
ModelOptMxFp8Config,
ModelOptNvFp4Config,
@@ -137,6 +138,64 @@ def test_modelopt_fp8_updates_weight_dims_after_transpose():
method.fp8_linear.process_weights_after_loading.assert_called_once_with(layer)
def test_modelopt_fp8_pb_wo_hides_output_padding(default_vllm_config):
config = ModelOptFp8Config(
quant_method="FP8_PB_WO",
is_checkpoint_fp8_serialized=True,
kv_cache_quant_method=None,
exclude_modules=[],
)
default_vllm_config.model_config = Mock(dtype=torch.bfloat16)
kernel = Mock()
with (
set_current_vllm_config(default_vllm_config),
patch(
"vllm.model_executor.layers.quantization.modelopt.init_fp8_linear_kernel",
return_value=kernel,
),
patch(
"vllm.model_executor.parameter.get_tensor_model_parallel_rank",
return_value=0,
),
patch(
"vllm.model_executor.parameter.get_tensor_model_parallel_world_size",
return_value=1,
),
):
method = ModelOptFp8PbWoLinearMethod(config)
layer = torch.nn.Module()
method.create_weights(
layer,
input_size_per_partition=128,
output_partition_sizes=[128, 65],
input_size=128,
output_size=193,
params_dtype=torch.bfloat16,
)
assert layer.weight.shape == (193, 128)
assert layer.weight_scale.shape == (2, 1, 1, 1)
layer.weight.data.fill_(1)
method.process_weights_after_loading(layer)
assert layer.weight.shape == (256, 128)
assert torch.count_nonzero(layer.weight[193:].float()) == 0
kernel.process_weights_after_loading.assert_called_once_with(layer)
physical_output = torch.randn(2, 256, dtype=torch.bfloat16)
kernel.apply_weights.return_value = physical_output
bias = torch.randn(193, dtype=torch.bfloat16)
output = method.apply(layer, torch.randn(2, 128), bias)
torch.testing.assert_close(output, physical_output[:, :193] + bias)
assert output.shape == (2, 193)
assert output.is_contiguous()
kernel.apply_weights.assert_called_once()
assert kernel.apply_weights.call_args.args[2] is None
def test_modelopt_nvfp4_leaves_excluded_parallel_lm_head_unquantized():
config = ModelOptNvFp4Config(
is_checkpoint_nvfp4_serialized=True,
@@ -371,8 +371,17 @@ def _get_kv_b_proj_input_dtype(
return kv_b_proj.params_dtype
if weight_dtype == torch.uint8:
return None
if weight_dtype == current_platform.fp8_dtype() and not use_fp8_prefill:
return None
if weight_dtype == current_platform.fp8_dtype():
from vllm.model_executor.layers.quantization.modelopt import (
ModelOptFp8PbWoLinearMethod,
)
quant_method = getattr(kv_b_proj, "quant_method", None)
# FP8_PB_WO dynamically quantizes BF16/FP16 inputs in the linear method.
if isinstance(quant_method, ModelOptFp8PbWoLinearMethod):
return quant_method.input_dtype
if not use_fp8_prefill:
return None
return weight_dtype
@@ -70,16 +70,9 @@ class TrtLlmNvFp4ExpertsBase:
)
self.local_num_experts = moe_config.num_local_experts
self.ep_rank = moe_config.moe_parallel_config.ep_rank
self.is_situ = moe_config.activation == MoEActivation.SITU
assert self.quant_config.g1_alphas is not None
assert self.quant_config.a2_gscale is not None
if moe_config.is_act_and_mul:
# g1_alpha_s = a13_scale * w13_scale_2
# a2_gscale = (1 / a2_scale)
# g1_scale_c = a13_scale * w13_scale_2 / a2_scale
self.g1_scale_c = self.quant_config.g1_alphas * self.quant_config.a2_gscale
else:
self.g1_scale_c = self.quant_config.a2_gscale.clone()
self.g1_scale_c = self._compute_g1_scale_c()
# Fall back to moe_config.swiglu_* when quant_config doesn't carry them
# (ModelOpt NVFP4 checkpoints store these on moe_config, not quant_config).
@@ -120,7 +113,6 @@ class TrtLlmNvFp4ExpertsBase:
# (gemm1_alpha) and situ linear_beta -> gatedActBeta (gemm1_beta).
# These operate on the dequantized gate/up, so they are NOT folded by
# g1_alphas in process_weights_after_loading.
self.is_situ = moe_config.activation == MoEActivation.SITU
if self.is_situ:
situ_beta = moe_config.activation_situ_beta
situ_linear_beta = moe_config.activation_situ_linear_beta
@@ -143,18 +135,28 @@ class TrtLlmNvFp4ExpertsBase:
clamp,
)
def _compute_g1_scale_c(self) -> torch.Tensor:
assert self.quant_config.g1_alphas is not None
assert self.quant_config.a2_gscale is not None
if not self.moe_config.is_act_and_mul:
return self.quant_config.a2_gscale.clone()
if self.is_situ:
# SITU applies its nonlinear activation after g1_alphas, so only
# the output quantization factor belongs in g1_scale_c.
return self.quant_config.a2_gscale.clone()
# g1_alphas = a13_scale * w13_scale_2
# a2_gscale = 1 / a2_scale
# g1_scale_c = a13_scale * w13_scale_2 / a2_scale
return self.quant_config.g1_alphas * self.quant_config.a2_gscale
def process_weights_after_loading(self, layer: torch.nn.Module) -> None:
layer.w13_weight_scale_2.data.mul_(layer.w13_input_scale)
layer.w2_weight_scale_2.data.mul_(layer.w2_input_scale)
# Recompute g1_scale_c since g1_alphas was just fused in-place.
# Register as a layer parameter so EPLB rearranges it alongside
# other expert weights.
assert self.quant_config.g1_alphas is not None
assert self.quant_config.a2_gscale is not None
if self.moe_config.is_act_and_mul:
g1_scale_c = self.quant_config.g1_alphas * self.quant_config.a2_gscale
else:
g1_scale_c = self.quant_config.a2_gscale.clone()
g1_scale_c = self._compute_g1_scale_c()
layer.register_parameter(
"g1_scale_c",
torch.nn.Parameter(g1_scale_c, requires_grad=False),
@@ -624,6 +624,8 @@ class ModelOptFp8PbWoLinearMethod(LinearMethodBase):
where block size is typically 128 for both dims.
vLLM executes it as FP8 GEMM with *dynamic per-token* activation quant.
Output widths that are not block-aligned are padded and restored
to their logical width before returning to the model.
"""
_WEIGHT_BLOCK_SIZE: tuple[int, int] = (128, 128)
@@ -670,6 +672,12 @@ class ModelOptFp8PbWoLinearMethod(LinearMethodBase):
# element-space -> block-space for BlockQuantScaleParameter.
layer.weight_block_size = self.weight_block_size
block_n, block_k = self._WEIGHT_BLOCK_SIZE
remainder = output_size_per_partition % block_n
self.output_padding = 0 if remainder == 0 else block_n - remainder
self.logical_output_size = output_size_per_partition
physical_output_size = output_size_per_partition + self.output_padding
weight = ModelWeightParameter(
data=torch.empty(
output_size_per_partition,
@@ -682,19 +690,13 @@ class ModelOptFp8PbWoLinearMethod(LinearMethodBase):
)
layer.register_parameter("weight", weight)
block_n, block_k = self._WEIGHT_BLOCK_SIZE
if output_size_per_partition % block_n != 0:
raise ValueError(
"ModelOpt FP8_PB_WO requires out_features divisible by "
f"{block_n}, got {output_size_per_partition}."
)
if input_size_per_partition % block_k != 0:
raise ValueError(
"ModelOpt FP8_PB_WO requires in_features divisible by "
f"{block_k}, got {input_size_per_partition}."
)
out_blks = output_size_per_partition // block_n
out_blks = physical_output_size // block_n
in_blks = input_size_per_partition // block_k
# Match ModelOpt's exported shape so weight loading works without a
@@ -711,7 +713,7 @@ class ModelOptFp8PbWoLinearMethod(LinearMethodBase):
self.w8a8_block_fp8_linear = init_fp8_linear_kernel(
activation_quant_key=self.activation_quant_key,
weight_quant_key=self.weight_quant_key,
weight_shape=layer.weight.shape,
weight_shape=(physical_output_size, input_size_per_partition),
input_dtype=self.input_dtype,
out_dtype=self.out_dtype,
module_name=self.__class__.__name__,
@@ -719,7 +721,15 @@ class ModelOptFp8PbWoLinearMethod(LinearMethodBase):
def process_weights_after_loading(self, layer: torch.nn.Module) -> None:
# Keep weight in [out, in] layout for Fp8BlockScaledMMLinearKernel.
layer.weight = Parameter(layer.weight.data, requires_grad=False)
weight = layer.weight.data
if self.output_padding:
padded_weight = weight.new_zeros(
self.logical_output_size + self.output_padding,
weight.shape[1],
)
padded_weight[: self.logical_output_size].copy_(weight)
weight = padded_weight
layer.weight = Parameter(weight, requires_grad=False)
scale = layer.weight_scale
if scale.dim() == 4:
@@ -733,8 +743,7 @@ class ModelOptFp8PbWoLinearMethod(LinearMethodBase):
layer.weight_scale = Parameter(scale.contiguous(), requires_grad=False)
if hasattr(self, "fp8_linear"):
self.fp8_linear.process_weights_after_loading(layer)
self.w8a8_block_fp8_linear.process_weights_after_loading(layer)
def apply(
self,
@@ -742,7 +751,15 @@ class ModelOptFp8PbWoLinearMethod(LinearMethodBase):
x: torch.Tensor,
bias: torch.Tensor | None = None,
) -> torch.Tensor:
return self.w8a8_block_fp8_linear.apply_weights(layer, x, bias)
kernel_bias = None if self.output_padding else bias
output = self.w8a8_block_fp8_linear.apply_weights(layer, x, kernel_bias)
if not self.output_padding:
return output
output = output[..., : self.logical_output_size].contiguous()
if bias is not None:
output.add_(bias)
return output
class ModelOptFp8MoEMethod(FusedMoEMethodBase):
@@ -2390,6 +2407,8 @@ class ModelOptMixedPrecisionConfig(ModelOptQuantConfigBase):
if isinstance(layer, (LinearBase, ParallelLMHead)):
if quant_algo == "FP8":
return ModelOptFp8LinearMethod(self.fp8_config)
if quant_algo == "FP8_PB_WO":
return ModelOptFp8PbWoLinearMethod(self.fp8_config)
if quant_algo == "NVFP4":
return ModelOptNvFp4LinearMethod(self.nvfp4_config)
if quant_algo == "W4A16_NVFP4":
+11 -3
View File
@@ -36,7 +36,7 @@ from vllm.model_executor.model_loader.weight_utils import (
default_weight_loader,
sharded_weight_loader,
)
from vllm.model_executor.parameter import BasevLLMParameter
from vllm.model_executor.parameter import BasevLLMParameter, BlockQuantScaleParameter
from vllm.model_executor.utils import set_weight_attrs
from vllm.models.kimi_k3.nvidia.kda_metadata import (
KimiK3KDAAttentionBackend,
@@ -102,7 +102,11 @@ class _KimiGDNMergedColumnParallelLinear(MergedColumnParallelLinear):
) -> None:
tp_rank = self.tp_rank
param_tp_rank = getattr(param, "tp_rank", None)
if loaded_shard_id == self.replicated_shard_id:
replicate_block_scale = (
isinstance(param, BlockQuantScaleParameter)
and loaded_weight.shape[param.output_dim] < self.tp_size
)
if loaded_shard_id == self.replicated_shard_id or replicate_block_scale:
self.tp_rank = 0
if param_tp_rank is not None:
param.tp_rank = 0
@@ -121,7 +125,11 @@ class _KimiGDNMergedColumnParallelLinear(MergedColumnParallelLinear):
) -> None:
tp_rank = self.tp_rank
param_tp_rank = getattr(param, "tp_rank", None)
if loaded_shard_id == self.replicated_shard_id:
replicate_block_scale = (
isinstance(param, BlockQuantScaleParameter)
and loaded_weight.shape[param.output_dim] < self.tp_size
)
if loaded_shard_id == self.replicated_shard_id or replicate_block_scale:
self.tp_rank = 0
if param_tp_rank is not None:
param.tp_rank = 0