Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 66 additions & 4 deletions examples/minimax_h3/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,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, and
Expand All @@ -314,10 +376,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

Expand Down
48 changes: 47 additions & 1 deletion examples/minimax_h3/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@
ModelRuntimeConfig,
OffloadConfig,
ParallelConfig,
QuantConfig,
QuantKernelBackend,
QuantType,
WeightOffloadType,
)
from telefuser.core.module_manager import ModuleManager
Expand Down Expand Up @@ -127,6 +130,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,
*,
Expand All @@ -141,6 +172,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.")
Expand All @@ -162,6 +194,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]
Expand All @@ -171,13 +208,20 @@ 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=(
WeightOffloadType.NO_CPU_OFFLOAD if use_resident_modules else WeightOffloadType.MODEL_CPU_OFFLOAD
),
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)),
Expand All @@ -203,9 +247,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,
Expand Down Expand Up @@ -344,6 +389,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",
"run_minimax_h3_request",
"save_generation",
Expand Down
55 changes: 55 additions & 0 deletions examples/minimax_h3/minimax_h3_fl2va_bnb_nf4_h100.py
Original file line number Diff line number Diff line change
@@ -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()
16 changes: 15 additions & 1 deletion examples/minimax_h3/minimax_h3_fl2va_h100.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"feature_cache_model_type": "MiniMax-H3-Base",
"feature_cache_n_derivatives": 1,
"feature_cache_taylor_threshold": 2,
"quantization": None,
}


Expand Down Expand Up @@ -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
Expand All @@ -108,6 +110,7 @@ def get_pipeline(
n_derivatives=feature_cache_n_derivatives,
taylor_threshold=feature_cache_taylor_threshold,
),
quantization=quantization,
)


Expand Down Expand Up @@ -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"))
Expand All @@ -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(
"--attn-impl",
Expand Down Expand Up @@ -319,6 +328,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(
Expand All @@ -339,5 +349,9 @@ def main() -> None:
pipeline.stop()


def main() -> None:
_main()


if __name__ == "__main__":
main()
55 changes: 55 additions & 0 deletions examples/minimax_h3/minimax_h3_fl2va_torchao_fp8_h100.py
Original file line number Diff line number Diff line change
@@ -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()
Loading
Loading