From 3f73ffaf89e5a2f6e581164f2d34e07db052bded Mon Sep 17 00:00:00 2001 From: Uxito-Ada <414416158@qq.com> Date: Thu, 6 Aug 2026 01:59:13 +0000 Subject: [PATCH 1/3] Enable Online FP8&NF4 on Minimax-H3 --- examples/minimax_h3/README.md | 70 +++++++++++++++++-- examples/minimax_h3/common.py | 48 ++++++++++++- .../minimax_h3_fl2va_bnb_nf4_h100.py | 55 +++++++++++++++ examples/minimax_h3/minimax_h3_fl2va_h100.py | 16 ++++- .../minimax_h3_fl2va_torchao_fp8_h100.py | 55 +++++++++++++++ examples/minimax_h3/minimax_h3_ref2va_h100.py | 5 ++ .../minimax_h3/minimax_h3_request_h100.py | 5 ++ telefuser/models/minimax_h3_dit.py | 39 ++++++++++- telefuser/pipelines/minimax_h3/denoising.py | 15 ++++ 9 files changed, 301 insertions(+), 7 deletions(-) create mode 100644 examples/minimax_h3/minimax_h3_fl2va_bnb_nf4_h100.py create mode 100644 examples/minimax_h3/minimax_h3_fl2va_torchao_fp8_h100.py diff --git a/examples/minimax_h3/README.md b/examples/minimax_h3/README.md index 270be80..84462c0 100644 --- a/examples/minimax_h3/README.md +++ b/examples/minimax_h3/README.md @@ -290,6 +290,68 @@ The Ulysses degree must divide 56 attention heads. Scripts must run from their g processes can spawn safely. H100 examples request packed FlashAttention 4 and fall back to packed PyTorch SDPA when FlashAttention 4 is unavailable. +## Online DiT Quantization + +MiniMax H3 supports two single-GPU online quantization backends for the DiT transformer Linear layers: + +| CLI value | Backend | Weight/activation path | +|---|---|---| +| torchao-fp8 | TorchAO | FP8 dynamic activation and FP8 weight when supported, otherwise TorchAO's FP8 weight-only path | +| bnb-nf4 | bitsandbytes | NF4 weight-only with BF16 compute | + +Both paths convert the 258 Linear layers in the main and token-refiner transformer blocks. The FP32 video/audio +patch projections, timestep embedding, output projections, text encoder, and VAEs retain their reference dtypes. +The BF16 DiT is loaded from the original shards, moved to CUDA after text encoding, quantized on first denoising use, +and then kept resident for the pipeline lifetime. This ordering avoids a simultaneous BF16 text encoder and DiT on +one GPU and avoids unsupported CPU transfers of quantized tensor subclasses. +TorchAO conversion has a transient memory peak near the BF16 footprint; use the full 80 GB device without colocated workloads. + +Use the dedicated TorchAO FP8 example: + +~~~bash +python examples/minimax_h3/minimax_h3_fl2va_torchao_fp8_h100.py \ + --mode t2va \ + --duration 5 \ + --output outputs/minimax_h3_torchao_fp8.mp4 +~~~ + +Or the dedicated bitsandbytes NF4 example: + +~~~bash +python examples/minimax_h3/minimax_h3_fl2va_bnb_nf4_h100.py \ + --mode t2va \ + --duration 5 \ + --output outputs/minimax_h3_bnb_nf4.mp4 +~~~ + +The standard FL2VA, Ref2VA, and JSON request CLIs also accept +--quantization with either torchao-fp8 or bnb-nf4. The Python loader accepts the same names: + +~~~python +from examples.minimax_h3.common import load_minimax_h3_pipeline + +pipeline = load_minimax_h3_pipeline( + "/path/to/MiniMaxAI_MiniMax-H3", + partition="FL2VA", + quantization="torchao-fp8", +) +~~~ + +Online quantization currently requires ulysses_degree=1, tp_degree=1, and FSDP disabled. Quantizing before TP/FSDP +would invalidate those wrappers' BF16 parameter-sharding contract, so unsupported combinations fail before checkpoint +loading. + +For matched BF16/FP8/NF4 profiling, use the validation benchmark. It writes the synchronized MP4 plus a JSON report +containing load time, end-to-end generation time, stage timings, and denoising allocator peaks: + +~~~bash +python tools/validation/benchmark_minimax_h3_quantization.py \ + --backend torchao-fp8 \ + --duration 5 \ + --steps 50 \ + --output outputs/minimax_h3_torchao_fp8_50step.mp4 +~~~ + For multi-GPU resident profiles, `WorkerTensorChannel` transports text conditioning, visual condition rows, and the final video latent directly between worker groups. CUDA intermediates therefore do not stage through the parent process or CPU. The pipeline reports media, text, condition VAE, denoising, video/audio decode, allocator peak, DiT @@ -313,10 +375,10 @@ The standard four-GPU profile already uses Ulysses2 x TP2 and therefore leaves F `load_minimax_h3_pipeline` directly to construct another supported combination; the product of Ulysses and TP degrees must be 1, 2, or 4. -Ring attention, CFG parallelism, pipeline parallelism, sparse attention, quantization, and `torch.compile` are not -enabled for H3. Video-VAE parallelism is spatial tiling over the existing TP process group, not parameter tensor -parallelism. The dedicated service manifests expose the pipeline without adding framework-level configuration fields -or changing the shared request schema. +Ring attention, CFG parallelism, pipeline parallelism, sparse attention, and `torch.compile` are not enabled for H3. +Video-VAE parallelism is spatial tiling over the existing TP process group, not parameter tensor parallelism. The +dedicated service manifests expose the pipeline without adding framework-level configuration fields or changing the +shared request schema. ## Four-GPU Regression diff --git a/examples/minimax_h3/common.py b/examples/minimax_h3/common.py index 6c44a3c..8cff68f 100644 --- a/examples/minimax_h3/common.py +++ b/examples/minimax_h3/common.py @@ -19,6 +19,9 @@ ModelRuntimeConfig, OffloadConfig, ParallelConfig, + QuantConfig, + QuantKernelBackend, + QuantType, WeightOffloadType, ) from telefuser.core.module_manager import ModuleManager @@ -110,6 +113,34 @@ def _checkpoint_shards(component: Path) -> list[str]: return shards +def minimax_h3_quant_config(quantization: str | QuantType | None) -> QuantConfig: + """Resolve a public MiniMax H3 online-quantization name to runtime config.""" + if quantization is None: + return QuantConfig() + if isinstance(quantization, str): + normalized = quantization.strip().lower().replace("_", "-") + names = { + "torchao-fp8": QuantType.TORCHAO_FP8, + "bnb-nf4": QuantType.BNB_NF4, + } + try: + quant_type = names[normalized] + except KeyError as exc: + raise ValueError("quantization must be 'torchao-fp8', 'bnb-nf4', or None") from exc + elif isinstance(quantization, QuantType): + quant_type = quantization + else: + raise TypeError("quantization must be a string, QuantType, or None") + + backends = { + QuantType.TORCHAO_FP8: QuantKernelBackend.TORCHAO, + QuantType.BNB_NF4: QuantKernelBackend.BITSANDBYTES, + } + if quant_type not in backends: + raise ValueError(f"MiniMax H3 does not support online quantization type {quant_type.name}") + return QuantConfig(enabled=True, quant_type=quant_type, kernel_backend=backends[quant_type]) + + def load_minimax_h3_pipeline( model_root: str | Path, *, @@ -124,6 +155,7 @@ def load_minimax_h3_pipeline( feature_cache_config: FeatureCacheConfig | None = None, adaln_cache_path: str | Path | None = None, online_adaln_cache: bool = False, + quantization: str | QuantType | None = None, ) -> MiniMaxH3Pipeline: if adaln_cache_path is not None and online_adaln_cache: raise ValueError("Choose either adaln_cache_path or online_adaln_cache, not both.") @@ -145,6 +177,11 @@ def load_minimax_h3_pipeline( raise ValueError("enable_fsdp requires multi-GPU sequence parallelism without tensor parallelism") if (adaln_cache_path is not None or online_adaln_cache) and resolved_enable_fsdp: raise ValueError("AdaLN cache modes do not yet support FSDP deployment.") + quant_config = minimax_h3_quant_config(quantization) + if quant_config.enabled and world_size != 1: + raise ValueError("MiniMax H3 online quantization currently requires a single-GPU profile") + if quant_config.enabled and resolved_enable_fsdp: + raise ValueError("MiniMax H3 online quantization cannot be combined with FSDP") if isinstance(attn_impl, str): try: attn_impl = AttnImplType[attn_impl] @@ -154,6 +191,8 @@ def load_minimax_h3_pipeline( if not component_root.is_dir(): raise FileNotFoundError(f"MiniMax H3 partition not found: {component_root}") runtime_device = torch.device(device) + if quant_config.enabled and runtime_device.type != "cuda": + raise ValueError("MiniMax H3 online quantization requires a CUDA device") use_resident_modules = world_size > 1 or resolved_enable_fsdp resident_offload = OffloadConfig( offload_type=( @@ -161,6 +200,11 @@ def load_minimax_h3_pipeline( ), pin_cpu_memory=False, ) + dit_offload = ( + OffloadConfig(offload_type=WeightOffloadType.NO_CPU_OFFLOAD, pin_cpu_memory=False) + if quant_config.enabled + else resident_offload + ) text_parallel = ( ParallelConfig( device_ids=list(range(resolved_encoder_tp)), @@ -186,9 +230,10 @@ def load_minimax_h3_pipeline( device_type=runtime_device.type, device_id=runtime_device.index or 0, torch_dtype=torch.bfloat16, - offload_config=resident_offload, + offload_config=dit_offload, attention_config=AttentionConfig.dense_attention(attn_impl), feature_cache_config=feature_cache_config or FeatureCacheConfig(), + quant_config=quant_config, parallel_config=ParallelConfig( device_ids=list(range(world_size)), sp_ulysses_degree=ulysses_degree, @@ -327,6 +372,7 @@ def save_generation(result: MiniMaxH3Generation, output_path: str | Path) -> Non "load_minimax_h3_pipeline", "load_minimax_h3_request", "minimax_h3_adaln_cache_timesteps", + "minimax_h3_quant_config", "partition_for_minimax_h3_request", "save_generation", ] diff --git a/examples/minimax_h3/minimax_h3_fl2va_bnb_nf4_h100.py b/examples/minimax_h3/minimax_h3_fl2va_bnb_nf4_h100.py new file mode 100644 index 0000000..2164a86 --- /dev/null +++ b/examples/minimax_h3/minimax_h3_fl2va_bnb_nf4_h100.py @@ -0,0 +1,55 @@ +# SPDX-License-Identifier: Apache-2.0 +"""MiniMax H3 FL2VA example with bitsandbytes NF4 online quantization.""" + +from __future__ import annotations + +from copy import deepcopy +from functools import wraps + +if __package__: + from . import minimax_h3_fl2va_h100 as base +else: + try: + from examples.minimax_h3 import minimax_h3_fl2va_h100 as base + except ModuleNotFoundError: + import minimax_h3_fl2va_h100 as base + +PPL_CONFIG = { + **base.PPL_CONFIG, + "name": "minimax_h3_fl2va_bnb_nf4_h100", + "quantization": "bnb-nf4", +} +PIPELINE_MANIFEST = deepcopy(base.PIPELINE_MANIFEST) +PIPELINE_MANIFEST["pipeline_name"] = PPL_CONFIG["name"] + + +@wraps(base.run) +def run(*args: object, **kwargs: object) -> base.MiniMaxH3Generation: + return base.run(*args, **kwargs) + + +@wraps(base.run_with_file) +def run_with_file(*args: object, **kwargs: object) -> dict[str, str]: + return base.run_with_file(*args, **kwargs) + + +def get_pipeline( + parallelism: int = 1, + model_root: str = PPL_CONFIG["model_root"], + **kwargs: object, +) -> base.MiniMaxH3Pipeline: + """Load the single-GPU bitsandbytes NF4 FL2VA pipeline.""" + return base.get_pipeline( + parallelism, + model_root, + quantization=PPL_CONFIG["quantization"], + **kwargs, + ) + + +def main() -> None: + base._main(PPL_CONFIG["quantization"]) + + +if __name__ == "__main__": + main() diff --git a/examples/minimax_h3/minimax_h3_fl2va_h100.py b/examples/minimax_h3/minimax_h3_fl2va_h100.py index ff24620..95b6336 100644 --- a/examples/minimax_h3/minimax_h3_fl2va_h100.py +++ b/examples/minimax_h3/minimax_h3_fl2va_h100.py @@ -38,6 +38,7 @@ "feature_cache_model_type": "MiniMax-H3-Base", "feature_cache_n_derivatives": 1, "feature_cache_taylor_threshold": 2, + "quantization": None, } @@ -88,6 +89,7 @@ def get_pipeline( feature_cache_model_type: str = PPL_CONFIG["feature_cache_model_type"], feature_cache_n_derivatives: int = PPL_CONFIG["feature_cache_n_derivatives"], feature_cache_taylor_threshold: int = PPL_CONFIG["feature_cache_taylor_threshold"], + quantization: str | None = PPL_CONFIG["quantization"], ) -> MiniMaxH3Pipeline: """Load the FL2VA checkpoint partition for one, two, or four GPUs.""" tp_degree = 2 if parallelism == 4 else 1 @@ -108,6 +110,7 @@ def get_pipeline( n_derivatives=feature_cache_n_derivatives, taylor_threshold=feature_cache_taylor_threshold, ), + quantization=quantization, ) @@ -241,7 +244,7 @@ def run_with_file( return {"output_path": str(Path(output_path))} -def main() -> None: +def _main(default_quantization: str | None = PPL_CONFIG["quantization"]) -> None: parser = argparse.ArgumentParser(description="Generate MiniMax H3 T2VA/FL2VA audio-video on H100 GPUs") parser.add_argument("--model-root", default=PPL_CONFIG["model_root"]) parser.add_argument("--mode", choices=("t2va", "first-frame", "last-frame", "first-last")) @@ -265,6 +268,12 @@ def main() -> None: parser.add_argument("--flow-shift", type=float, default=PPL_CONFIG["flow_shift"]) parser.add_argument("--audio-flow-shift", type=float, default=PPL_CONFIG["audio_flow_shift"]) parser.add_argument("--device", default=PPL_CONFIG["device"]) + parser.add_argument( + "--quantization", + choices=("torchao-fp8", "bnb-nf4"), + default=default_quantization, + help="Online DiT Linear quantization backend (single GPU only).", + ) parser.add_argument("--gpu-num", "--ulysses-degree", dest="gpu_num", type=int, choices=(1, 2, 4), default=1) parser.add_argument("--enable-feature-cache", action="store_true") parser.add_argument("--feature-cache-model-type", default=PPL_CONFIG["feature_cache_model_type"]) @@ -313,6 +322,7 @@ def main() -> None: feature_cache_model_type=args.feature_cache_model_type, feature_cache_n_derivatives=args.feature_cache_n_derivatives, feature_cache_taylor_threshold=args.feature_cache_taylor_threshold, + quantization=args.quantization, ) try: result = run_with_file( @@ -333,5 +343,9 @@ def main() -> None: pipeline.stop() +def main() -> None: + _main() + + if __name__ == "__main__": main() diff --git a/examples/minimax_h3/minimax_h3_fl2va_torchao_fp8_h100.py b/examples/minimax_h3/minimax_h3_fl2va_torchao_fp8_h100.py new file mode 100644 index 0000000..1adc3ce --- /dev/null +++ b/examples/minimax_h3/minimax_h3_fl2va_torchao_fp8_h100.py @@ -0,0 +1,55 @@ +# SPDX-License-Identifier: Apache-2.0 +"""MiniMax H3 FL2VA example with TorchAO FP8 online quantization.""" + +from __future__ import annotations + +from copy import deepcopy +from functools import wraps + +if __package__: + from . import minimax_h3_fl2va_h100 as base +else: + try: + from examples.minimax_h3 import minimax_h3_fl2va_h100 as base + except ModuleNotFoundError: + import minimax_h3_fl2va_h100 as base + +PPL_CONFIG = { + **base.PPL_CONFIG, + "name": "minimax_h3_fl2va_torchao_fp8_h100", + "quantization": "torchao-fp8", +} +PIPELINE_MANIFEST = deepcopy(base.PIPELINE_MANIFEST) +PIPELINE_MANIFEST["pipeline_name"] = PPL_CONFIG["name"] + + +@wraps(base.run) +def run(*args: object, **kwargs: object) -> base.MiniMaxH3Generation: + return base.run(*args, **kwargs) + + +@wraps(base.run_with_file) +def run_with_file(*args: object, **kwargs: object) -> dict[str, str]: + return base.run_with_file(*args, **kwargs) + + +def get_pipeline( + parallelism: int = 1, + model_root: str = PPL_CONFIG["model_root"], + **kwargs: object, +) -> base.MiniMaxH3Pipeline: + """Load the single-GPU TorchAO FP8 FL2VA pipeline.""" + return base.get_pipeline( + parallelism, + model_root, + quantization=PPL_CONFIG["quantization"], + **kwargs, + ) + + +def main() -> None: + base._main(PPL_CONFIG["quantization"]) + + +if __name__ == "__main__": + main() diff --git a/examples/minimax_h3/minimax_h3_ref2va_h100.py b/examples/minimax_h3/minimax_h3_ref2va_h100.py index 26f2bf6..4f7dd99 100644 --- a/examples/minimax_h3/minimax_h3_ref2va_h100.py +++ b/examples/minimax_h3/minimax_h3_ref2va_h100.py @@ -34,6 +34,7 @@ "device": "cuda:0", "enable_fsdp": None, "online_adaln_cache": True, + "quantization": None, } PIPELINE_MANIFEST = build_pipeline_manifest( @@ -89,6 +90,7 @@ def get_pipeline( num_inference_steps: int = PPL_CONFIG["num_inference_steps"], enable_fsdp: bool | None = PPL_CONFIG["enable_fsdp"], online_adaln_cache: bool = PPL_CONFIG["online_adaln_cache"], + quantization: str | None = PPL_CONFIG["quantization"], ) -> MiniMaxH3Pipeline: """Load the Ref2VA checkpoint partition for one, two, or four GPUs.""" tp_degree = 2 if parallelism == 4 else 1 @@ -102,6 +104,7 @@ def get_pipeline( text_encoder_tp_degree=parallelism, enable_fsdp=enable_fsdp, online_adaln_cache=online_adaln_cache, + quantization=quantization, ) @@ -240,6 +243,7 @@ def main() -> None: parser.add_argument("--flow-shift", type=float, default=PPL_CONFIG["flow_shift"]) parser.add_argument("--audio-flow-shift", type=float, default=PPL_CONFIG["audio_flow_shift"]) parser.add_argument("--device", default=PPL_CONFIG["device"]) + parser.add_argument("--quantization", choices=("torchao-fp8", "bnb-nf4")) parser.add_argument("--gpu-num", "--ulysses-degree", dest="gpu_num", type=int, choices=(1, 2, 4), default=1) fsdp_group = parser.add_mutually_exclusive_group() fsdp_group.add_argument("--enable-fsdp", dest="enable_fsdp", action="store_true") @@ -257,6 +261,7 @@ def main() -> None: device=args.device, num_inference_steps=args.steps, enable_fsdp=args.enable_fsdp, + quantization=args.quantization, ) try: result = run_with_file( diff --git a/examples/minimax_h3/minimax_h3_request_h100.py b/examples/minimax_h3/minimax_h3_request_h100.py index 3524683..97ac702 100644 --- a/examples/minimax_h3/minimax_h3_request_h100.py +++ b/examples/minimax_h3/minimax_h3_request_h100.py @@ -25,6 +25,7 @@ "enable_fsdp": None, "adaln_cache_path": None, "online_adaln_cache": False, + "quantization": None, } @@ -45,6 +46,7 @@ def get_pipeline( enable_fsdp: bool | None = PPL_CONFIG["enable_fsdp"], adaln_cache_path: str | None = PPL_CONFIG["adaln_cache_path"], online_adaln_cache: bool = PPL_CONFIG["online_adaln_cache"], + quantization: str | None = PPL_CONFIG["quantization"], ) -> MiniMaxH3Pipeline: """Load the checkpoint partition required by a local JSON request.""" request = _load_request(request_path, num_inference_steps) @@ -61,6 +63,7 @@ def get_pipeline( enable_fsdp=enable_fsdp, adaln_cache_path=adaln_cache_path, online_adaln_cache=online_adaln_cache, + quantization=quantization, ) @@ -94,6 +97,7 @@ def main() -> None: parser.add_argument("--device", default=PPL_CONFIG["device"]) parser.add_argument("--adaln-cache", dest="adaln_cache_path", default=PPL_CONFIG["adaln_cache_path"]) parser.add_argument("--online-adaln-cache", action="store_true", default=PPL_CONFIG["online_adaln_cache"]) + parser.add_argument("--quantization", choices=("torchao-fp8", "bnb-nf4")) parser.add_argument("--gpu-num", "--ulysses-degree", dest="gpu_num", type=int, choices=(1, 2, 4), default=1) fsdp_group = parser.add_mutually_exclusive_group() fsdp_group.add_argument("--enable-fsdp", dest="enable_fsdp", action="store_true") @@ -111,6 +115,7 @@ def main() -> None: enable_fsdp=args.enable_fsdp, adaln_cache_path=args.adaln_cache_path, online_adaln_cache=args.online_adaln_cache, + quantization=args.quantization, ) try: result = run_with_file( diff --git a/telefuser/models/minimax_h3_dit.py b/telefuser/models/minimax_h3_dit.py index 30ea894..767192a 100644 --- a/telefuser/models/minimax_h3_dit.py +++ b/telefuser/models/minimax_h3_dit.py @@ -16,7 +16,7 @@ import torch.nn as nn from telefuser.core.base_model import BaseModel -from telefuser.core.config import AttentionConfig +from telefuser.core.config import AttentionConfig, QuantConfig, QuantType from telefuser.distributed.collectives import all_gather_cat, all_reduce_sum_ from telefuser.distributed.device_mesh import ( get_tp_group, @@ -31,6 +31,7 @@ from telefuser.ops import RMSNorm, apply_qk_norm_rope_neox, indexed_gate, indexed_scale_shift, silu_and_mul_reuse_input from telefuser.ops.attention import attention from telefuser.ops.rotary import apply_rotary_emb_neox +from telefuser.utils.logging import logger MINIMAX_H3_ADALN_MODALITY_NUM = 3 MINIMAX_H3_FP32_PARAM_NAMES = frozenset( @@ -1138,6 +1139,42 @@ def enable_usp(self, device_mesh: Any | None = None) -> None: for block in self.blocks: block.attn.set_ulysses_group(group) + def enable_quant(self, quant_type: QuantConfig | str | torch.dtype) -> None: + """Apply supported online quantization to transformer Linear layers.""" + if not isinstance(quant_type, QuantConfig): + super().enable_quant(quant_type) + return + if not quant_type.enabled: + return + + include_names = quant_type.quantize_modules or ("blocks.",) + if quant_type.quant_type == QuantType.TORCHAO_FP8: + from telefuser.ops.torchao_fp8_linear import replace_linear_layers_with_torchao_fp8 + + replaced = replace_linear_layers_with_torchao_fp8( + self, + include_names=include_names, + exclude_names=quant_type.skip_modules, + ) + self.torchao_fp8_replaced_linear = replaced + elif quant_type.quant_type == QuantType.BNB_NF4: + from telefuser.ops.bnb_nf4_linear import replace_linear_layers_with_bnb_nf4 + + replaced = replace_linear_layers_with_bnb_nf4( + self, + compute_dtype=torch.bfloat16, + include_names=include_names, + exclude_names=quant_type.skip_modules, + ) + self.bnb_nf4_replaced_linear = replaced + else: + raise ValueError(f"MiniMax H3 does not support online quantization type {quant_type.quant_type.name}") + + if replaced == 0: + raise RuntimeError("MiniMax H3 online quantization did not select any Linear layers") + self.quant_type = quant_type.quant_type + logger.info(f"MiniMax H3 {quant_type.quant_type.name} converted {replaced} transformer Linear layers") + def enable_tp(self, device_mesh: Any | None = None) -> None: self.device_mesh = device_mesh if device_mesh is not None else self.device_mesh world_size = get_tp_world_size(self.device_mesh) diff --git a/telefuser/pipelines/minimax_h3/denoising.py b/telefuser/pipelines/minimax_h3/denoising.py index 281055f..6be8f57 100644 --- a/telefuser/pipelines/minimax_h3/denoising.py +++ b/telefuser/pipelines/minimax_h3/denoising.py @@ -125,6 +125,20 @@ def __init__(self, module_manager: ModuleManager, model_runtime_config: ModelRun self.model_names = ["transformer"] self._request_serial = 0 + def _ensure_online_quantized(self) -> None: + quant_config = self.model_runtime_config.quant_config + if not quant_config.enabled: + return + if self.transformer.quant_type == quant_config.quant_type: + return + if self.transformer.quant_type is not None: + raise RuntimeError( + f"MiniMax H3 DiT is already quantized as {self.transformer.quant_type}, " + f"cannot apply {quant_config.quant_type}" + ) + self.transformer.enable_quant(quant_config) + current_platform.empty_cache() + def parallel_models(self) -> None: parallel_config = self.model_runtime_config.parallel_config unsupported = { @@ -222,6 +236,7 @@ def denoise( num_inference_steps: int, _transport_video: bool = False, ) -> MiniMaxH3DenoiseResult: + self._ensure_online_quantized() if isinstance(text, dict): text = MiniMaxH3TextCondition(**text) conditions = [ From aa42f354560a7fc4ea5048adfcf3e6a628ef8208 Mon Sep 17 00:00:00 2001 From: Uxito-Ada <414416158@qq.com> Date: Thu, 6 Aug 2026 02:20:58 +0000 Subject: [PATCH 2/3] fix(minimax-h3): align quantized example tests and docs --- PR_DESCRIPTION_MINIMAX_H3_QUANTIZATION.md | 92 ++++++++++++++++++ .../minimax_h3_quantization_benchmark.svg | 46 +++++++++ tests/unit/models/test_minimax_h3_dit.py | 44 +++++++++ .../pipelines/minimax_h3/test_examples.py | 70 +++++++++++++- .../pipelines/minimax_h3/test_parallelism.py | 26 ++++- .../pipelines/minimax_h3/test_pipeline.py | 13 +++ .../service/test_example_service_parity.py | 2 + tests/unit/test_example_registry.py | 2 + .../benchmark_minimax_h3_quantization.py | 95 +++++++++++++++++++ 9 files changed, 388 insertions(+), 2 deletions(-) create mode 100644 PR_DESCRIPTION_MINIMAX_H3_QUANTIZATION.md create mode 100644 docs/assets/minimax_h3_quantization_benchmark.svg create mode 100644 tools/validation/benchmark_minimax_h3_quantization.py diff --git a/PR_DESCRIPTION_MINIMAX_H3_QUANTIZATION.md b/PR_DESCRIPTION_MINIMAX_H3_QUANTIZATION.md new file mode 100644 index 0000000..6030d85 --- /dev/null +++ b/PR_DESCRIPTION_MINIMAX_H3_QUANTIZATION.md @@ -0,0 +1,92 @@ +# MiniMax H3 Online Quantization Support + +## Summary + +This change adds single-GPU online DiT quantization for MiniMax H3 with two backends: + +- TorchAO FP8 dynamic activation and weight quantization (`torchao-fp8`) +- bitsandbytes NF4 weight-only quantization with BF16 compute (`bnb-nf4`) + +The quantized DiT is loaded from the original BF16 checkpoint, moved to CUDA after text encoding, converted on the +first denoising request, and kept resident for the remainder of the pipeline lifetime. The implementation converts +258 Linear layers across the main and token-refiner transformer blocks while preserving the reference dtype of the +FP32 projections, text encoder, and VAEs. + +## Motivation + +MiniMax H3's BF16 DiT profile requires most of an 80 GB H100. Online quantization provides a practical single-GPU +deployment option while retaining the existing checkpoint format, pipeline API, service contracts, and audio-video +output format. + +## Implementation + +- Added `MiniMaxH3DiT.enable_quant()` dispatch for TorchAO FP8 and BNB NF4. +- Added public `quantization` loading support and validation for CUDA, single-GPU execution, and FSDP exclusion. +- Added first-use quantization and allocator cache release in the denoising stage. +- Added dedicated H100 examples: + - `examples/minimax_h3/minimax_h3_fl2va_torchao_fp8_h100.py` + - `examples/minimax_h3/minimax_h3_fl2va_bnb_nf4_h100.py` +- Added `--quantization` to the existing FL2VA, Ref2VA, and JSON request examples. +- Added a reproducible benchmark at `tools/validation/benchmark_minimax_h3_quantization.py`. +- Added model, pipeline, loader, CLI, registry, and service-contract parity tests. +- Documented the lifecycle, backend behavior, constraints, and commands in `examples/minimax_h3/README.md`. + +## Benchmark Method + +The matched benchmark uses one NVIDIA H100 80 GB, MiniMax H3 FL2VA, 768p 16:9 output, five seconds, 50 inference +steps, seed `0`, and the prompt: + +> Steam rises from the ramen while the family talks in the background. + +The memory value is the runtime allocator's peak allocated bytes converted to decimal GB. The throughput value is +end-to-end `generation_seconds / 50`, so lower values are better. BF16 was measured on an H100 of the same model; +the final FP8 and NF4 runs were measured serially on an idle H100 to avoid unrelated GPU contention. + +![MiniMax H3 quantization benchmark](docs/assets/minimax_h3_quantization_benchmark.svg) + +| Precision | Backend | Peak allocated memory | Peak reserved memory | Generation time | s/step | Change vs BF16 | +|---|---|---:|---:|---:|---:|---:| +| BF16 | Reference | 71.66 GB | 75.38 GB | 406.33 s | 8.13 s/step | Baseline | +| FP8 | TorchAO | 43.49 GB | 59.38 GB | 361.29 s | 7.23 s/step | 11.1% faster, 39.3% less allocated memory | +| NF4 | bitsandbytes | 22.62 GB | 55.97 GB | 378.05 s | 7.56 s/step | 7.0% faster, 68.4% less allocated memory | + +FP8's core denoising time is 283.67 s, 4.3% below BF16. NF4's core denoising time is 298.13 s, effectively neutral +and 0.6% above BF16; its main benefit is memory reduction and avoiding the BF16 DiT offload footprint. + +## Generated Video Comparison + +The video file cells are intentionally blank for attaching or embedding the final review media. + +| Precision | Backend | Video file | Visual/audio observation | +|---|---|---|---| +| BF16 | Reference | | Reference generation for comparison | +| FP8 | TorchAO | | PSNR 21.52, SSIM 0.729, audio cosine 0.626 versus BF16; composition and lighting remain close | +| NF4 | bitsandbytes | | PSNR 14.45, SSIM 0.472, audio cosine 0.283 versus BF16; coherent scene, but composition and details diverge | + +All three finalized artifacts are H.264 1344x768 at 24 fps with AAC 32 kHz stereo audio and 5.175 seconds of media. + +## Validation + +- Unit tests were not rerun after syncing latest main, as requested; the migrated test changes are included for CI review. +- Python source compilation, Ruff linting, formatting, and whitespace checks pass on the rebased files. +- `ruff check` and `ruff format --check` pass for all changed Python files. +- `git diff --check` passes. +- The benchmark numbers and generated-media comparison above come from the completed H100 validation of the implementation path. + +Unit tests and full GPU generation were intentionally not rerun after the fork synchronization; no claim is made here about post-sync test execution. + +## Constraints And Follow-Up + +- Online quantization currently requires one CUDA device, `tp_degree=1`, `ulysses_degree=1`, and FSDP disabled. +- TorchAO's first conversion has a transient memory peak near the BF16 footprint; an otherwise idle 80 GB H100 is + recommended. +- PSNR, SSIM, and audio cosine compare fixed-seed trajectories against BF16. They are regression indicators, not an + absolute perceptual quality score. + +## Contribution Checklist + +- [x] Code follows the repository's ruff and formatting rules. +- [x] Tests were added for new quantization and lifecycle behavior. +- [x] Documentation and runnable examples were updated. +- [x] Prior implementation validation includes real H100 generation checks; post-sync UT was intentionally not rerun. +- [ ] Review video attachments: intentionally left blank in the comparison table above. diff --git a/docs/assets/minimax_h3_quantization_benchmark.svg b/docs/assets/minimax_h3_quantization_benchmark.svg new file mode 100644 index 0000000..743c673 --- /dev/null +++ b/docs/assets/minimax_h3_quantization_benchmark.svg @@ -0,0 +1,46 @@ + + MiniMax H3 quantization memory and generation cost + Bars compare peak allocated memory in gigabytes. A red line compares end-to-end generation seconds per step. + + MiniMax H3 Online Quantization + Matched H100 benchmark: 768p, 5 seconds, 50 steps, seed 0 + + + + + + + + + + 020406080 + + + 6.57.07.58.08.5 + + + + + + + + + + 71.7 GB43.5 GB22.6 GB + BF16TorchAO FP8BNB NF4 + + + + + + + + 8.13 s/step7.23 s/step7.56 s/step + + + + Peak allocated memory (GB) + Generation time (s/step; lower is better) + + Left axis: memory. Right axis: end-to-end generation time divided by 50 steps. + diff --git a/tests/unit/models/test_minimax_h3_dit.py b/tests/unit/models/test_minimax_h3_dit.py index a099a37..0ddd1f6 100644 --- a/tests/unit/models/test_minimax_h3_dit.py +++ b/tests/unit/models/test_minimax_h3_dit.py @@ -4,6 +4,7 @@ import pytest import torch +from telefuser.core.config import QuantConfig, QuantType from telefuser.models.minimax_h3_dit import ( MINIMAX_H3_FP32_BUFFER_NAMES, MINIMAX_H3_FP32_PARAM_NAMES, @@ -480,3 +481,46 @@ def gather_rank_copies(tensor: torch.Tensor, *, dim: int, **_: object) -> torch. torch.testing.assert_close(actual_video, expected_video) torch.testing.assert_close(actual_audio, expected_audio) + + +@pytest.mark.parametrize( + ("quant_type", "helper_path", "count_attribute"), + [ + ( + QuantType.TORCHAO_FP8, + "telefuser.ops.torchao_fp8_linear.replace_linear_layers_with_torchao_fp8", + "torchao_fp8_replaced_linear", + ), + ( + QuantType.BNB_NF4, + "telefuser.ops.bnb_nf4_linear.replace_linear_layers_with_bnb_nf4", + "bnb_nf4_replaced_linear", + ), + ], +) +def test_online_quantization_selects_only_transformer_blocks( + monkeypatch: pytest.MonkeyPatch, + quant_type: QuantType, + helper_path: str, + count_attribute: str, +) -> None: + model = MiniMaxH3DiT(_small_config()) + calls = [] + + def fake_replace(module: torch.nn.Module, **kwargs: object) -> int: + calls.append((module, kwargs)) + return 15 + + monkeypatch.setattr(helper_path, fake_replace) + model.enable_quant(QuantConfig(enabled=True, quant_type=quant_type)) + + assert calls[0][0] is model + assert calls[0][1]["include_names"] == ("blocks.",) + assert getattr(model, count_attribute) == 15 + assert model.quant_type == quant_type + + +def test_online_quantization_rejects_unsupported_type() -> None: + model = MiniMaxH3DiT(_small_config()) + with pytest.raises(ValueError, match="does not support"): + model.enable_quant(QuantConfig(enabled=True, quant_type=QuantType.INT8)) diff --git a/tests/unit/pipelines/minimax_h3/test_examples.py b/tests/unit/pipelines/minimax_h3/test_examples.py index 634d6b6..f6b6d12 100644 --- a/tests/unit/pipelines/minimax_h3/test_examples.py +++ b/tests/unit/pipelines/minimax_h3/test_examples.py @@ -3,15 +3,19 @@ import pytest +from examples.minimax_h3 import minimax_h3_fl2va_bnb_nf4_h100 as bnb_nf4_example from examples.minimax_h3 import minimax_h3_fl2va_h100 as fl2va_example +from examples.minimax_h3 import minimax_h3_fl2va_torchao_fp8_h100 as torchao_fp8_example from examples.minimax_h3 import minimax_h3_ref2va_h100 as ref2va_example from examples.minimax_h3 import minimax_h3_request_h100 as request_example from examples.minimax_h3.common import ( MINIMAX_H3_DEFAULT_FL2VA_IMAGE, MINIMAX_H3_DEFAULT_REF2VA_AUDIO, MINIMAX_H3_DEFAULT_REF2VA_VIDEO, + load_minimax_h3_pipeline, load_minimax_h3_request, minimax_h3_adaln_cache_timesteps, + minimax_h3_quant_config, partition_for_minimax_h3_request, ) from examples.minimax_h3.minimax_h3_cache_calibrate import _apply_cache_profile @@ -21,7 +25,7 @@ default_ref2va_conditions, parse_ref2va_ordered_materials, ) -from telefuser.core.config import AttnImplType, FeatureCacheConfig +from telefuser.core.config import AttnImplType, FeatureCacheConfig, QuantKernelBackend, QuantType from telefuser.pipelines.minimax_h3.task_profiles import MINIMAX_H3_FINITE_ASPECT_RATIOS from telefuser.service.core.pipeline_contract import PipelineContract @@ -139,6 +143,7 @@ def fake_loader(model_root: str, **kwargs: object) -> object: n_derivatives=1, taylor_threshold=2, ), + "quantization": None, }, ) ] @@ -159,6 +164,69 @@ def test_cache_calibration_applies_validated_h3_profile(tmp_path: Path) -> None: assert params == {"K": 2, "retention_ratio": 0.2, "thresh": 0.03} +@pytest.mark.parametrize( + ("name", "quant_type", "backend"), + [ + ("torchao-fp8", QuantType.TORCHAO_FP8, QuantKernelBackend.TORCHAO), + ("torchao_fp8", QuantType.TORCHAO_FP8, QuantKernelBackend.TORCHAO), + ("bnb-nf4", QuantType.BNB_NF4, QuantKernelBackend.BITSANDBYTES), + ], +) +def test_quantization_names_resolve_to_runtime_config( + name: str, + quant_type: QuantType, + backend: QuantKernelBackend, +) -> None: + config = minimax_h3_quant_config(name) + assert config.enabled is True + assert config.quant_type == quant_type + assert config.kernel_backend == backend + + +def test_quantization_rejects_unsupported_parallel_and_cpu_profiles(tmp_path: Path) -> None: + with pytest.raises(ValueError, match="single-GPU"): + load_minimax_h3_pipeline( + tmp_path, + partition="FL2VA", + ulysses_degree=2, + quantization="torchao-fp8", + ) + + (tmp_path / "FL2VA").mkdir() + with pytest.raises(ValueError, match="CUDA"): + load_minimax_h3_pipeline( + tmp_path, + partition="FL2VA", + device="cpu", + quantization="bnb-nf4", + ) + + +@pytest.mark.parametrize( + ("example", "quantization"), + [ + (torchao_fp8_example, "torchao-fp8"), + (bnb_nf4_example, "bnb-nf4"), + ], +) +def test_dedicated_quantized_examples_forward_fixed_backend( + monkeypatch: pytest.MonkeyPatch, + example: object, + quantization: str, +) -> None: + calls = [] + sentinel = object() + + def fake_get_pipeline(*args: object, **kwargs: object) -> object: + calls.append((args, kwargs)) + return sentinel + + monkeypatch.setattr(example.base, "get_pipeline", fake_get_pipeline) + assert example.get_pipeline(1, "/models/h3", device="cuda:1") is sentinel + assert calls == [((1, "/models/h3"), {"device": "cuda:1", "quantization": quantization})] + assert example.PIPELINE_MANIFEST["pipeline_name"] == example.PPL_CONFIG["name"] + + def test_fl2va_run_maps_standard_service_tasks_to_model_conditions() -> None: calls = [] marker = object() diff --git a/tests/unit/pipelines/minimax_h3/test_parallelism.py b/tests/unit/pipelines/minimax_h3/test_parallelism.py index 1179477..379366a 100644 --- a/tests/unit/pipelines/minimax_h3/test_parallelism.py +++ b/tests/unit/pipelines/minimax_h3/test_parallelism.py @@ -3,7 +3,14 @@ import pytest import torch -from telefuser.core.config import ModelRuntimeConfig, OffloadConfig, ParallelConfig, WeightOffloadType +from telefuser.core.config import ( + ModelRuntimeConfig, + OffloadConfig, + ParallelConfig, + QuantConfig, + QuantType, + WeightOffloadType, +) from telefuser.pipelines.minimax_h3.denoising import ( MiniMaxH3DenoisingStage, _build_local_embedding_layout, @@ -116,6 +123,23 @@ def test_parallel_models_rejects_tp_with_fsdp() -> None: stage.parallel_models() +def test_online_quantization_is_applied_once_after_stage_onload() -> None: + stage, transformer = _stage(ParallelConfig()) + stage.model_runtime_config.quant_config = QuantConfig(enabled=True, quant_type=QuantType.TORCHAO_FP8) + transformer.quant_type = None + + def enable_quant(config: QuantConfig) -> None: + transformer.quant_type = config.quant_type + + transformer.enable_quant.side_effect = enable_quant + with patch("telefuser.pipelines.minimax_h3.denoising.current_platform.empty_cache") as empty_cache: + stage._ensure_online_quantized() + stage._ensure_online_quantized() + + transformer.enable_quant.assert_called_once_with(stage.model_runtime_config.quant_config) + empty_cache.assert_called_once_with() + + def test_text_encoder_direct_handoff_keeps_token_tags_on_cpu() -> None: manager = MagicMock() manager.fetch_module.return_value = MagicMock() diff --git a/tests/unit/pipelines/minimax_h3/test_pipeline.py b/tests/unit/pipelines/minimax_h3/test_pipeline.py index d18e71e..c68206f 100644 --- a/tests/unit/pipelines/minimax_h3/test_pipeline.py +++ b/tests/unit/pipelines/minimax_h3/test_pipeline.py @@ -480,6 +480,19 @@ def init(self, manager, config) -> None: assert config.video_vae_config.parallel_config.tp_degree == 4 assert config.audio_vae_config.offload_config.offload_type is WeightOffloadType.NO_CPU_OFFLOAD + common.load_minimax_h3_pipeline( + tmp_path, + partition="Ref2VA", + device="cuda:0", + quantization="torchao-fp8", + ) + quantized_config = captured["config"] + assert quantized_config.dit_config.quant_config.enabled is True + assert quantized_config.dit_config.offload_config.offload_type is WeightOffloadType.NO_CPU_OFFLOAD + assert quantized_config.text_encoder_config.offload_config.offload_type is WeightOffloadType.MODEL_CPU_OFFLOAD + assert quantized_config.video_vae_config.offload_config.offload_type is WeightOffloadType.MODEL_CPU_OFFLOAD + assert quantized_config.audio_vae_config.offload_config.offload_type is WeightOffloadType.MODEL_CPU_OFFLOAD + def test_example_writer_preserves_complete_generated_audio( monkeypatch: pytest.MonkeyPatch, diff --git a/tests/unit/service/test_example_service_parity.py b/tests/unit/service/test_example_service_parity.py index 257f0fa..7106b33 100644 --- a/tests/unit/service/test_example_service_parity.py +++ b/tests/unit/service/test_example_service_parity.py @@ -29,6 +29,8 @@ SERVICE_EXAMPLES = { "wan21_i2v_service": (Path("examples/wan_video/wan21_14b_image_to_video_480p_service.py"), "i2v", True), "minimax_h3_fl2va": (Path("examples/minimax_h3/minimax_h3_fl2va_h100.py"), "t2v", True), + "minimax_h3_fl2va_torchao_fp8": (Path("examples/minimax_h3/minimax_h3_fl2va_torchao_fp8_h100.py"), "t2v", True), + "minimax_h3_fl2va_bnb_nf4": (Path("examples/minimax_h3/minimax_h3_fl2va_bnb_nf4_h100.py"), "t2v", True), "minimax_h3_ref2va": (Path("examples/minimax_h3/minimax_h3_ref2va_h100.py"), "s2v", True), "wan22_i2v_distill": (Path("examples/wan_video/wan22_14b_image_to_video_distill_h100.py"), "i2v", True), "lingbot_video_dense": (Path("examples/lingbot_video/lingbot_video_dense_1_3b.py"), "t2i", True), diff --git a/tests/unit/test_example_registry.py b/tests/unit/test_example_registry.py index a56fb12..1925e87 100644 --- a/tests/unit/test_example_registry.py +++ b/tests/unit/test_example_registry.py @@ -15,6 +15,8 @@ "lingbot_video/lingbot_video_dense_1_3b.py", "lingbot_video/lingbot_video_moe_30b.py", "minimax_h3/minimax_h3_fl2va_h100.py", + "minimax_h3/minimax_h3_fl2va_torchao_fp8_h100.py", + "minimax_h3/minimax_h3_fl2va_bnb_nf4_h100.py", "minimax_h3/minimax_h3_ref2va_h100.py", } diff --git a/tools/validation/benchmark_minimax_h3_quantization.py b/tools/validation/benchmark_minimax_h3_quantization.py new file mode 100644 index 0000000..6eb89b4 --- /dev/null +++ b/tools/validation/benchmark_minimax_h3_quantization.py @@ -0,0 +1,95 @@ +# SPDX-License-Identifier: Apache-2.0 +"""Benchmark MiniMax H3 BF16 and online-quantized single-GPU profiles.""" + +from __future__ import annotations + +import argparse +import json +import time +from importlib import metadata +from pathlib import Path + +from examples.minimax_h3.common import load_minimax_h3_pipeline, save_generation +from telefuser.core.config import AttnImplType + + +def _package_version(name: str) -> str | None: + try: + return metadata.version(name) + except metadata.PackageNotFoundError: + return None + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--model-root", default="/hhb-data/aigc/model_zoo/MiniMaxAI_MiniMax-H3") + parser.add_argument("--backend", choices=("bf16", "torchao-fp8", "bnb-nf4"), required=True) + parser.add_argument("--prompt", default="Steam rises from the ramen while the family talks in the background.") + parser.add_argument("--duration", type=float, default=5.0) + parser.add_argument("--steps", type=int, default=50) + parser.add_argument("--seed", type=int, default=0) + parser.add_argument("--aspect-ratio", default="16:9") + parser.add_argument("--device", default="cuda:0") + parser.add_argument("--output", type=Path, required=True) + parser.add_argument("--metrics-json", type=Path) + args = parser.parse_args() + + quantization = None if args.backend == "bf16" else args.backend + load_started = time.perf_counter() + pipeline = load_minimax_h3_pipeline( + args.model_root, + partition="FL2VA", + device=args.device, + num_inference_steps=args.steps, + attn_impl=AttnImplType.FLASH_ATTN_4, + quantization=quantization, + ) + load_seconds = time.perf_counter() - load_started + try: + generation_started = time.perf_counter() + result = pipeline( + task="t2va", + prompt=args.prompt, + conditions=[], + target={ + "short_edge": 768, + "aspect_ratio": args.aspect_ratio, + "duration_seconds": args.duration, + }, + seed=args.seed, + ) + generation_seconds = time.perf_counter() - generation_started + save_started = time.perf_counter() + save_generation(result, args.output) + save_seconds = time.perf_counter() - save_started + finally: + pipeline.stop() + + report = { + "backend": args.backend, + "model_root": str(Path(args.model_root)), + "output": str(args.output), + "prompt": args.prompt, + "duration_seconds": args.duration, + "num_inference_steps": args.steps, + "seed": args.seed, + "aspect_ratio": args.aspect_ratio, + "load_seconds": load_seconds, + "generation_seconds": generation_seconds, + "save_seconds": save_seconds, + "runtime_metrics": result.runtime_metrics, + "versions": { + "torch": _package_version("torch"), + "torchao": _package_version("torchao"), + "bitsandbytes": _package_version("bitsandbytes"), + "telefuser": _package_version("telefuser"), + }, + } + metrics_path = args.metrics_json or args.output.with_suffix(".metrics.json") + metrics_path.parent.mkdir(parents=True, exist_ok=True) + metrics_path.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="utf-8") + print(json.dumps(report, indent=2, sort_keys=True)) + + +if __name__ == "__main__": + main() From 0481c216120b60d79888d8582b23e20e7359f3e1 Mon Sep 17 00:00:00 2001 From: Uxito-Ada <414416158@qq.com> Date: Thu, 6 Aug 2026 02:23:14 +0000 Subject: [PATCH 3/3] chore: remove PR-only benchmark artifacts --- PR_DESCRIPTION_MINIMAX_H3_QUANTIZATION.md | 92 ------------------- .../minimax_h3_quantization_benchmark.svg | 46 ---------- 2 files changed, 138 deletions(-) delete mode 100644 PR_DESCRIPTION_MINIMAX_H3_QUANTIZATION.md delete mode 100644 docs/assets/minimax_h3_quantization_benchmark.svg diff --git a/PR_DESCRIPTION_MINIMAX_H3_QUANTIZATION.md b/PR_DESCRIPTION_MINIMAX_H3_QUANTIZATION.md deleted file mode 100644 index 6030d85..0000000 --- a/PR_DESCRIPTION_MINIMAX_H3_QUANTIZATION.md +++ /dev/null @@ -1,92 +0,0 @@ -# MiniMax H3 Online Quantization Support - -## Summary - -This change adds single-GPU online DiT quantization for MiniMax H3 with two backends: - -- TorchAO FP8 dynamic activation and weight quantization (`torchao-fp8`) -- bitsandbytes NF4 weight-only quantization with BF16 compute (`bnb-nf4`) - -The quantized DiT is loaded from the original BF16 checkpoint, moved to CUDA after text encoding, converted on the -first denoising request, and kept resident for the remainder of the pipeline lifetime. The implementation converts -258 Linear layers across the main and token-refiner transformer blocks while preserving the reference dtype of the -FP32 projections, text encoder, and VAEs. - -## Motivation - -MiniMax H3's BF16 DiT profile requires most of an 80 GB H100. Online quantization provides a practical single-GPU -deployment option while retaining the existing checkpoint format, pipeline API, service contracts, and audio-video -output format. - -## Implementation - -- Added `MiniMaxH3DiT.enable_quant()` dispatch for TorchAO FP8 and BNB NF4. -- Added public `quantization` loading support and validation for CUDA, single-GPU execution, and FSDP exclusion. -- Added first-use quantization and allocator cache release in the denoising stage. -- Added dedicated H100 examples: - - `examples/minimax_h3/minimax_h3_fl2va_torchao_fp8_h100.py` - - `examples/minimax_h3/minimax_h3_fl2va_bnb_nf4_h100.py` -- Added `--quantization` to the existing FL2VA, Ref2VA, and JSON request examples. -- Added a reproducible benchmark at `tools/validation/benchmark_minimax_h3_quantization.py`. -- Added model, pipeline, loader, CLI, registry, and service-contract parity tests. -- Documented the lifecycle, backend behavior, constraints, and commands in `examples/minimax_h3/README.md`. - -## Benchmark Method - -The matched benchmark uses one NVIDIA H100 80 GB, MiniMax H3 FL2VA, 768p 16:9 output, five seconds, 50 inference -steps, seed `0`, and the prompt: - -> Steam rises from the ramen while the family talks in the background. - -The memory value is the runtime allocator's peak allocated bytes converted to decimal GB. The throughput value is -end-to-end `generation_seconds / 50`, so lower values are better. BF16 was measured on an H100 of the same model; -the final FP8 and NF4 runs were measured serially on an idle H100 to avoid unrelated GPU contention. - -![MiniMax H3 quantization benchmark](docs/assets/minimax_h3_quantization_benchmark.svg) - -| Precision | Backend | Peak allocated memory | Peak reserved memory | Generation time | s/step | Change vs BF16 | -|---|---|---:|---:|---:|---:|---:| -| BF16 | Reference | 71.66 GB | 75.38 GB | 406.33 s | 8.13 s/step | Baseline | -| FP8 | TorchAO | 43.49 GB | 59.38 GB | 361.29 s | 7.23 s/step | 11.1% faster, 39.3% less allocated memory | -| NF4 | bitsandbytes | 22.62 GB | 55.97 GB | 378.05 s | 7.56 s/step | 7.0% faster, 68.4% less allocated memory | - -FP8's core denoising time is 283.67 s, 4.3% below BF16. NF4's core denoising time is 298.13 s, effectively neutral -and 0.6% above BF16; its main benefit is memory reduction and avoiding the BF16 DiT offload footprint. - -## Generated Video Comparison - -The video file cells are intentionally blank for attaching or embedding the final review media. - -| Precision | Backend | Video file | Visual/audio observation | -|---|---|---|---| -| BF16 | Reference | | Reference generation for comparison | -| FP8 | TorchAO | | PSNR 21.52, SSIM 0.729, audio cosine 0.626 versus BF16; composition and lighting remain close | -| NF4 | bitsandbytes | | PSNR 14.45, SSIM 0.472, audio cosine 0.283 versus BF16; coherent scene, but composition and details diverge | - -All three finalized artifacts are H.264 1344x768 at 24 fps with AAC 32 kHz stereo audio and 5.175 seconds of media. - -## Validation - -- Unit tests were not rerun after syncing latest main, as requested; the migrated test changes are included for CI review. -- Python source compilation, Ruff linting, formatting, and whitespace checks pass on the rebased files. -- `ruff check` and `ruff format --check` pass for all changed Python files. -- `git diff --check` passes. -- The benchmark numbers and generated-media comparison above come from the completed H100 validation of the implementation path. - -Unit tests and full GPU generation were intentionally not rerun after the fork synchronization; no claim is made here about post-sync test execution. - -## Constraints And Follow-Up - -- Online quantization currently requires one CUDA device, `tp_degree=1`, `ulysses_degree=1`, and FSDP disabled. -- TorchAO's first conversion has a transient memory peak near the BF16 footprint; an otherwise idle 80 GB H100 is - recommended. -- PSNR, SSIM, and audio cosine compare fixed-seed trajectories against BF16. They are regression indicators, not an - absolute perceptual quality score. - -## Contribution Checklist - -- [x] Code follows the repository's ruff and formatting rules. -- [x] Tests were added for new quantization and lifecycle behavior. -- [x] Documentation and runnable examples were updated. -- [x] Prior implementation validation includes real H100 generation checks; post-sync UT was intentionally not rerun. -- [ ] Review video attachments: intentionally left blank in the comparison table above. diff --git a/docs/assets/minimax_h3_quantization_benchmark.svg b/docs/assets/minimax_h3_quantization_benchmark.svg deleted file mode 100644 index 743c673..0000000 --- a/docs/assets/minimax_h3_quantization_benchmark.svg +++ /dev/null @@ -1,46 +0,0 @@ - - MiniMax H3 quantization memory and generation cost - Bars compare peak allocated memory in gigabytes. A red line compares end-to-end generation seconds per step. - - MiniMax H3 Online Quantization - Matched H100 benchmark: 768p, 5 seconds, 50 steps, seed 0 - - - - - - - - - - 020406080 - - - 6.57.07.58.08.5 - - - - - - - - - - 71.7 GB43.5 GB22.6 GB - BF16TorchAO FP8BNB NF4 - - - - - - - - 8.13 s/step7.23 s/step7.56 s/step - - - - Peak allocated memory (GB) - Generation time (s/step; lower is better) - - Left axis: memory. Right axis: end-to-end generation time divided by 50 steps. -