[perf] integrate compile-safe H3 fusions for matched benchmarking
This commit is contained in:
@@ -61,13 +61,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.
|
||||
"""Triton kernels are inference-only and trace as opaque custom ops.
|
||||
|
||||
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())
|
||||
return HAVE_TRITON and tensor.is_cuda and not torch.is_grad_enabled()
|
||||
|
||||
|
||||
class MiniMaxH3RotaryPosEmbed(nn.Module):
|
||||
@@ -712,16 +712,15 @@ class MiniMaxH3Transformer3DModel(BaseDiT):
|
||||
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.
|
||||
The inference-only Triton fusions expose fake-backed custom operators,
|
||||
so Dynamo can keep them active as opaque nodes inside each fullgraph
|
||||
block instead of tracing into their launcher implementation.
|
||||
"""
|
||||
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)))
|
||||
logger.info(
|
||||
"MiniMax H3 inference fusions remain active under torch.compile through custom-op boundaries: %s",
|
||||
",".join(sorted(self.enabled_fusions)),
|
||||
)
|
||||
|
||||
def materialize_non_persistent_buffers(
|
||||
self,
|
||||
|
||||
@@ -202,7 +202,7 @@ def _row_addressable(table: torch.Tensor) -> torch.Tensor:
|
||||
return table if table.stride(-1) == 1 else table.contiguous()
|
||||
|
||||
|
||||
def fused_rmsnorm_modulate(
|
||||
def _fused_rmsnorm_modulate_impl(
|
||||
x: torch.Tensor,
|
||||
weight: torch.Tensor,
|
||||
scale: torch.Tensor,
|
||||
@@ -210,13 +210,6 @@ def fused_rmsnorm_modulate(
|
||||
index: torch.Tensor,
|
||||
eps: float,
|
||||
) -> torch.Tensor:
|
||||
"""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)
|
||||
@@ -248,7 +241,7 @@ def fused_rmsnorm_modulate(
|
||||
return output.view_as(x)
|
||||
|
||||
|
||||
def fused_residual_gate_rmsnorm_modulate(
|
||||
def _fused_residual_gate_rmsnorm_modulate_impl(
|
||||
residual: torch.Tensor,
|
||||
branch: torch.Tensor,
|
||||
gate: torch.Tensor,
|
||||
@@ -258,11 +251,6 @@ 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.
|
||||
|
||||
``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)
|
||||
@@ -300,3 +288,136 @@ def fused_residual_gate_rmsnorm_modulate(
|
||||
num_warps=_num_warps(block_size),
|
||||
)
|
||||
return hidden.view_as(residual), modulated.view_as(residual)
|
||||
|
||||
|
||||
# Dynamo must see the Triton launches as opaque nodes. In eager mode the
|
||||
# public wrappers below keep their strict, actionable input validation; while
|
||||
# compiling, these custom-op boundaries avoid tracing into Triton's launcher
|
||||
# and let the surrounding H3 block remain one full graph.
|
||||
@torch.library.custom_op(
|
||||
"fastvideo::_minimax_h3_rmsnorm_modulate",
|
||||
mutates_args=(),
|
||||
device_types="cuda",
|
||||
)
|
||||
def _fused_rmsnorm_modulate_op(
|
||||
x: torch.Tensor,
|
||||
weight: torch.Tensor,
|
||||
scale: torch.Tensor,
|
||||
shift: torch.Tensor,
|
||||
index: torch.Tensor,
|
||||
eps: float,
|
||||
) -> torch.Tensor:
|
||||
return _fused_rmsnorm_modulate_impl(x, weight, scale, shift, index, eps)
|
||||
|
||||
|
||||
@torch.library.register_fake("fastvideo::_minimax_h3_rmsnorm_modulate")
|
||||
def _fused_rmsnorm_modulate_fake(
|
||||
x: torch.Tensor,
|
||||
weight: torch.Tensor,
|
||||
scale: torch.Tensor,
|
||||
shift: torch.Tensor,
|
||||
index: torch.Tensor,
|
||||
eps: float,
|
||||
) -> torch.Tensor:
|
||||
del weight, scale, shift, index, eps
|
||||
return x.new_empty(x.shape)
|
||||
|
||||
|
||||
@torch.library.custom_op(
|
||||
"fastvideo::_minimax_h3_residual_gate_rmsnorm_modulate",
|
||||
mutates_args=(),
|
||||
device_types="cuda",
|
||||
)
|
||||
def _fused_residual_gate_rmsnorm_modulate_op(
|
||||
residual: torch.Tensor,
|
||||
branch: torch.Tensor,
|
||||
gate: torch.Tensor,
|
||||
weight: torch.Tensor,
|
||||
scale: torch.Tensor,
|
||||
shift: torch.Tensor,
|
||||
index: torch.Tensor,
|
||||
eps: float,
|
||||
) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
return _fused_residual_gate_rmsnorm_modulate_impl(
|
||||
residual,
|
||||
branch,
|
||||
gate,
|
||||
weight,
|
||||
scale,
|
||||
shift,
|
||||
index,
|
||||
eps,
|
||||
)
|
||||
|
||||
|
||||
@torch.library.register_fake("fastvideo::_minimax_h3_residual_gate_rmsnorm_modulate")
|
||||
def _fused_residual_gate_rmsnorm_modulate_fake(
|
||||
residual: torch.Tensor,
|
||||
branch: torch.Tensor,
|
||||
gate: torch.Tensor,
|
||||
weight: torch.Tensor,
|
||||
scale: torch.Tensor,
|
||||
shift: torch.Tensor,
|
||||
index: torch.Tensor,
|
||||
eps: float,
|
||||
) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
del branch, gate, weight, scale, shift, index, eps
|
||||
return residual.new_empty(residual.shape), residual.new_empty(residual.shape)
|
||||
|
||||
|
||||
def fused_rmsnorm_modulate(
|
||||
x: torch.Tensor,
|
||||
weight: torch.Tensor,
|
||||
scale: torch.Tensor,
|
||||
shift: torch.Tensor,
|
||||
index: torch.Tensor,
|
||||
eps: float,
|
||||
) -> torch.Tensor:
|
||||
"""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).
|
||||
"""
|
||||
if torch.compiler.is_compiling():
|
||||
return torch.ops.fastvideo._minimax_h3_rmsnorm_modulate(x, weight, scale, shift, index, eps)
|
||||
return _fused_rmsnorm_modulate_impl(x, weight, scale, shift, index, eps)
|
||||
|
||||
|
||||
def fused_residual_gate_rmsnorm_modulate(
|
||||
residual: torch.Tensor,
|
||||
branch: torch.Tensor,
|
||||
gate: torch.Tensor,
|
||||
weight: torch.Tensor,
|
||||
scale: torch.Tensor,
|
||||
shift: torch.Tensor,
|
||||
index: torch.Tensor,
|
||||
eps: float,
|
||||
) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
"""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.
|
||||
"""
|
||||
if torch.compiler.is_compiling():
|
||||
return torch.ops.fastvideo._minimax_h3_residual_gate_rmsnorm_modulate(
|
||||
residual,
|
||||
branch,
|
||||
gate,
|
||||
weight,
|
||||
scale,
|
||||
shift,
|
||||
index,
|
||||
eps,
|
||||
)
|
||||
return _fused_residual_gate_rmsnorm_modulate_impl(
|
||||
residual,
|
||||
branch,
|
||||
gate,
|
||||
weight,
|
||||
scale,
|
||||
shift,
|
||||
index,
|
||||
eps,
|
||||
)
|
||||
|
||||
@@ -121,23 +121,13 @@ def _validate_inputs(
|
||||
return batch, seq_len, num_heads, head_dim, rotary_dim
|
||||
|
||||
|
||||
def fused_qknorm_rope(
|
||||
def _fused_qknorm_rope_impl(
|
||||
x: torch.Tensor,
|
||||
weight: torch.Tensor,
|
||||
cos: torch.Tensor,
|
||||
sin: torch.Tensor,
|
||||
eps: float,
|
||||
) -> torch.Tensor:
|
||||
"""Run per-head RMSNorm and partial RoPE in one Sol-Engine-style kernel.
|
||||
|
||||
RMSNorm reduction and RoPE arithmetic stay in FP32 registers until the
|
||||
final store. Triton's reduction order and the absence of eager's BF16
|
||||
intermediate materializations can produce small, expected rounding drift.
|
||||
|
||||
Row offsets are computed in int64, so inputs beyond 2**31 total elements
|
||||
(about 300k tokens per rank at H3's 56 heads x 128 head_dim) address
|
||||
correctly.
|
||||
"""
|
||||
batch, seq_len, num_heads, head_dim, rotary_dim = _validate_inputs(x, weight, cos, sin, eps)
|
||||
if not weight.is_contiguous():
|
||||
raise ValueError("weight must be contiguous")
|
||||
@@ -171,4 +161,53 @@ def fused_qknorm_rope(
|
||||
return flat_out.view(batch, seq_len, num_heads, head_dim)
|
||||
|
||||
|
||||
@torch.library.custom_op(
|
||||
"fastvideo::_minimax_h3_qknorm_rope",
|
||||
mutates_args=(),
|
||||
device_types="cuda",
|
||||
)
|
||||
def _fused_qknorm_rope_op(
|
||||
x: torch.Tensor,
|
||||
weight: torch.Tensor,
|
||||
cos: torch.Tensor,
|
||||
sin: torch.Tensor,
|
||||
eps: float,
|
||||
) -> torch.Tensor:
|
||||
return _fused_qknorm_rope_impl(x, weight, cos, sin, eps)
|
||||
|
||||
|
||||
@torch.library.register_fake("fastvideo::_minimax_h3_qknorm_rope")
|
||||
def _fused_qknorm_rope_fake(
|
||||
x: torch.Tensor,
|
||||
weight: torch.Tensor,
|
||||
cos: torch.Tensor,
|
||||
sin: torch.Tensor,
|
||||
eps: float,
|
||||
) -> torch.Tensor:
|
||||
del weight, cos, sin, eps
|
||||
return x.new_empty(x.shape)
|
||||
|
||||
|
||||
def fused_qknorm_rope(
|
||||
x: torch.Tensor,
|
||||
weight: torch.Tensor,
|
||||
cos: torch.Tensor,
|
||||
sin: torch.Tensor,
|
||||
eps: float,
|
||||
) -> torch.Tensor:
|
||||
"""Run per-head RMSNorm and partial RoPE in one Sol-Engine-style kernel.
|
||||
|
||||
RMSNorm reduction and RoPE arithmetic stay in FP32 registers until the
|
||||
final store. Triton's reduction order and the absence of eager's BF16
|
||||
intermediate materializations can produce small, expected rounding drift.
|
||||
|
||||
Row offsets are computed in int64, so inputs beyond 2**31 total elements
|
||||
(about 300k tokens per rank at H3's 56 heads x 128 head_dim) address
|
||||
correctly.
|
||||
"""
|
||||
if torch.compiler.is_compiling():
|
||||
return torch.ops.fastvideo._minimax_h3_qknorm_rope(x, weight, cos, sin, eps)
|
||||
return _fused_qknorm_rope_impl(x, weight, cos, sin, eps)
|
||||
|
||||
|
||||
__all__ = ["HAVE_TRITON", "fused_qknorm_rope"]
|
||||
|
||||
@@ -65,12 +65,7 @@ def _num_warps(block_size: int) -> int:
|
||||
return 4
|
||||
|
||||
|
||||
def minimax_h3_swiglu(x: torch.Tensor) -> torch.Tensor:
|
||||
"""Run the forward-only Triton fusion over an H3 ``(..., 2 * ffn_dim)`` input.
|
||||
|
||||
This is intentionally a strict kernel wrapper: callers own fallback policy and
|
||||
must only invoke it for a supported CUDA inference path.
|
||||
"""
|
||||
def _minimax_h3_swiglu_impl(x: torch.Tensor) -> torch.Tensor:
|
||||
ffn_dim = _validate_input(x)
|
||||
if not x.is_cuda:
|
||||
raise ValueError("MiniMax H3 fused SwiGLU requires a CUDA tensor")
|
||||
@@ -101,4 +96,29 @@ def minimax_h3_swiglu(x: torch.Tensor) -> torch.Tensor:
|
||||
return out.view(output_shape)
|
||||
|
||||
|
||||
@torch.library.custom_op(
|
||||
"fastvideo::_minimax_h3_swiglu",
|
||||
mutates_args=(),
|
||||
device_types="cuda",
|
||||
)
|
||||
def _minimax_h3_swiglu_op(x: torch.Tensor) -> torch.Tensor:
|
||||
return _minimax_h3_swiglu_impl(x)
|
||||
|
||||
|
||||
@torch.library.register_fake("fastvideo::_minimax_h3_swiglu")
|
||||
def _minimax_h3_swiglu_fake(x: torch.Tensor) -> torch.Tensor:
|
||||
return x.new_empty((*x.shape[:-1], x.shape[-1] // 2))
|
||||
|
||||
|
||||
def minimax_h3_swiglu(x: torch.Tensor) -> torch.Tensor:
|
||||
"""Run the forward-only Triton fusion over an H3 ``(..., 2 * ffn_dim)`` input.
|
||||
|
||||
This is intentionally a strict kernel wrapper: callers own fallback policy and
|
||||
must only invoke it for a supported CUDA inference path.
|
||||
"""
|
||||
if torch.compiler.is_compiling():
|
||||
return torch.ops.fastvideo._minimax_h3_swiglu(x)
|
||||
return _minimax_h3_swiglu_impl(x)
|
||||
|
||||
|
||||
__all__ = ["HAVE_TRITON", "minimax_h3_swiglu"]
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Fullgraph trace contract for the MiniMax-H3 inference fusions."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import torch
|
||||
from torch._subclasses.fake_tensor import FakeTensorMode
|
||||
|
||||
from fastvideo.models.dits.minimax_h3_fusions import (
|
||||
fused_qknorm_rope,
|
||||
fused_residual_gate_rmsnorm_modulate,
|
||||
fused_rmsnorm_modulate,
|
||||
minimax_h3_swiglu,
|
||||
)
|
||||
|
||||
|
||||
def _all_fusions(
|
||||
x: torch.Tensor,
|
||||
branch: torch.Tensor,
|
||||
weight: torch.Tensor,
|
||||
scale: torch.Tensor,
|
||||
shift: torch.Tensor,
|
||||
gate: torch.Tensor,
|
||||
index: torch.Tensor,
|
||||
q: torch.Tensor,
|
||||
q_weight: torch.Tensor,
|
||||
cos: torch.Tensor,
|
||||
sin: torch.Tensor,
|
||||
packed_mlp: torch.Tensor,
|
||||
) -> tuple[torch.Tensor, ...]:
|
||||
norm1 = fused_rmsnorm_modulate(x, weight, scale, shift, index, 1e-5)
|
||||
hidden, norm2 = fused_residual_gate_rmsnorm_modulate(
|
||||
x,
|
||||
branch,
|
||||
gate,
|
||||
weight,
|
||||
scale,
|
||||
shift,
|
||||
index,
|
||||
1e-5,
|
||||
)
|
||||
q_out = fused_qknorm_rope(q, q_weight, cos, sin, 1e-5)
|
||||
mlp_out = minimax_h3_swiglu(packed_mlp)
|
||||
return norm1, hidden, norm2, q_out, mlp_out
|
||||
|
||||
|
||||
def test_minimax_h3_fusions_are_dynamic_fullgraph_traceable() -> None:
|
||||
"""Dynamo sees four opaque ops without requiring a physical CUDA GPU."""
|
||||
graphs: list[torch.fx.GraphModule] = []
|
||||
|
||||
def capture_graph(graph: torch.fx.GraphModule, example_inputs: list[torch.Tensor]):
|
||||
del example_inputs
|
||||
graphs.append(graph)
|
||||
return graph.forward
|
||||
|
||||
compiled = torch.compile(_all_fusions, backend=capture_graph, fullgraph=True, dynamic=True)
|
||||
|
||||
with FakeTensorMode():
|
||||
x = torch.empty((2, 3, 8), device="cuda", dtype=torch.bfloat16)
|
||||
weight = torch.empty((8,), device="cuda", dtype=torch.bfloat16)
|
||||
table = torch.empty((6, 8), device="cuda", dtype=torch.bfloat16)
|
||||
index = torch.empty((3,), device="cuda", dtype=torch.int64)
|
||||
q = torch.empty((2, 3, 4, 8), device="cuda", dtype=torch.bfloat16)
|
||||
rotary = torch.empty((3, 8), device="cuda", dtype=torch.bfloat16)
|
||||
packed_mlp = torch.empty((2, 3, 16), device="cuda", dtype=torch.bfloat16)
|
||||
|
||||
outputs = compiled(
|
||||
x,
|
||||
x,
|
||||
weight,
|
||||
table,
|
||||
table,
|
||||
table,
|
||||
index,
|
||||
q,
|
||||
weight,
|
||||
rotary,
|
||||
rotary,
|
||||
packed_mlp,
|
||||
)
|
||||
|
||||
assert tuple(output.shape for output in outputs) == (
|
||||
(2, 3, 8),
|
||||
(2, 3, 8),
|
||||
(2, 3, 8),
|
||||
(2, 3, 4, 8),
|
||||
(2, 3, 8),
|
||||
)
|
||||
assert all(output.is_contiguous() for output in outputs)
|
||||
assert len(graphs) == 1
|
||||
call_targets = {node.target for node in graphs[0].graph.nodes if node.op == "call_function"}
|
||||
assert {
|
||||
torch.ops.fastvideo._minimax_h3_rmsnorm_modulate,
|
||||
torch.ops.fastvideo._minimax_h3_residual_gate_rmsnorm_modulate,
|
||||
torch.ops.fastvideo._minimax_h3_qknorm_rope,
|
||||
torch.ops.fastvideo._minimax_h3_swiglu,
|
||||
} <= call_targets
|
||||
Reference in New Issue
Block a user