[misc] MiniMax-H3 fusions: exact eager fallback, enable/inert logging, engagement test

- _can_run_minimax_h3_fusion now also requires Triton availability, so an
  enabled fusion on a CUDA build without a working Triton falls back to
  eager instead of hitting the strict wrappers' RuntimeError mid-forward.
- One-time logger.info of the resolved fusion set at model init (and a
  warning when the set is requested without Triton), plus a
  prepare_for_compile hook warning that torch.compile capture makes the
  fusions inert inside compiled block forwards.
- Positive-engagement routing test: counts 1/1/2/1 fused-kernel calls in
  one CUDA inference block forward and asserts a grad-enabled forward
  leaves the counters unchanged, so a guard regression to always-eager
  can no longer pass silently.
- Document the index-bounds contract ([0, table_rows)) on the modulation
  wrappers; a device-side check would synchronize, and in-model callers
  are safe by construction.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Will Lin
2026-08-21 10:06:23 +00:00
co-authored by Claude Fable 5
parent ac98869aa1
commit cbab605eff
4 changed files with 144 additions and 5 deletions
+35 -2
View File
@@ -24,8 +24,10 @@ from fastvideo.layers.linear import ReplicatedLinear
from fastvideo.layers.mlp import MLP
from fastvideo.layers.quantization import QuantizationConfig
from fastvideo.layers.visual_embedding import Timesteps
from fastvideo.logger import init_logger
from fastvideo.models.dits.base import BaseDiT
from fastvideo.models.dits.minimax_h3_fusions import (
HAVE_TRITON,
fused_qknorm_rope,
fused_residual_gate_rmsnorm_modulate,
fused_rmsnorm_modulate,
@@ -34,6 +36,8 @@ from fastvideo.models.dits.minimax_h3_fusions import (
from fastvideo.platforms import AttentionBackendEnum
from fastvideo.utils import get_compute_dtype
logger = init_logger(__name__)
MINIMAX_H3_MODALITY_NUM = 3
_CFG = MiniMaxH3Config()
_MINIMAX_H3_FUSION_NAMES = frozenset({"modulate", "qknorm_rope", "swiglu"})
@@ -56,8 +60,13 @@ def _enabled_minimax_h3_fusions(value: str | None = None) -> frozenset[str]:
def _can_run_minimax_h3_fusion(tensor: torch.Tensor) -> bool:
"""Triton kernels are inference-only and stay outside Dynamo capture."""
return tensor.is_cuda and not torch.is_grad_enabled() and not torch.compiler.is_compiling()
"""Triton kernels are inference-only and stay outside Dynamo capture.
The ``HAVE_TRITON`` check makes the eager fallback exact: on a CUDA build
whose Triton failed to import, an enabled fusion falls back instead of
hitting the strict wrappers' hard RuntimeError mid-forward.
"""
return (HAVE_TRITON and tensor.is_cuda and not torch.is_grad_enabled() and not torch.compiler.is_compiling())
class MiniMaxH3RotaryPosEmbed(nn.Module):
@@ -563,6 +572,16 @@ class MiniMaxH3Transformer3DModel(BaseDiT):
super().__init__(config, hf_config)
arch = config.arch_config
self.enabled_fusions = _enabled_minimax_h3_fusions()
if self.enabled_fusions:
if HAVE_TRITON:
logger.info(
"MiniMax H3 inference fusions enabled: %s (CUDA inference-only; grad-enabled and "
"torch.compile-captured forwards fall back to eager).",
",".join(sorted(self.enabled_fusions)))
else:
logger.warning(
"FASTVIDEO_MINIMAX_H3_FUSIONS requested %s but Triton is unavailable; "
"every forward stays on the eager path.", ",".join(sorted(self.enabled_fusions)))
sp_world_size = get_sp_world_size() if model_parallel_is_initialized() else 1
if arch.num_attention_heads % sp_world_size:
raise ValueError(f"MiniMax H3 attention heads ({arch.num_attention_heads}) must be divisible by "
@@ -689,6 +708,20 @@ class MiniMaxH3Transformer3DModel(BaseDiT):
)
self.__post_init__()
def prepare_for_compile(self) -> None:
"""Pipeline hook, called once right before torch.compile wraps the blocks.
Dynamo capture traces the eager branch of every fusion guard, so an
enabled ``FASTVIDEO_MINIMAX_H3_FUSIONS`` set is silently inert inside
compiled block forwards (H3 compiles per-block by default). Say so
once instead of leaving the flag looking active.
"""
if self.enabled_fusions:
logger.warning(
"torch.compile is enabled for MiniMax H3, so the requested inference fusions (%s) are "
"inert inside compiled block forwards; the compiled eager path runs instead.",
",".join(sorted(self.enabled_fusions)))
def materialize_non_persistent_buffers(
self,
device: torch.device,
@@ -8,10 +8,11 @@ from .modulation import (
fused_residual_gate_rmsnorm_modulate,
fused_rmsnorm_modulate,
)
from .qknorm_rope import fused_qknorm_rope
from .qknorm_rope import HAVE_TRITON, fused_qknorm_rope
from .swiglu import minimax_h3_swiglu
__all__ = [
"HAVE_TRITON",
"fused_qknorm_rope",
"fused_residual_gate_rmsnorm_modulate",
"fused_rmsnorm_modulate",
@@ -210,7 +210,13 @@ def fused_rmsnorm_modulate(
index: torch.Tensor,
eps: float,
) -> torch.Tensor:
"""Run RMSNorm and row-indexed modulation in one strict Triton kernel."""
"""Run RMSNorm and row-indexed modulation in one strict Triton kernel.
``index`` values must lie in ``[0, table_rows)``. Unlike eager
``index_select``, the kernel does not raise on out-of-range values (a
device-side bounds check would synchronize); callers are safe by
construction (``timestep_indices * 3 + token_tags``, SP pads with 0).
"""
_validate_contract(x, weight, (scale, shift), index, eps)
_require_forward_only(x, weight, scale, shift)
_require_triton_cuda(x)
@@ -252,7 +258,11 @@ def fused_residual_gate_rmsnorm_modulate(
index: torch.Tensor,
eps: float,
) -> tuple[torch.Tensor, torch.Tensor]:
"""Fuse residual update, row-indexed gate, RMSNorm, and modulation."""
"""Fuse residual update, row-indexed gate, RMSNorm, and modulation.
``index`` values must lie in ``[0, table_rows)``; see
:func:`fused_rmsnorm_modulate` for why the wrapper does not check them.
"""
_validate_residual_branch(residual, branch)
_validate_contract(residual, weight, (gate, scale, shift), index, eps)
_require_forward_only(residual, branch, gate, weight, scale, shift)
@@ -134,3 +134,98 @@ def test_all_minimax_h3_fusions_match_one_eager_block_under_fa4(
torch.testing.assert_close(fused_output, eager_output, atol=3e-2, rtol=3e-2)
finally:
cleanup_dist_env_and_memory()
def test_minimax_h3_fusions_engage_on_cuda_inference(monkeypatch: pytest.MonkeyPatch) -> None:
"""Pin the positive side of the routing guard.
The parity test above still passes if ``_can_run_minimax_h3_fusion``
silently degrades to always-False (both blocks then run the identical
eager path), so count the fused-kernel calls: one CUDA inference forward
through a fully fused block must hit ``fused_rmsnorm_modulate`` once,
``fused_residual_gate_rmsnorm_modulate`` once, ``fused_qknorm_rope``
twice (q and k), and ``minimax_h3_swiglu`` once -- and a grad-enabled
forward must leave every counter unchanged.
"""
if not torch.cuda.is_available() or not torch.cuda.is_bf16_supported():
pytest.skip("BF16 CUDA is required")
pytest.importorskip("triton")
monkeypatch.setenv("FASTVIDEO_ATTENTION_BACKEND", "TORCH_SDPA")
monkeypatch.setenv("MASTER_ADDR", "127.0.0.1")
monkeypatch.setenv("MASTER_PORT", "29574")
monkeypatch.setenv("RANK", "0")
monkeypatch.setenv("WORLD_SIZE", "1")
monkeypatch.setenv("LOCAL_RANK", "0")
import fastvideo.models.dits.minimax_h3 as h3
from fastvideo.distributed import cleanup_dist_env_and_memory, maybe_init_distributed_environment_and_model_parallel
from fastvideo.forward_context import set_forward_context
calls = dict.fromkeys(("rmsnorm_modulate", "residual_gate_rmsnorm_modulate", "qknorm_rope", "swiglu"), 0)
def _counting(name: str, real):
def wrapper(*args, **kwargs):
calls[name] += 1
return real(*args, **kwargs)
return wrapper
monkeypatch.setattr(h3, "fused_rmsnorm_modulate", _counting("rmsnorm_modulate", h3.fused_rmsnorm_modulate))
monkeypatch.setattr(h3, "fused_residual_gate_rmsnorm_modulate",
_counting("residual_gate_rmsnorm_modulate", h3.fused_residual_gate_rmsnorm_modulate))
monkeypatch.setattr(h3, "fused_qknorm_rope", _counting("qknorm_rope", h3.fused_qknorm_rope))
monkeypatch.setattr(h3, "minimax_h3_swiglu", _counting("swiglu", h3.minimax_h3_swiglu))
maybe_init_distributed_environment_and_model_parallel(1, 1)
try:
block = h3.MiniMaxH3TransformerBlock(
hidden_size=128,
num_attention_heads=1,
attention_head_dim=128,
ffn_dim=256,
time_embed_dim=64,
norm_eps=1e-5,
qk_norm_eps=1e-5,
supported_attention_backends=(AttentionBackendEnum.TORCH_SDPA, ),
quant_config=None,
prefix="minimax_h3.engagement_block",
fuse_modulate=True,
fuse_qknorm_rope=True,
fuse_swiglu=True,
)
device = torch.device("cuda")
block = block.to(device=device, dtype=torch.bfloat16).eval()
generator = torch.Generator(device=device).manual_seed(2026)
hidden_states = torch.randn(2, 12, 128, generator=generator, device=device, dtype=torch.bfloat16)
temb = torch.randn(2, 64, generator=generator, device=device, dtype=torch.bfloat16)
adaln_indices = torch.arange(12, device=device, dtype=torch.long).remainder(6)
position_ids = torch.zeros(12, 3, device=device, dtype=torch.float32)
position_ids[:, 0] = torch.arange(12, device=device)
rotary_emb = h3.MiniMaxH3RotaryPosEmbed(rope_freq_dim=16, rope_theta=10000.0).to(device)(position_ids)
inputs = dict(
hidden_states=hidden_states,
temb=temb,
adaln_indices=adaln_indices,
rotary_emb=tuple(value.to(torch.bfloat16) for value in rotary_emb),
original_seq_len=12,
)
with torch.inference_mode(), set_forward_context(current_timestep=0, attn_metadata=None):
block(**inputs)
engaged = dict(calls)
assert engaged == {
"rmsnorm_modulate": 1,
"residual_gate_rmsnorm_modulate": 1,
"qknorm_rope": 2,
"swiglu": 1,
}, engaged
grad_inputs = {**inputs, "hidden_states": hidden_states.clone().requires_grad_(True)}
with set_forward_context(current_timestep=0, attn_metadata=None):
block(**grad_inputs)
assert dict(calls) == engaged, f"a fusion ran under grad: {calls} vs {engaged}"
finally:
cleanup_dist_env_and_memory()