[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
@@ -314,6 +314,12 @@ honors each layer's activation mode; omit `--quantization`.
Mixed exports use the same command: SGLang dispatches each marked layer to its Mixed exports use the same command: SGLang dispatches each marked layer to its
serialized W4A4 or INT8 ConvRot kernel instead of applying one global method. serialized W4A4 or INT8 ConvRot kernel instead of applying one global method.
Comfy NVFP4 DiTs reuse SGLang's ModelOpt NVFP4 backend, which requires CUDA
compute capability 10.0 or newer. Pass a pruned FL2VA / Ref2VA file such as
`Abiray/Minimax-H3-nvfp4-INT4-INT8-Convrot/MiniMax_H3_FL2VA_pruned_nvfp4.safetensors`
to `--transformer-weights-path` and omit `--quantization`. SGLang infers the
packed group size and Comfy scale layout from the checkpoint; FSDP is rejected.
### Advanced: precomputed AdaLN cache ### Advanced: precomputed AdaLN cache
The [model card](https://huggingface.co/MiniMaxAI/MiniMax-H3) notes that about The [model card](https://huggingface.co/MiniMaxAI/MiniMax-H3) notes that about
+3 -3
View File
@@ -154,11 +154,11 @@ backend.
</tr> </tr>
<tr> <tr>
<td><code>modelopt-nvfp4</code></td> <td><code>modelopt-nvfp4</code></td>
<td>Mixed transformer directory/repo with <code>config.json</code>, raw NVFP4 safetensors export/repo, or full ModelOpt Diffusers repo</td> <td>Mixed transformer directory/repo with <code>config.json</code>, raw or Comfy layer-marked NVFP4 safetensors, or full ModelOpt Diffusers repo</td>
<td><code>--transformer-path</code> for mixed overrides; <code>--transformer-weights-path</code> for raw exports; <code>--model-path</code> for full repos</td> <td><code>--transformer-path</code> for mixed overrides; <code>--transformer-weights-path</code> for raw exports; <code>--model-path</code> for full repos</td>
<td>FLUX.1, FLUX.2, Wan2.2, Qwen Image, Qwen Image 2512, Qwen Image Edit, Qwen Image Edit 2511</td> <td>FLUX.1, FLUX.2, Wan2.2, Qwen Image, Qwen Image 2512, Qwen Image Edit, Qwen Image Edit 2511, MiniMax-H3</td>
<td>None</td> <td>None</td>
<td>Mixed override repos keep the base model separate; full Qwen Image exports can be loaded directly as <code>--model-path</code>; raw exports such as <code>black-forest-labs/FLUX.2-dev-NVFP4</code> still use the weights-path flow</td> <td>Mixed override repos keep the base model separate; full Qwen Image exports can be loaded directly as <code>--model-path</code>; raw exports such as <code>black-forest-labs/FLUX.2-dev-NVFP4</code> use the weights-path flow. Comfy markers select their checkpoint layout automatically; omit <code>--quantization</code>.</td>
</tr> </tr>
<tr> <tr>
<td><code>gguf</code></td> <td><code>gguf</code></td>
@@ -36,6 +36,7 @@ class QuantizationConfig(SRTQuantizationConfig):
# for quantization frameworks with a separate quantized model provided, e.g. Nunchaku # for quantization frameworks with a separate quantized model provided, e.g. Nunchaku
quantized_model_path: str | None = None quantized_model_path: str | None = None
checkpoint_uses_native_qkv_layout: bool = False checkpoint_uses_native_qkv_layout: bool = False
checkpoint_uses_comfy_quantization: bool = False
supports_srt_linear_layers: bool = False supports_srt_linear_layers: bool = False
def get_scaled_act_names(self) -> list[str]: def get_scaled_act_names(self) -> list[str]:
@@ -236,6 +236,7 @@ class ModelOptFp4Config(ModelOptQuantConfig):
checkpoint_uses_packed_qkv: bool = False, checkpoint_uses_packed_qkv: bool = False,
swap_weight_nibbles: bool = False, swap_weight_nibbles: bool = False,
checkpoint_weight_scale_layout: str = "linear", checkpoint_weight_scale_layout: str = "linear",
checkpoint_uses_comfy_quantization: bool = False,
) -> None: ) -> None:
super().__init__(exclude_modules, packed_modules_mapping) super().__init__(exclude_modules, packed_modules_mapping)
self.is_checkpoint_nvfp4_serialized = is_checkpoint_nvfp4_serialized 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.checkpoint_uses_packed_qkv = checkpoint_uses_packed_qkv
self.swap_weight_nibbles = swap_weight_nibbles self.swap_weight_nibbles = swap_weight_nibbles
self.checkpoint_weight_scale_layout = checkpoint_weight_scale_layout self.checkpoint_weight_scale_layout = checkpoint_weight_scale_layout
self.checkpoint_uses_comfy_quantization = checkpoint_uses_comfy_quantization
@classmethod @classmethod
def get_name(cls) -> str: def get_name(cls) -> str:
@@ -348,6 +350,9 @@ class ModelOptFp4Config(ModelOptQuantConfig):
checkpoint_weight_scale_layout=config.get( checkpoint_weight_scale_layout=config.get(
"checkpoint_weight_scale_layout", "linear" "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): def get_quant_method(self, layer: torch.nn.Module, prefix: str):
@@ -248,7 +248,10 @@ class TransformerLoader(ComponentLoader):
safetensors_list safetensors_list
) )
checkpoint_quant_config = resolve_minimax_h3_checkpoint_quantization( 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: if adaln_curve_shape is not None:
( (
@@ -9,6 +9,7 @@ from sglang.multimodal_gen.runtime.layers.quantization.configs.base_config impor
QuantizationConfig, QuantizationConfig,
) )
from sglang.multimodal_gen.runtime.utils.quantization_utils import ( from sglang.multimodal_gen.runtime.utils.quantization_utils import (
build_nvfp4_config_from_safetensors_list,
inspect_comfy_quant_markers, inspect_comfy_quant_markers,
resolve_comfy_checkpoint_quantization, resolve_comfy_checkpoint_quantization,
) )
@@ -47,7 +48,26 @@ def inspect_minimax_h3_safetensors(
def resolve_minimax_h3_checkpoint_quantization( def resolve_minimax_h3_checkpoint_quantization(
layer_markers: dict[str, dict[str, Any]], 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: ) -> 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) 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: if getattr(inferred_config, "group_size", None) is None:
inferred_config.group_size = getattr(existing_config, "group_size", 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 return inferred_config
@@ -194,6 +202,10 @@ class TransformerQuantLoadSpec:
or self.is_serialized_kitchen_int8 or self.is_serialized_kitchen_int8
or self.is_serialized_kitchen_w4a4 or self.is_serialized_kitchen_w4a4
or self.is_serialized_kitchen_w4a8 or self.is_serialized_kitchen_w4a8
or (
self.quant_config is not None
and self.quant_config.checkpoint_uses_comfy_quantization
)
or ( or (
_get_quant_config_name(self.quant_config) == "mxfp8" _get_quant_config_name(self.quant_config) == "mxfp8"
and self.quant_config.layer_markers is not None 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 and "layers" in quant_config_dict
): ):
layers = quant_config_dict.get("layers", {}) layers = quant_config_dict.get("layers", {})
file_quantized_modules.update( metadata_nvfp4_modules = {
layer_name layer_name
for layer_name, layer_cfg in layers.items() for layer_name, layer_cfg in layers.items()
if isinstance(layer_cfg, dict) and layer_cfg.get("format") == "nvfp4" 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) tensor_metadata = _read_safetensors_tensor_metadata(file_path)
with safe_open(file_path, framework="pt", device="cpu") as f: 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" "swizzled" if checkpoint_uses_swizzled_scales else "linear"
), ),
"swap_weight_nibbles": checkpoint_uses_swizzled_scales, "swap_weight_nibbles": checkpoint_uses_swizzled_scales,
"checkpoint_uses_comfy_quantization": checkpoint_uses_comfy_quant,
} }
) )
logger.info( logger.info(
@@ -1105,6 +1105,16 @@ class TestTransformerQuantHelpers(unittest.TestCase):
self.assertTrue(config.load_in_4bit) self.assertTrue(config.load_in_4bit)
def test_nvfp4_safetensors_inference_ignores_fp8_fallback_scales(self): 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: with tempfile.NamedTemporaryFile(suffix=".safetensors") as f:
save_file( save_file(
{ {
@@ -1127,6 +1137,7 @@ class TestTransformerQuantHelpers(unittest.TestCase):
), ),
}, },
f.name, f.name,
metadata=metadata,
) )
config = build_nvfp4_config_from_safetensors_list([f.name]) 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.assertNotIn("layers.0.attention.qkv", config.exclude_modules)
self.assertEqual(config.checkpoint_weight_scale_layout, "linear") self.assertEqual(config.checkpoint_weight_scale_layout, "linear")
self.assertFalse(config.swap_weight_nibbles) self.assertFalse(config.swap_weight_nibbles)
self.assertFalse(config.checkpoint_uses_comfy_quantization)
def test_nvfp4_safetensors_inference_uses_comfy_checkpoint_layout(self): def test_nvfp4_safetensors_inference_uses_comfy_checkpoint_layout(self):
with tempfile.NamedTemporaryFile(suffix=".safetensors") as f: 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.assertNotIn("layers.0.attention.qkv", config.exclude_modules)
self.assertEqual(config.checkpoint_weight_scale_layout, "swizzled") self.assertEqual(config.checkpoint_weight_scale_layout, "swizzled")
self.assertTrue(config.swap_weight_nibbles) 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): def test_builder_adds_diffusers_quant_type_for_nvfp4(self):
updated = _updated_quant_config( updated = _updated_quant_config(