[diffusion] fix: fix quantized qkv scales and missing-param policy for minimax-h3 (#35740)
This commit is contained in:
@@ -176,6 +176,39 @@ def _reorder_grouped_qkv_to_qkv(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _install_qkv_row_reorder(
|
||||||
|
param: torch.Tensor,
|
||||||
|
reorder: Callable[[torch.Tensor], torch.Tensor],
|
||||||
|
qkv_rows: int,
|
||||||
|
) -> None:
|
||||||
|
"""Reorder a per-output-row qkv parameter the same way its rows are reordered.
|
||||||
|
|
||||||
|
Applied to quantization metadata rather than the weight itself. Anything whose
|
||||||
|
leading dim is not the checkpoint's qkv row count is passed through: per-tensor
|
||||||
|
scales are scalars, and a swizzled block-scale layout is not row-indexed.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _maybe_reorder(loaded_weight: torch.Tensor) -> torch.Tensor:
|
||||||
|
if loaded_weight.dim() >= 2 and loaded_weight.shape[0] == qkv_rows:
|
||||||
|
return reorder(loaded_weight)
|
||||||
|
return loaded_weight
|
||||||
|
|
||||||
|
base_loader = (
|
||||||
|
param._weight_loader
|
||||||
|
if hasattr(param, "_weight_loader")
|
||||||
|
else param.weight_loader
|
||||||
|
)
|
||||||
|
|
||||||
|
def _weight_loader(p: torch.Tensor, loaded_weight: torch.Tensor) -> None:
|
||||||
|
base_loader(p, _maybe_reorder(loaded_weight))
|
||||||
|
|
||||||
|
if hasattr(param, "_weight_loader"):
|
||||||
|
param._weight_loader = _weight_loader
|
||||||
|
else:
|
||||||
|
param.weight_loader = _weight_loader
|
||||||
|
param.rank_local_weight_transform = _maybe_reorder
|
||||||
|
|
||||||
|
|
||||||
def _copy_grouped_qkv_tp_shard(
|
def _copy_grouped_qkv_tp_shard(
|
||||||
param: torch.Tensor,
|
param: torch.Tensor,
|
||||||
loaded_weight: torch.Tensor,
|
loaded_weight: torch.Tensor,
|
||||||
@@ -662,6 +695,17 @@ class MiniMaxH3Attention(nn.Module):
|
|||||||
# rank-local FSDP must reorder grouped QKV before selecting each shard
|
# rank-local FSDP must reorder grouped QKV before selecting each shard
|
||||||
weight.rank_local_weight_transform = _reorder_checkpoint_weight
|
weight.rank_local_weight_transform = _reorder_checkpoint_weight
|
||||||
|
|
||||||
|
# A quantized checkpoint stores metadata indexed by output row next to the
|
||||||
|
# rows themselves (NVFP4 block scales, fp8 per-channel scales). Those rows
|
||||||
|
# are permuted above, so the per-row metadata has to be permuted the same
|
||||||
|
# way. Row count is the gate: a swizzled scale layout is not row-indexed,
|
||||||
|
# and per-tensor scales are scalars, so both are passed through untouched.
|
||||||
|
qkv_rows = 3 * arch.num_attention_heads * arch.attention_head_dim
|
||||||
|
for name, param in self.qkv_proj.named_parameters(recurse=False):
|
||||||
|
if name == "weight":
|
||||||
|
continue
|
||||||
|
_install_qkv_row_reorder(param, _reorder_checkpoint_weight, qkv_rows)
|
||||||
|
|
||||||
def _forward_mps_streamed_attention(
|
def _forward_mps_streamed_attention(
|
||||||
self,
|
self,
|
||||||
x: torch.Tensor,
|
x: torch.Tensor,
|
||||||
@@ -1816,6 +1860,10 @@ class MiniMaxH3DiTModel(BaseDiT, LayerwiseOffloadableModuleMixin):
|
|||||||
|
|
||||||
def _mark_missing_params_required(self) -> None:
|
def _mark_missing_params_required(self) -> None:
|
||||||
for _, param in self.named_parameters():
|
for _, param in self.named_parameters():
|
||||||
|
# A quant method's create_weights() declares its own policy for scales
|
||||||
|
# it can synthesize (weight-only NVFP4 has no input_scale and marks it
|
||||||
|
# "ones"); claiming only undeclared params keeps that intact.
|
||||||
|
if getattr(param, "missing_param_init", None) is None:
|
||||||
param.missing_param_init = "error"
|
param.missing_param_init = "error"
|
||||||
|
|
||||||
def post_load_weights(self) -> None:
|
def post_load_weights(self) -> None:
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import unittest
|
||||||
|
|
||||||
|
import torch
|
||||||
|
|
||||||
|
from sglang.multimodal_gen.runtime.models.dits.minimax_h3 import (
|
||||||
|
_install_qkv_row_reorder,
|
||||||
|
)
|
||||||
|
|
||||||
|
QKV_ROWS = 12
|
||||||
|
|
||||||
|
|
||||||
|
def _install(reorder=lambda w: w.flip(0)):
|
||||||
|
param = torch.zeros(QKV_ROWS)
|
||||||
|
seen = []
|
||||||
|
param.weight_loader = lambda p, loaded_weight: seen.append(loaded_weight)
|
||||||
|
_install_qkv_row_reorder(param, reorder, QKV_ROWS)
|
||||||
|
return param, seen
|
||||||
|
|
||||||
|
|
||||||
|
class TestMiniMaxH3QkvScaleReorder(unittest.TestCase):
|
||||||
|
"""MiniMax-H3 permutes the fused qkv weight's output rows on load.
|
||||||
|
|
||||||
|
Quantization metadata indexed by output row (NVFP4 block scales, fp8
|
||||||
|
per-channel scales) has to move with those rows, or every scale lands on the
|
||||||
|
wrong row: the model loads and runs, and renders noise. Metadata that is not
|
||||||
|
row-indexed must be left alone, so the row count is the gate.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def test_row_indexed_metadata_is_reordered(self):
|
||||||
|
param, seen = _install()
|
||||||
|
scales = torch.arange(QKV_ROWS * 2, dtype=torch.float32).reshape(QKV_ROWS, 2)
|
||||||
|
param.weight_loader(param, scales)
|
||||||
|
self.assertTrue(torch.equal(seen[0], scales.flip(0)))
|
||||||
|
|
||||||
|
def test_metadata_with_another_row_count_is_passed_through(self):
|
||||||
|
param, seen = _install()
|
||||||
|
swizzled = torch.arange(QKV_ROWS, dtype=torch.float32).reshape(QKV_ROWS // 3, 3)
|
||||||
|
param.weight_loader(param, swizzled)
|
||||||
|
self.assertTrue(torch.equal(seen[0], swizzled))
|
||||||
|
|
||||||
|
def test_per_tensor_scale_is_passed_through(self):
|
||||||
|
param, seen = _install()
|
||||||
|
scale = torch.tensor(0.5)
|
||||||
|
param.weight_loader(param, scale)
|
||||||
|
self.assertTrue(torch.equal(seen[0], scale))
|
||||||
|
|
||||||
|
def test_rank_local_transform_follows_the_same_gate(self):
|
||||||
|
# rank-local FSDP reorders before selecting its shard, so it needs the
|
||||||
|
# transform rather than the wrapped loader.
|
||||||
|
param, _ = _install()
|
||||||
|
scales = torch.arange(QKV_ROWS * 2, dtype=torch.float32).reshape(QKV_ROWS, 2)
|
||||||
|
self.assertTrue(
|
||||||
|
torch.equal(param.rank_local_weight_transform(scales), scales.flip(0))
|
||||||
|
)
|
||||||
|
scale = torch.tensor(0.5)
|
||||||
|
self.assertTrue(torch.equal(param.rank_local_weight_transform(scale), scale))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user