[Model][Feat] MiniMax H3 online FP8 support (#5737)

Signed-off-by: mglyn <1203789601@qq.com>
This commit is contained in:
Mu GuanLin authored and GitHub committed 2026-08-04 11:35:01 +00:00
1 parent 5215e03a91
commit b9a51592f0
8 files changed
+742 -62

No files matched your search

@@ -16,6 +16,17 @@ pytestmark = [
MODEL_ENV = "VLLM_TEST_MINIMAX_H3_FL2VA_MODEL"
def _assert_joint_output(outputs):
assert len(outputs) == 1
frames = np.asarray(outputs[0].images[0])
assert frames.shape == (39, 256, 448, 3)
multimodal = outputs[0].multimodal_output
assert multimodal is not None
assert np.asarray(multimodal["audio"]).shape[1] == 2
assert multimodal["audio_sample_rate"] == 32000
assert multimodal["fps"] == 24
@pytest.mark.skipif(
not os.environ.get(MODEL_ENV),
reason=f"set {MODEL_ENV} to an authorized FL2VA checkpoint path",
@@ -59,11 +70,56 @@ def test_minimax_h3_t2va_ulysses8_smoke():
finally:
engine.close()
assert len(outputs) == 1
frames = np.asarray(outputs[0].images[0])
assert frames.shape == (39, 256, 448, 3)
multimodal = outputs[0].multimodal_output
assert multimodal is not None
assert np.asarray(multimodal["audio"]).shape[1] == 2
assert multimodal["audio_sample_rate"] == 32000
assert multimodal["fps"] == 24
_assert_joint_output(outputs)
@pytest.mark.skipif(
not os.environ.get(MODEL_ENV),
reason=f"set {MODEL_ENV} to an authorized FL2VA checkpoint path",
)
def test_minimax_h3_t2va_fp8_single_gpu_smoke():
import torch
from vllm_omni.diffusion.data import DiffusionParallelConfig
from vllm_omni.entrypoints.omni import Omni
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
if torch.accelerator.device_count() < 1:
pytest.skip("MiniMax H3 FP8 smoke requires a CUDA device")
engine = Omni(
model=os.environ[MODEL_ENV],
quantization_config={
"transformer": {"method": "fp8"},
"text_encoder": None,
"video_vae": None,
"audio_vae": None,
},
parallel_config=DiffusionParallelConfig(ulysses_degree=1),
trust_remote_code=True,
enable_cpu_offload=True,
enforce_eager=True,
)
try:
outputs = engine.generate(
"A quiet cinematic night scene with matching ambient sound.",
OmniDiffusionSamplingParams(
height=256,
width=448,
num_frames=29,
fps=24,
num_inference_steps=2,
seed=42,
output_type="np",
extra_args={
"task": "t2va",
"flow_shift": 12.0,
"audio_flow_shift": 3.0,
},
),
use_tqdm=False,
)
finally:
engine.close()
_assert_joint_output(outputs)
@@ -0,0 +1,213 @@
# SPDX-License-Identifier: Apache-2.0
from types import SimpleNamespace
from unittest.mock import Mock
import pytest
import torch
import torch.nn as nn
pytestmark = [pytest.mark.core_model, pytest.mark.cpu, pytest.mark.diffusion]
class _FakeLinear(nn.Module):
def __init__(
self,
*args,
bias=False,
params_dtype=None,
quant_config=None,
prefix="",
total_num_heads=None,
total_num_kv_heads=None,
**kwargs,
):
del args, kwargs
super().__init__()
self.prefix = prefix
self.quant_config = quant_config
self.weight = nn.Parameter(torch.empty(1, dtype=params_dtype))
if bias:
self.bias = nn.Parameter(torch.empty(1, dtype=params_dtype))
else:
self.register_parameter("bias", None)
self.num_heads = total_num_heads
self.num_kv_heads = total_num_kv_heads
class _FakeAttention(nn.Module):
def __init__(self, **kwargs):
del kwargs
super().__init__()
def _small_od_config():
arch = {
"num_layers": 1,
"token_refiner_num_layers": 1,
"hidden_size": 8,
"num_attention_heads": 2,
"attention_head_dim": 4,
"ffn_hidden_size": 16,
"latents_dim": 2,
"audio_latents_dim": 2,
"patch_size": (1, 2, 2),
"text_dim": 6,
"timestep_input_dim": 4,
"time_embed_hidden_size": 8,
"time_embed_dim": 4,
"adaln_out_features": 18 * 8,
"final_adaln_out_features": 2 * 8,
"rope_inv_freq_len": 2,
}
return SimpleNamespace(
tf_model_config=arch,
parallel_config=SimpleNamespace(ulysses_degree=1),
)
def test_fp8_scope_and_prefix_propagation(monkeypatch):
from vllm.model_executor.layers.linear import LinearBase, UnquantizedLinearMethod
from vllm.model_executor.layers.quantization.fp8 import Fp8Config
from vllm.model_executor.layers.quantization.utils.quant_utils import (
is_layer_skipped,
)
from vllm_omni.diffusion.models.minimax_h3 import minimax_h3_transformer as h3
monkeypatch.setattr(h3, "ColumnParallelLinear", _FakeLinear)
monkeypatch.setattr(h3, "MergedColumnParallelLinear", _FakeLinear)
monkeypatch.setattr(h3, "QKVParallelLinear", _FakeLinear)
monkeypatch.setattr(h3, "RowParallelLinear", _FakeLinear)
monkeypatch.setattr(h3, "Attention", _FakeAttention)
monkeypatch.setattr(h3, "get_tensor_model_parallel_world_size", lambda: 1)
ignored_layers = {
"token_refiner.blocks.0.mlp.fc2",
"blocks.0.attn.qkv_proj",
"condition_proj",
"blocks.0.adaln_proj.linear",
"final_layer.adaln_proj.linear",
}
fp8_config = Fp8Config(ignored_layers=sorted(ignored_layers))
model = h3.MiniMaxH3DiTModel(
_small_od_config(),
quant_config=fp8_config,
)
linears = {module.prefix: module.quant_config for module in model.modules() if isinstance(module, _FakeLinear)}
quantized = {
"token_refiner.blocks.0.attn.qkv_proj",
"token_refiner.blocks.0.attn.out_proj",
"token_refiner.blocks.0.mlp.fc1",
"token_refiner.blocks.0.mlp.fc2",
"blocks.0.attn.qkv_proj",
"blocks.0.attn.out_proj",
"blocks.0.mlp.fc1",
"blocks.0.mlp.fc2",
"condition_proj",
"blocks.0.adaln_proj.linear",
"final_layer.adaln_proj.linear",
}
full_precision = {
"video_patch_proj",
"audio_patch_proj",
"time_embedder.proj_in",
"time_embedder.proj_out",
"final_layer.video_out",
"final_layer.audio_out",
}
assert quantized <= linears.keys()
assert full_precision <= linears.keys()
assert all(linears[prefix] is fp8_config for prefix in quantized)
assert all(linears[prefix] is None for prefix in full_precision)
assert {prefix for prefix in quantized if is_layer_skipped(prefix, fp8_config.ignored_layers)} == ignored_layers
linear = Mock(spec=LinearBase)
assert all(
isinstance(
fp8_config.get_quant_method(linear, prefix),
UnquantizedLinearMethod,
)
for prefix in ignored_layers
)
class _WeightTarget(nn.Module):
def __init__(self, loader):
super().__init__()
self.weight = nn.Parameter(torch.empty(1))
self.weight.weight_loader = loader
def test_model_load_weights_transforms_before_calling_vllm_loader():
from vllm_omni.diffusion.models.minimax_h3.minimax_h3_transformer import (
MiniMaxH3DiTArchConfig,
MiniMaxH3DiTModel,
)
qkv_calls = []
fc1_calls = []
def qkv_loader(param, loaded_weight):
del param
qkv_calls.append(loaded_weight.clone())
def fc1_loader(param, loaded_weight, shard_id):
del param
fc1_calls.append((shard_id, loaded_weight.clone()))
model = object.__new__(MiniMaxH3DiTModel)
nn.Module.__init__(model)
model.arch = MiniMaxH3DiTArchConfig(
hidden_size=1,
num_attention_heads=2,
attention_head_dim=1,
ffn_hidden_size=2,
)
model.blocks = nn.ModuleList([nn.Module()])
model.blocks[0].attn = nn.Module()
model.blocks[0].attn.qkv_proj = _WeightTarget(qkv_loader)
model.blocks[0].mlp = nn.Module()
model.blocks[0].mlp.fc1 = _WeightTarget(fc1_loader)
qkv = torch.arange(6, dtype=torch.float32).reshape(6, 1)
fc1 = torch.arange(4, dtype=torch.float32).reshape(4, 1)
loaded = model.load_weights(
[
("blocks.0.attn.qkv_proj.weight", qkv),
("blocks.0.mlp.fc1.weight", fc1),
]
)
assert loaded == {
"blocks.0.attn.qkv_proj.weight",
"blocks.0.mlp.fc1.weight",
}
assert qkv_calls[0][:, 0].tolist() == [0, 3, 1, 4, 2, 5]
assert [(shard_id, tensor[:, 0].tolist()) for shard_id, tensor in fc1_calls] == [
(0, [0, 1]),
(1, [2, 3]),
]
def test_pipeline_resolves_transformer_component_quant_config():
from vllm_omni.diffusion.models.minimax_h3.pipeline_minimax_h3 import (
_resolve_component_quant_config,
)
from vllm_omni.quantization import build_quant_config
ignored_layers = ["blocks.0.attn.qkv_proj"]
component_config = build_quant_config(
{
"transformer": {
"method": "fp8",
"ignored_layers": ignored_layers,
}
}
)
transformer_config = component_config.resolve("transformer")
assert transformer_config.ignored_layers == ignored_layers
assert _resolve_component_quant_config(component_config, "transformer") is transformer_config
assert _resolve_component_quant_config(transformer_config, "transformer") is transformer_config
@@ -0,0 +1,297 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
from pathlib import Path
import numpy as np
import pytest
import torch
from tests.diffusion.quantization.test_quantization_quality import (
_OUTPUT_DIR,
QualityTestConfig,
_compute_lpips,
_compute_psnr_and_mae,
_free_gpu_memory,
_maybe_save_output,
)
from tests.helpers.mark import hardware_marks
_MINIMAX_H3_REPO = "MiniMaxAI/MiniMax-H3"
_MINIMAX_H3_REVISION = "48d93ede732756e404a3b1b2f3b3a9b5a22f6cfc"
_MINIMAX_H3_AUDIO_SAMPLE_RATE = 32000
_MIN_AUDIO_SPECTRAL_COSINE = 0.80
_MIN_AUDIO_RMS_RATIO = 0.50
_MAX_AUDIO_RMS_RATIO = 2.00
_QUALITY_CONFIG = QualityTestConfig(
id="fp8_minimax_h3_fl2va",
model=_MINIMAX_H3_REPO,
quantization={"transformer": {"method": "fp8"}},
task="t2v",
prompt=(
"A bright daytime cinematic tracking shot of a golden retriever running through a sunlit meadow "
"filled with colorful wildflowers, vivid blue sky, crisp natural colors."
),
max_lpips=0.20,
height=384,
width=672,
num_frames=107,
num_inference_steps=10,
)
def _resolve_fl2va_model_ref() -> str:
from huggingface_hub import snapshot_download
repo_root = Path(
snapshot_download(
repo_id=_MINIMAX_H3_REPO,
revision=_MINIMAX_H3_REVISION,
allow_patterns=["FL2VA/**"],
)
)
fl2va_root = repo_root / "FL2VA"
if not (fl2va_root / "model_index.json").is_file():
raise FileNotFoundError(f"MiniMax H3 FL2VA model_index.json not found under {repo_root}")
return str(fl2va_root)
def _canonical_audio(audio: object) -> np.ndarray:
"""Return H3 audio as float32 [channels, samples]."""
array = np.asarray(audio, dtype=np.float32)
while array.ndim > 2 and array.shape[0] == 1:
array = array[0]
if array.ndim == 1:
array = array[None, :]
if array.ndim != 2:
raise ValueError(f"Expected mono/stereo audio, got shape {array.shape}")
if array.shape[0] > array.shape[1] and array.shape[1] <= 8:
array = array.T
if not 1 <= array.shape[0] <= 8:
raise ValueError(f"Expected channel-first audio, got shape {array.shape}")
if not np.isfinite(array).all():
raise ValueError("Audio contains non-finite samples")
return np.ascontiguousarray(array)
def _audio_quality_metrics(baseline: object, quantized: object) -> tuple[float, float]:
"""Compare phase-tolerant spectral structure and overall signal energy."""
baseline_array = _canonical_audio(baseline)
quantized_array = _canonical_audio(quantized)
if baseline_array.shape != quantized_array.shape:
raise ValueError(
"Audio shapes do not match for metric computation: "
f"baseline={baseline_array.shape}, quantized={quantized_array.shape}"
)
baseline_tensor = torch.from_numpy(baseline_array)
quantized_tensor = torch.from_numpy(quantized_array)
similarities: list[float] = []
for n_fft in (512, 1024, 2048):
window = torch.hann_window(n_fft)
baseline_magnitude = torch.stft(
baseline_tensor,
n_fft=n_fft,
hop_length=n_fft // 4,
window=window,
return_complex=True,
).abs()
quantized_magnitude = torch.stft(
quantized_tensor,
n_fft=n_fft,
hop_length=n_fft // 4,
window=window,
return_complex=True,
).abs()
cosine = torch.nn.functional.cosine_similarity(
baseline_magnitude.flatten(1),
quantized_magnitude.flatten(1),
dim=1,
eps=1e-12,
)
similarities.extend(float(value) for value in cosine)
baseline_rms = float(torch.sqrt(torch.mean(baseline_tensor.square())))
quantized_rms = float(torch.sqrt(torch.mean(quantized_tensor.square())))
rms_ratio = quantized_rms / max(baseline_rms, 1e-12)
return float(np.mean(similarities)), rms_ratio
def _generate_joint_output(omni, config: QualityTestConfig):
"""Generate H3 video/audio, returning both payloads and peak GPU memory."""
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
from vllm_omni.platforms import current_omni_platform
generator = torch.Generator(device=current_omni_platform.device_type).manual_seed(config.seed)
torch.accelerator.reset_peak_memory_stats()
outputs = omni.generate(
{"prompt": config.prompt, "negative_prompt": config.negative_prompt},
OmniDiffusionSamplingParams(
height=config.height,
width=config.width,
generator=generator,
num_inference_steps=config.num_inference_steps,
num_frames=config.num_frames,
guidance_scale=config.guidance_scale,
sigmas=config.sigmas,
),
)
main_process_peak_mem = torch.accelerator.max_memory_allocated() / (1024**3)
first = outputs[0]
worker_peak_memory_mb = float(getattr(first, "peak_memory_mb", 0.0) or 0.0)
if hasattr(first, "request_output") and isinstance(first.request_output, list):
first = first.request_output[0]
worker_peak_memory_mb = max(
worker_peak_memory_mb,
float(getattr(first, "peak_memory_mb", 0.0) or 0.0),
)
if not hasattr(first, "images") or not first.images:
raise ValueError("Could not extract video frames from H3 output")
frames = first.images[0]
if isinstance(frames, torch.Tensor):
frames = frames.detach().cpu()
if frames.dim() == 5:
frames = frames[0]
if frames.dim() == 4 and frames.shape[0] in (3, 4):
frames = frames.permute(1, 2, 3, 0)
if frames.is_floating_point():
frames = frames.clamp(-1, 1) * 0.5 + 0.5
video = frames.float().numpy()
else:
video = np.asarray(frames)
if video.ndim == 5:
video = video[0]
multimodal = getattr(first, "multimodal_output", None)
if not multimodal or "audio" not in multimodal:
raise ValueError("Could not extract audio from H3 output")
sample_rate = int(multimodal.get("audio_sample_rate", 0))
peak_mem = worker_peak_memory_mb / 1024 if worker_peak_memory_mb > 0 else main_process_peak_mem
return video, _canonical_audio(multimodal["audio"]), sample_rate, peak_mem
@pytest.mark.full_model
@pytest.mark.diffusion
@pytest.mark.parametrize(
"config",
[
pytest.param(
_QUALITY_CONFIG,
id=_QUALITY_CONFIG.id,
marks=hardware_marks(res={"cuda": _QUALITY_CONFIG.gpu}, num_cards=2),
)
],
)
def test_minimax_h3_quantization_quality(config: QualityTestConfig):
from vllm_omni.entrypoints.omni import Omni
model_ref = _resolve_fl2va_model_ref()
common_kwargs = {
"model": model_ref,
"enforce_eager": True,
"tensor_parallel_size": 2,
"text_encoder_tp_size": 2,
"vae_use_tiling": True,
}
omni_bl = Omni(**common_kwargs)
baseline_out, baseline_audio, baseline_sample_rate, bl_mem = _generate_joint_output(omni_bl, config)
omni_bl.shutdown()
del omni_bl
_free_gpu_memory()
_maybe_save_output(_OUTPUT_DIR, config, "baseline", baseline_out)
quantization = config.quantization_ref()
omni_qt = Omni(**common_kwargs, quantization_config=quantization)
quant_out, quant_audio, quant_sample_rate, qt_mem = _generate_joint_output(omni_qt, config)
omni_qt.shutdown()
del omni_qt
_free_gpu_memory()
_maybe_save_output(_OUTPUT_DIR, config, "quantized", quant_out)
lpips_score = _compute_lpips(baseline_out, quant_out, config.task)
psnr_score, mae_score = _compute_psnr_and_mae(baseline_out, quant_out, config.task)
if baseline_sample_rate != _MINIMAX_H3_AUDIO_SAMPLE_RATE or quant_sample_rate != baseline_sample_rate:
raise AssertionError(
f"Unexpected H3 audio sample rate: baseline={baseline_sample_rate}, quantized={quant_sample_rate}"
)
audio_spectral_cosine, audio_rms_ratio = _audio_quality_metrics(baseline_audio, quant_audio)
assert lpips_score <= config.max_lpips, (
f"LPIPS {lpips_score:.4f} exceeds threshold {config.max_lpips} "
f"for {config.quantization_ref()} on {config.quantized_ref()}"
)
assert audio_spectral_cosine >= _MIN_AUDIO_SPECTRAL_COSINE, (
f"Audio spectral cosine {audio_spectral_cosine:.4f} is below threshold "
f"{_MIN_AUDIO_SPECTRAL_COSINE} for {config.id}"
)
assert _MIN_AUDIO_RMS_RATIO <= audio_rms_ratio <= _MAX_AUDIO_RMS_RATIO, (
f"Audio RMS ratio {audio_rms_ratio:.4f} is outside "
f"[{_MIN_AUDIO_RMS_RATIO}, {_MAX_AUDIO_RMS_RATIO}] for {config.id}"
)
mem_reduction = (bl_mem - qt_mem) / bl_mem * 100 if bl_mem > 0 else 0
print(f"\n{'=' * 60}")
print(f"Quantization Quality: {config.id}")
print(f"{'=' * 60}")
print(f" Baseline: {config.baseline_ref()}")
print(f" Quantized: {config.quantized_ref()}")
print(f" Method: {config.quantization_ref()}")
print(f" LPIPS: {lpips_score:.4f} (threshold: {config.max_lpips})")
print(f" PSNR: {psnr_score:.4f} dB (higher is better)")
print(f" MAE: {mae_score:.6f} (lower is better)")
print(f" Audio cosine: {audio_spectral_cosine:.4f} (threshold: {_MIN_AUDIO_SPECTRAL_COSINE})")
print(f" Audio RMS: {audio_rms_ratio:.4f}x (range: {_MIN_AUDIO_RMS_RATIO}-{_MAX_AUDIO_RMS_RATIO})")
print(f" BF16 memory: {bl_mem:.2f} GiB")
print(f" Quant memory: {qt_mem:.2f} GiB ({mem_reduction:.0f}% reduction)")
print(" Result: PASS")
print(f"{'=' * 60}\n")
assert np.isfinite(psnr_score) or np.isinf(psnr_score), f"PSNR is invalid for {config.id}: {psnr_score}"
assert np.isfinite(mae_score), f"MAE is not finite for {config.id}: {mae_score}"
def test_resolve_fl2va_model_ref(tmp_path, monkeypatch):
fl2va_root = tmp_path / "FL2VA"
fl2va_root.mkdir()
(fl2va_root / "model_index.json").write_text("{}", encoding="utf-8")
def fake_snapshot_download(*, repo_id, revision, allow_patterns):
assert repo_id == _MINIMAX_H3_REPO
assert revision == _MINIMAX_H3_REVISION
assert allow_patterns == ["FL2VA/**"]
return str(tmp_path)
monkeypatch.setattr("huggingface_hub.snapshot_download", fake_snapshot_download)
assert _resolve_fl2va_model_ref() == str(fl2va_root)
def test_audio_quality_metrics_ignore_phase_but_reject_spectral_drift():
sample_rate = _MINIMAX_H3_AUDIO_SAMPLE_RATE
time = np.arange(sample_rate, dtype=np.float32) / sample_rate
baseline = np.stack(
[
np.sin(2 * np.pi * 440 * time),
np.sin(2 * np.pi * 880 * time),
]
)
phase_shifted = np.stack(
[
np.sin(2 * np.pi * 440 * time + np.pi / 2),
np.sin(2 * np.pi * 880 * time + np.pi / 2),
]
)
different = np.stack(
[
np.sin(2 * np.pi * 3000 * time),
np.sin(2 * np.pi * 4000 * time),
]
)
phase_cosine, phase_rms_ratio = _audio_quality_metrics(baseline, phase_shifted)
different_cosine, _ = _audio_quality_metrics(baseline, different)
assert phase_cosine >= _MIN_AUDIO_SPECTRAL_COSINE
assert phase_rms_ratio == pytest.approx(1.0, abs=1e-5)
assert different_cosine < _MIN_AUDIO_SPECTRAL_COSINE