[diffusion] feat: support loading comfy nvfp4 minimax h3 checkpoints (#36044)

This commit is contained in:
Mick
2026-08-24 22:57:02 +08:00
committed by GitHub
parent 76d1401881
commit 9b0007ed19
9 changed files with 123 additions and 6 deletions
@@ -36,6 +36,7 @@ class QuantizationConfig(SRTQuantizationConfig):
# for quantization frameworks with a separate quantized model provided, e.g. Nunchaku
quantized_model_path: str | None = None
checkpoint_uses_native_qkv_layout: bool = False
checkpoint_uses_comfy_quantization: bool = False
supports_srt_linear_layers: bool = False
def get_scaled_act_names(self) -> list[str]:
@@ -236,6 +236,7 @@ class ModelOptFp4Config(ModelOptQuantConfig):
checkpoint_uses_packed_qkv: bool = False,
swap_weight_nibbles: bool = False,
checkpoint_weight_scale_layout: str = "linear",
checkpoint_uses_comfy_quantization: bool = False,
) -> None:
super().__init__(exclude_modules, packed_modules_mapping)
self.is_checkpoint_nvfp4_serialized = is_checkpoint_nvfp4_serialized
@@ -248,6 +249,7 @@ class ModelOptFp4Config(ModelOptQuantConfig):
self.checkpoint_uses_packed_qkv = checkpoint_uses_packed_qkv
self.swap_weight_nibbles = swap_weight_nibbles
self.checkpoint_weight_scale_layout = checkpoint_weight_scale_layout
self.checkpoint_uses_comfy_quantization = checkpoint_uses_comfy_quantization
@classmethod
def get_name(cls) -> str:
@@ -348,6 +350,9 @@ class ModelOptFp4Config(ModelOptQuantConfig):
checkpoint_weight_scale_layout=config.get(
"checkpoint_weight_scale_layout", "linear"
),
checkpoint_uses_comfy_quantization=config.get(
"checkpoint_uses_comfy_quantization", False
),
)
def get_quant_method(self, layer: torch.nn.Module, prefix: str):
@@ -248,7 +248,10 @@ class TransformerLoader(ComponentLoader):
safetensors_list
)
checkpoint_quant_config = resolve_minimax_h3_checkpoint_quantization(
layer_markers
layer_markers,
safetensors_list,
dit_config.arch_config.param_names_mapping,
dit_config.arch_config.reverse_param_names_mapping,
)
if adaln_curve_shape is not None:
(
@@ -9,6 +9,7 @@ from sglang.multimodal_gen.runtime.layers.quantization.configs.base_config impor
QuantizationConfig,
)
from sglang.multimodal_gen.runtime.utils.quantization_utils import (
build_nvfp4_config_from_safetensors_list,
inspect_comfy_quant_markers,
resolve_comfy_checkpoint_quantization,
)
@@ -47,7 +48,26 @@ def inspect_minimax_h3_safetensors(
def resolve_minimax_h3_checkpoint_quantization(
layer_markers: dict[str, dict[str, Any]],
safetensors_list: list[str] | None = None,
param_names_mapping: dict | None = None,
reverse_param_names_mapping: dict | None = None,
) -> QuantizationConfig | None:
formats = {str(marker.get("format")) for marker in layer_markers.values()}
if formats == {"nvfp4"}:
if safetensors_list is None:
raise ValueError("MiniMax-H3 NVFP4 metadata requires checkpoint files")
config = build_nvfp4_config_from_safetensors_list(
safetensors_list,
param_names_mapping,
reverse_param_names_mapping,
)
if config is None:
raise ValueError("Could not resolve MiniMax-H3 NVFP4 checkpoint layout")
config.checkpoint_uses_comfy_quantization = True
config.checkpoint_uses_native_qkv_layout = True
config.checkpoint_weight_scale_layout = "swizzled"
config.swap_weight_nibbles = True
return config
return resolve_comfy_checkpoint_quantization(layer_markers)
@@ -141,6 +141,14 @@ def _merge_modelopt_fp4_configs(
)
if getattr(inferred_config, "group_size", None) is None:
inferred_config.group_size = getattr(existing_config, "group_size", None)
inferred_config.checkpoint_uses_comfy_quantization = (
inferred_config.checkpoint_uses_comfy_quantization
or existing_config.checkpoint_uses_comfy_quantization
)
inferred_config.checkpoint_uses_native_qkv_layout = (
inferred_config.checkpoint_uses_native_qkv_layout
or existing_config.checkpoint_uses_native_qkv_layout
)
return inferred_config
@@ -194,6 +202,10 @@ class TransformerQuantLoadSpec:
or self.is_serialized_kitchen_int8
or self.is_serialized_kitchen_w4a4
or self.is_serialized_kitchen_w4a8
or (
self.quant_config is not None
and self.quant_config.checkpoint_uses_comfy_quantization
)
or (
_get_quant_config_name(self.quant_config) == "mxfp8"
and self.quant_config.layer_markers is not None
@@ -674,11 +674,12 @@ def _build_nvfp4_config_from_safetensors_files(
and "layers" in quant_config_dict
):
layers = quant_config_dict.get("layers", {})
file_quantized_modules.update(
metadata_nvfp4_modules = {
layer_name
for layer_name, layer_cfg in layers.items()
if isinstance(layer_cfg, dict) and layer_cfg.get("format") == "nvfp4"
)
}
file_quantized_modules.update(metadata_nvfp4_modules)
tensor_metadata = _read_safetensors_tensor_metadata(file_path)
with safe_open(file_path, framework="pt", device="cpu") as f:
@@ -815,6 +816,7 @@ def _build_nvfp4_config_from_safetensors_files(
"swizzled" if checkpoint_uses_swizzled_scales else "linear"
),
"swap_weight_nibbles": checkpoint_uses_swizzled_scales,
"checkpoint_uses_comfy_quantization": checkpoint_uses_comfy_quant,
}
)
logger.info(
@@ -1105,6 +1105,16 @@ class TestTransformerQuantHelpers(unittest.TestCase):
self.assertTrue(config.load_in_4bit)
def test_nvfp4_safetensors_inference_ignores_fp8_fallback_scales(self):
metadata = {
"_quantization_metadata": json.dumps(
{
"format_version": "1.0",
"layers": {
"layers.0.attention.qkv": {"format": "nvfp4"},
},
}
)
}
with tempfile.NamedTemporaryFile(suffix=".safetensors") as f:
save_file(
{
@@ -1127,6 +1137,7 @@ class TestTransformerQuantHelpers(unittest.TestCase):
),
},
f.name,
metadata=metadata,
)
config = build_nvfp4_config_from_safetensors_list([f.name])
@@ -1137,6 +1148,7 @@ class TestTransformerQuantHelpers(unittest.TestCase):
self.assertNotIn("layers.0.attention.qkv", config.exclude_modules)
self.assertEqual(config.checkpoint_weight_scale_layout, "linear")
self.assertFalse(config.swap_weight_nibbles)
self.assertFalse(config.checkpoint_uses_comfy_quantization)
def test_nvfp4_safetensors_inference_uses_comfy_checkpoint_layout(self):
with tempfile.NamedTemporaryFile(suffix=".safetensors") as f:
@@ -1179,6 +1191,62 @@ class TestTransformerQuantHelpers(unittest.TestCase):
self.assertNotIn("layers.0.attention.qkv", config.exclude_modules)
self.assertEqual(config.checkpoint_weight_scale_layout, "swizzled")
self.assertTrue(config.swap_weight_nibbles)
self.assertTrue(config.checkpoint_uses_comfy_quantization)
self.assertFalse(config.checkpoint_uses_native_qkv_layout)
spec = TransformerQuantLoadSpec(
safetensors_list=[f.name],
quant_config=config,
nunchaku_config=None,
param_dtype=None,
)
self.assertTrue(spec.uses_comfy_layer_markers)
def test_minimax_h3_comfy_nvfp4_resolves_modelopt_backend(self):
metadata = {
"_quantization_metadata": json.dumps(
{
"format_version": "1.0",
"layers": {
"blocks.0.attn.qkv_proj": {"format": "nvfp4"},
},
}
)
}
with (
tempfile.NamedTemporaryFile(suffix=".safetensors") as quantized,
tempfile.NamedTemporaryFile(suffix=".safetensors") as fallback,
):
save_file(
{
"blocks.0.attn.qkv_proj.weight": torch.zeros(
(32, 8), dtype=torch.uint8
),
"blocks.0.attn.qkv_proj.weight_scale": torch.ones(
(32, 1), dtype=torch.float8_e4m3fn
),
"blocks.0.attn.qkv_proj.weight_scale_2": torch.tensor(1.0),
},
quantized.name,
metadata=metadata,
)
save_file(
{"blocks.0.mlp.fc1.weight": torch.ones((2, 2))},
fallback.name,
)
checkpoint_files = [quantized.name, fallback.name]
_, markers = inspect_minimax_h3_safetensors(checkpoint_files)
config = resolve_minimax_h3_checkpoint_quantization(
markers,
checkpoint_files,
)
self.assertIsInstance(config, ModelOptFp4Config)
self.assertEqual(config.group_size, 16)
self.assertIn("blocks.0.mlp.fc1", config.exclude_modules)
self.assertTrue(config.checkpoint_uses_comfy_quantization)
self.assertTrue(config.checkpoint_uses_native_qkv_layout)
self.assertEqual(config.checkpoint_weight_scale_layout, "swizzled")
self.assertTrue(config.swap_weight_nibbles)
def test_builder_adds_diffusers_quant_type_for_nvfp4(self):
updated = _updated_quant_config(