[Refactor][Quantization] Make FP8 and MLA weight transforms reusable pure functions (#57732)

Signed-off-by: aoshen02 <aoshen@inferact.ai> Co-authored-by: Yongye Zhu <zyy1102000@gmail.com>

aoshen02 committed Sep 23, 2026 at 16:16 UTC 973a3be780bd8b0d2f69c3727fb022e90a06ed74
6 files changed +36 -49
vllm/model_executor/layers/attention/mla_attention.py
+23 -23
@@ -404,6 +404,24 @@ def _get_kv_b_proj_input_dtype(
404 return weight_dtype
405
406
407 +def split_kv_b_proj(
408 + kv_b_proj: nn.Module,
409 + out_dtype: torch.dtype,
410 + kv_lora_rank: int,
411 + num_heads: int,
412 + qk_nope_head_dim: int,
413 + v_head_dim: int,
414 +) -> tuple[torch.Tensor, torch.Tensor]:
415 + """Dequantize ``kv_b_proj`` and return ``W_UK [L,N,P]``, ``W_UV [L,N,V]``."""
416 + weight = get_and_maybe_dequant_weights(kv_b_proj, out_dtype=out_dtype).T
417 + assert weight.shape == (
418 + kv_lora_rank,
419 + num_heads * (qk_nope_head_dim + v_head_dim),
420 + ), f"kv_b_proj weight {tuple(weight.shape)} vs {kv_lora_rank=} {num_heads=}"
421 + weight = weight.view(kv_lora_rank, num_heads, qk_nope_head_dim + v_head_dim)
422 + return weight.split([qk_nope_head_dim, v_head_dim], dim=-1)
423 +
424 +
425 class MLAAttention(nn.Module, AttentionLayerBase):
426 """Multi-Head Latent Attention layer.
427
@@ -1221,13 +1239,6 @@ class MLAAttention(nn.Module, AttentionLayerBase):
1239 )
1240 return
1241
1224 - # we currently do not have quantized bmm's which are needed for
1225 - # `W_UV` and `W_UK_T`, we just store fp16/bf16 copies and perform
1226 - # the bmm's in 16-bit, the extra memory overhead of this is fairly low
1227 - kv_b_proj_weight = get_and_maybe_dequant_weights(
1228 - self.kv_b_proj, out_dtype=act_dtype
1229 - ).T
1230 -
1242 if self.dcp_q_replicate:
1243 # qrep wired here: validate unsupported decode backends once.
1244 assert self.q_pad_num_heads in (None, self.num_heads), (
@@ -1243,24 +1254,13 @@ class MLAAttention(nn.Module, AttentionLayerBase):
1254 "FP4/FP8 MLA BMM paths."
1255 )
1256
1246 - assert kv_b_proj_weight.shape == (
1247 - self.kv_lora_rank,
1248 - self.num_heads * (self.qk_nope_head_dim + self.v_head_dim),
1249 - ), (
1250 - f"{kv_b_proj_weight.shape=}, "
1251 - f"{self.kv_lora_rank=}, "
1252 - f"{self.num_heads=}, "
1253 - f"{self.qk_nope_head_dim=}, "
1254 - f"{self.v_head_dim=}"
1255 - )
1256 - kv_b_proj_weight = kv_b_proj_weight.view(
1257 + W_UK, W_UV = split_kv_b_proj(
1258 + self.kv_b_proj,
1259 + act_dtype,
1260 self.kv_lora_rank,
1261 self.num_heads,
1259 - self.qk_nope_head_dim + self.v_head_dim,
1260 - )
1261 -
1262 - W_UK, W_UV = kv_b_proj_weight.split(
1263 - [self.qk_nope_head_dim, self.v_head_dim], dim=-1
1262 + self.qk_nope_head_dim,
1263 + self.v_head_dim,
1264 )
1265
1266 # If kv_b_proj_weight is unquantized, quantize it to mxfp4 if supported
vllm/model_executor/layers/quantization/compressed_tensors/schemes/compressed_tensors_w8a8_fp8.py
+1 -4
@@ -153,12 +153,10 @@ class CompressedTensorsW8A8Fp8(CompressedTensorsScheme):
153 layer.logical_widths,
154 getattr(layer, "input_scale", None),
155 )
156 - weight = weight.t()
156 elif self.strategy == QuantizationStrategy.CHANNEL:
157 weight, weight_scale, input_scale = process_fp8_weight_channel_strategy(
158 layer.weight, layer.weight_scale, getattr(layer, "input_scale", None)
159 )
161 - weight = weight.t()
160
161 elif self.strategy == QuantizationStrategy.BLOCK:
162 assert self.is_static_input_scheme is False
@@ -177,8 +175,7 @@ class CompressedTensorsW8A8Fp8(CompressedTensorsScheme):
175
176 # required by torch.compile to be torch.nn.Parameter
177 layer.weight = Parameter(weight.data, requires_grad=False)
180 - # Preserve the dim tags dropped by the transpose so layout-aware
181 - # kernels (humming) see (K, N) instead of assuming (N, K).
178 + # The helpers return (K, N); retag so layout-aware kernels see it.
179 layer.weight.input_dim = 0
180 layer.weight.output_dim = 1
181 layer.weight_scale = Parameter(weight_scale.data, requires_grad=False)
vllm/model_executor/layers/quantization/fp8.py
-1
@@ -416,7 +416,6 @@ class Fp8LinearMethod(LinearMethodBase):
416 if self.act_q_static:
417 assert input_scale is not None
418 input_scale = input_scale.max()
419 - weight = weight.t()
419
420 # Update layer with new values.
421 replace_parameter(layer, "weight", weight.data)
vllm/model_executor/layers/quantization/modelopt.py
+1 -1
@@ -2129,7 +2129,7 @@ class KFp8StaticChannel(QuantKeyScheme):
2129 weight, weight_scale, _ = process_fp8_weight_channel_strategy(
2130 layer.weight, layer.weight_scale.data
2131 )
2132 - layer.weight = Parameter(weight.t(), requires_grad=False)
2132 + layer.weight = Parameter(weight, requires_grad=False)
2133 layer.weight_scale = Parameter(weight_scale, requires_grad=False)
2134
2135
vllm/model_executor/layers/quantization/utils/fp8_utils.py
+5 -5
@@ -1373,7 +1373,7 @@ def process_fp8_weight_tensor_strategy(
1373 logical_widths: list[int],
1374 input_scale: torch.Tensor | None = None,
1375 ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor | None]:
1376 - """Process weights for tensor-wise quantization strategy."""
1376 + """Requantize fused shards to one scale and return ``(K, N)`` weight."""
1377 from vllm.model_executor.layers.quantization.utils.w8a8_utils import (
1378 normalize_e4m3fn_to_e4m3fnuz,
1379 requantize_with_max_scale,
@@ -1391,7 +1391,7 @@ def process_fp8_weight_tensor_strategy(
1391 logical_widths=logical_widths,
1392 )
1393
1394 - weight = _maybe_pad_fp8_weight(weight)
1394 + weight = _maybe_pad_fp8_weight(weight).t()
1395 return weight, weight_scale, input_scale
1396
1397
@@ -1400,7 +1400,7 @@ def process_fp8_weight_channel_strategy(
1400 weight_scale: torch.Tensor,
1401 input_scale: torch.Tensor | None = None,
1402 ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor | None]:
1403 - """Process weights for channel-wise quantization strategy."""
1403 + """Normalize FNUZ if needed and return ``(K, N)`` weight."""
1404 from vllm.model_executor.layers.quantization.utils.w8a8_utils import (
1405 normalize_e4m3fn_to_e4m3fnuz,
1406 )
@@ -1410,14 +1410,14 @@ def process_fp8_weight_channel_strategy(
1410 weight=weight, weight_scale=weight_scale, input_scale=input_scale
1411 )
1412
1413 - return weight, weight_scale, input_scale
1413 + return weight.t(), weight_scale, input_scale
1414
1415
1416 def process_fp8_weight_block_strategy(
1417 weight: torch.Tensor,
1418 weight_scale: torch.Tensor,
1419 ) -> tuple[torch.Tensor, torch.Tensor]:
1420 - """Process weights for block-wise quantization strategy."""
1420 + """Normalize FNUZ if needed and return ``(N, K)`` weight (no transpose)."""
1421 from vllm.model_executor.layers.quantization.utils.w8a8_utils import (
1422 normalize_e4m3fn_to_e4m3fnuz,
1423 )
vllm/models/kimi_k3/nvidia/mla.py
+6 -15
@@ -58,6 +58,7 @@ from vllm.model_executor.layers.attention.mla_attention import (
58 accumulate_mla_context_chunk,
59 init_mla_context_partial,
60 neutralize_empty_context_partials,
61 + split_kv_b_proj,
62 )
63 from vllm.model_executor.layers.attention_layer_base import AttentionLayerBase
64 from vllm.model_executor.layers.layernorm import RMSNorm
@@ -73,9 +74,6 @@ from vllm.model_executor.layers.quantization import (
74 QuantizationConfig,
75 resolve_quant_method,
76 )
76 -from vllm.model_executor.layers.quantization.utils.quant_utils import (
77 - get_and_maybe_dequant_weights,
78 -)
77 from vllm.model_executor.layers.rotary_embedding import RotaryEmbedding, get_rope
78 from vllm.model_executor.utils import replace_parameter
79 from vllm.models.common.ops import fused_q_kv_rmsnorm
@@ -441,20 +439,13 @@ class MultiHeadLatentAttention(nn.Module, AttentionLayerBase):
439 projected into latent space by ``W_UK_T`` and the attention output is
440 projected back to ``v`` by ``W_UV`` -- avoiding materializing full K/V.
441 """
444 - kv_b_proj_weight = get_and_maybe_dequant_weights(
445 - self.kv_b_proj, out_dtype=act_dtype
446 - ).T
447 - assert kv_b_proj_weight.shape == (
448 - self.kv_lora_rank,
449 - self.num_local_heads * (self.qk_nope_head_dim + self.v_head_dim),
450 - ), f"{kv_b_proj_weight.shape=}"
451 - kv_b_proj_weight = kv_b_proj_weight.view(
442 + W_UK, W_UV = split_kv_b_proj(
443 + self.kv_b_proj,
444 + act_dtype,
445 self.kv_lora_rank,
446 self.num_local_heads,
454 - self.qk_nope_head_dim + self.v_head_dim,
455 - )
456 - W_UK, W_UV = kv_b_proj_weight.split(
457 - [self.qk_nope_head_dim, self.v_head_dim], dim=-1
447 + self.qk_nope_head_dim,
448 + self.v_head_dim,
449 )
450 # (L, N, V) -> (N, L, V)
451 replace_parameter(self, "W_UV", W_UV.transpose(0, 1), prefer_copy=True)