Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bfc4577034 | ||
|
|
5e1e29196a | ||
|
|
cb6d12b55a |
@@ -208,7 +208,7 @@ longer exists also fails the test, so the fixing pull request deletes its entry.
|
||||
| `FASTVIDEO_VAE_PARALLEL_DECODE_STRATEGY` | str | unset | performance | Collective that moves chunks in parallel VAE decode: gather (used when unset) or all_gather. |
|
||||
| `FASTVIDEO_H3_ENCODER_SPLIT` | bool | `0` | distributed | MiniMax-H3 runs the Qwen3-VL text encoder on a dedicated group of nodes, so the denoising ranks never load it. Same as FastVideoArgs.h3_encoder_split=True. |
|
||||
| `FASTVIDEO_H3_ENCODER_NODES` | int | `1` | distributed | Number of leading nodes that FASTVIDEO_H3_ENCODER_SPLIT dedicates to the MiniMax-H3 text encoder. Values below 1 count as 1. |
|
||||
| `FASTVIDEO_MINIMAX_H3_FUSIONS` | str | `""` | performance | MiniMax-H3 inference-only Triton fusions: all, 1, or a comma-separated subset of modulate,qknorm_rope,swiglu. Empty, 0, or none keeps the eager implementation. |
|
||||
| `FASTVIDEO_MINIMAX_H3_FUSIONS` | str | `""` | performance | MiniMax-H3 inference-only Triton fusions: all, 1, or a comma-separated subset of modulate,qknorm_rope,swiglu. The whole-tier value mk selects the MotionKernel kernels instead of the Sol-Engine ones. Empty, 0, or none keeps the eager implementation. |
|
||||
| `FASTVIDEO_MINIMAX_H3_EXACT_KERNELS` | str | `""` | performance | MiniMax-H3 inference-only Triton kernels that reproduce the eager output bit for bit: all, 1, or a comma-separated subset of rope,modulate,swiglu. An op whose FASTVIDEO_MINIMAX_H3_FUSIONS fusion is on keeps the fusion. Empty, 0, or none keeps the eager implementation. |
|
||||
| `FASTVIDEO_FSDP2_AUTOWRAP` | bool | `0` | performance | FSDP2 shards modules by parameter count instead of the model's shard conditions. Not supported by self-forcing distillation. |
|
||||
| `FASTVIDEO_FSDP2_MIN_PARAMS` | int | `10000000` | performance | Minimum parameter count of a module that FASTVIDEO_FSDP2_AUTOWRAP shards. |
|
||||
|
||||
@@ -136,16 +136,44 @@ class DistributedAttention(nn.Module):
|
||||
"""
|
||||
# Check input shapes
|
||||
assert q.dim() == 4 and k.dim() == 4 and v.dim() == 4, "Expected 4D tensors"
|
||||
batch_size, _, num_heads, _ = q.shape
|
||||
# Stack QKV
|
||||
qkv = torch.cat([q, k, v], dim=0) # [3*batch, seq_len, num_heads, head_dim]
|
||||
return self.forward_stacked(
|
||||
qkv,
|
||||
original_seq_len=original_seq_len,
|
||||
replicated_q=replicated_q,
|
||||
replicated_k=replicated_k,
|
||||
replicated_v=replicated_v,
|
||||
freqs_cis=freqs_cis,
|
||||
attn_metadata=attn_metadata,
|
||||
)
|
||||
|
||||
def forward_stacked(
|
||||
self,
|
||||
qkv: torch.Tensor,
|
||||
original_seq_len: int | None = None,
|
||||
replicated_q: torch.Tensor | None = None,
|
||||
replicated_k: torch.Tensor | None = None,
|
||||
replicated_v: torch.Tensor | None = None,
|
||||
freqs_cis: tuple[torch.Tensor, torch.Tensor] | None = None,
|
||||
attn_metadata: AttentionMetadata | None = None,
|
||||
) -> tuple[torch.Tensor, torch.Tensor | None]:
|
||||
"""Attention over a pre-stacked ``[3*batch, seq_len, num_heads, head_dim]`` QKV buffer.
|
||||
|
||||
Identical to :meth:`forward` after its ``torch.cat``, so a fused QKV
|
||||
prologue that already wrote the stack (e.g. MiniMax H3's MotionKernel
|
||||
prologue) can skip that copy. Sequence-parallel redistribution,
|
||||
``freqs_cis``, replicated tokens and backend pre/post-processing are
|
||||
handled exactly as in :meth:`forward`.
|
||||
"""
|
||||
stacked, _, num_heads, _ = qkv.shape
|
||||
batch_size = stacked // 3
|
||||
local_rank = get_sp_parallel_rank()
|
||||
world_size = get_sp_world_size()
|
||||
|
||||
forward_context: ForwardContext = get_forward_context()
|
||||
ctx_attn_metadata = attn_metadata if attn_metadata is not None else forward_context.attn_metadata
|
||||
|
||||
# Stack QKV
|
||||
qkv = torch.cat([q, k, v], dim=0) # [3*batch, seq_len, num_heads, head_dim]
|
||||
|
||||
# Redistribute heads across sequence dimension
|
||||
qkv = sequence_model_parallel_all_to_all_4D(qkv, scatter_dim=2, gather_dim=1)
|
||||
|
||||
|
||||
+2
-1
@@ -427,7 +427,8 @@ FASTVIDEO_MINIMAX_H3_FUSIONS = EnvStr(
|
||||
"",
|
||||
category="performance",
|
||||
doc="MiniMax-H3 inference-only Triton fusions: all, 1, or a comma-separated subset of "
|
||||
"modulate,qknorm_rope,swiglu. Empty, 0, or none keeps the eager implementation.")
|
||||
"modulate,qknorm_rope,swiglu. The whole-tier value mk selects the MotionKernel kernels instead of "
|
||||
"the Sol-Engine ones. Empty, 0, or none keeps the eager implementation.")
|
||||
FASTVIDEO_MINIMAX_H3_EXACT_KERNELS = EnvStr(
|
||||
"",
|
||||
category="performance",
|
||||
|
||||
@@ -38,6 +38,7 @@ from fastvideo.models.dits.base import BaseDiT
|
||||
from fastvideo.models.dits.minimax_h3_vsa_fp4 import (STAGES, vsa_fp4_attention, vsa_fp4_attention_sp,
|
||||
vsa_fp4_requested, vsa_tile_first_attention)
|
||||
from fastvideo.models.dits.minimax_h3_fusions import exact as exact_kernels
|
||||
from fastvideo.models.dits.minimax_h3_fusions import mk as mk_kernels
|
||||
from fastvideo.models.dits.minimax_h3_fusions import (
|
||||
HAVE_TRITON,
|
||||
fused_qknorm_rope,
|
||||
@@ -52,9 +53,13 @@ from fastvideo.utils import get_compute_dtype
|
||||
logger = init_logger(__name__)
|
||||
|
||||
MINIMAX_H3_MODALITY_NUM = 3
|
||||
#: eps baked into the MotionKernel H3 kernels; their call sites require it.
|
||||
_MK_NORM_EPS = 1e-5
|
||||
_CFG = MiniMaxH3Config()
|
||||
_MINIMAX_H3_FUSION_NAMES = frozenset({"modulate", "qknorm_rope", "swiglu"})
|
||||
_MINIMAX_H3_EXACT_KERNEL_NAMES = frozenset({"rope", "modulate", "swiglu"})
|
||||
#: Whole-tier opt-in that replaces the Sol kernels with the MotionKernel ones.
|
||||
_MINIMAX_H3_MK_MODE = "mk"
|
||||
|
||||
|
||||
def _enabled_minimax_h3_fusions(value: str | None = None) -> frozenset[str]:
|
||||
@@ -65,7 +70,14 @@ def _enabled_minimax_h3_fusions(value: str | None = None) -> frozenset[str]:
|
||||
return frozenset()
|
||||
if normalized in {"1", "all"}:
|
||||
return _MINIMAX_H3_FUSION_NAMES
|
||||
if normalized == _MINIMAX_H3_MK_MODE:
|
||||
# ``mk`` is a whole-tier mode: every fusion is on, and each fused call
|
||||
# site dispatches to the MotionKernel kernel instead of the Sol one.
|
||||
return _MINIMAX_H3_FUSION_NAMES | {_MINIMAX_H3_MK_MODE}
|
||||
enabled = frozenset(item.strip() for item in normalized.split(",") if item.strip())
|
||||
if _MINIMAX_H3_MK_MODE in enabled:
|
||||
raise ValueError(f"'{_MINIMAX_H3_MK_MODE}' is a whole-tier mode and cannot be combined with individual "
|
||||
f"fusions: {sorted(enabled - {_MINIMAX_H3_MK_MODE})}")
|
||||
unknown = enabled - _MINIMAX_H3_FUSION_NAMES
|
||||
if unknown:
|
||||
supported = ",".join(sorted(_MINIMAX_H3_FUSION_NAMES))
|
||||
@@ -139,6 +151,7 @@ class MiniMaxH3FeedForward(nn.Module):
|
||||
prefix: str = "",
|
||||
fuse_swiglu: bool = False,
|
||||
exact_swiglu: bool = False,
|
||||
mk_swiglu: bool = False,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.fc_in = ReplicatedLinear(
|
||||
@@ -157,6 +170,7 @@ class MiniMaxH3FeedForward(nn.Module):
|
||||
)
|
||||
self.fuse_swiglu = fuse_swiglu
|
||||
self.exact_swiglu = exact_swiglu
|
||||
self.mk_swiglu = mk_swiglu
|
||||
self.use_mxfp8 = isinstance(self.fc_in.quant_method, MXFP8QuantizeMethod) and isinstance(
|
||||
self.fc_out.quant_method, MXFP8QuantizeMethod)
|
||||
# Inference-only token chunking: the 2 * ffn_dim intermediate is ~5.3x the block input
|
||||
@@ -181,7 +195,10 @@ class MiniMaxH3FeedForward(nn.Module):
|
||||
return mxfp8_swiglu_feed_forward(hidden_states, self.fc_in, self.fc_out)
|
||||
hidden_states, _ = self.fc_in(hidden_states)
|
||||
if self.fuse_swiglu and _can_run_minimax_h3_fusion(hidden_states):
|
||||
hidden_states = minimax_h3_swiglu(hidden_states)
|
||||
if self.mk_swiglu and mk_kernels.HAVE_TRITON and not torch.compiler.is_compiling():
|
||||
hidden_states = mk_kernels.swiglu(hidden_states)
|
||||
else:
|
||||
hidden_states = minimax_h3_swiglu(hidden_states)
|
||||
elif self.exact_swiglu and exact_kernels.supports_rowwise(hidden_states):
|
||||
hidden_states = exact_kernels.swiglu(hidden_states)
|
||||
else:
|
||||
@@ -206,6 +223,7 @@ class MiniMaxH3Attention(nn.Module):
|
||||
fuse_qknorm_rope: bool = False,
|
||||
fa4_packed_varlen: bool = False,
|
||||
exact_rope: bool = False,
|
||||
mk_qkv: bool = False,
|
||||
vsa_fp4: dict[str, Any] | None = None,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
@@ -244,6 +262,7 @@ class MiniMaxH3Attention(nn.Module):
|
||||
)
|
||||
self.fuse_qknorm_rope = fuse_qknorm_rope
|
||||
self.exact_rope = exact_rope
|
||||
self.mk_qkv = mk_qkv
|
||||
# VSA carries a learned gate on its pooled-compression branch. The H3
|
||||
# checkpoint has no such weight, so the loader zero-initializes it
|
||||
# (ALLOWED_NEW_PARAM_PATTERNS) and the branch is exactly disabled
|
||||
@@ -401,6 +420,22 @@ class MiniMaxH3Attention(nn.Module):
|
||||
cos, sin = rotary_emb
|
||||
cos = cos.to(query.dtype)
|
||||
sin = sin.to(query.dtype)
|
||||
if (self.mk_qkv and mk_kernels.HAVE_TRITON and not torch.compiler.is_compiling()
|
||||
and self.norm_q.eps == _MK_NORM_EPS
|
||||
and self.norm_k.eps == _MK_NORM_EPS and type(self.distributed_attention) is DistributedAttention
|
||||
and not (self.to_gate_compress is not None and self._gate_active())):
|
||||
# One launch replaces the two Sol QK-norm/RoPE kernels and the
|
||||
# ``torch.cat`` stack: the prologue writes a contiguous
|
||||
# ``[3*B, S, H, D]`` buffer that ``forward_stacked`` feeds to the
|
||||
# backend directly, so q/k/v are never copied again.
|
||||
qkv = mk_kernels.qkv_prologue(query, key, value, self.norm_q.weight, self.norm_k.weight, cos, sin)
|
||||
with STAGES.span("attention"):
|
||||
hidden_states, _ = self.distributed_attention.forward_stacked(
|
||||
qkv, original_seq_len=original_seq_len, attn_metadata=attn_metadata)
|
||||
hidden_states = hidden_states.flatten(2, 3).type_as(qkv)
|
||||
with STAGES.span("out_proj"):
|
||||
hidden_states, _ = self.to_out(hidden_states)
|
||||
return hidden_states
|
||||
query = fused_qknorm_rope(query, self.norm_q.weight, cos, sin, self.norm_q.eps)
|
||||
key = fused_qknorm_rope(key, self.norm_k.weight, cos, sin, self.norm_k.eps)
|
||||
else:
|
||||
@@ -658,6 +693,7 @@ class MiniMaxH3TransformerBlock(nn.Module):
|
||||
fa4_packed_varlen: bool = False,
|
||||
exact_kernels: frozenset[str] = frozenset(),
|
||||
vsa_fp4: dict[str, Any] | None = None,
|
||||
mk_tier: bool = False,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.norm1 = nn.RMSNorm(hidden_size, eps=norm_eps)
|
||||
@@ -673,6 +709,7 @@ class MiniMaxH3TransformerBlock(nn.Module):
|
||||
fa4_packed_varlen=fa4_packed_varlen,
|
||||
exact_rope="rope" in exact_kernels,
|
||||
vsa_fp4=vsa_fp4,
|
||||
mk_qkv=mk_tier,
|
||||
)
|
||||
self.norm2 = nn.RMSNorm(hidden_size, eps=norm_eps)
|
||||
self.ff = MiniMaxH3FeedForward(
|
||||
@@ -682,6 +719,7 @@ class MiniMaxH3TransformerBlock(nn.Module):
|
||||
prefix=f"{prefix}.ff",
|
||||
fuse_swiglu=fuse_swiglu,
|
||||
exact_swiglu="swiglu" in exact_kernels,
|
||||
mk_swiglu=mk_tier,
|
||||
)
|
||||
self.adaln_proj = MiniMaxH3AdaLayerNormModulation(
|
||||
time_embed_dim,
|
||||
@@ -692,6 +730,7 @@ class MiniMaxH3TransformerBlock(nn.Module):
|
||||
)
|
||||
self.fuse_modulate = fuse_modulate
|
||||
self.exact_modulate = "modulate" in exact_kernels
|
||||
self.mk_tier = mk_tier
|
||||
|
||||
def forward(
|
||||
self,
|
||||
@@ -711,15 +750,20 @@ class MiniMaxH3TransformerBlock(nn.Module):
|
||||
and exact_kernels.supports_rowwise(hidden_states, shift_msa, scale_msa, gate_msa,
|
||||
shift_mlp, scale_mlp, gate_mlp))
|
||||
if use_modulate_fusion:
|
||||
with nvtx_range("minimax_h3.transformer_block.modulate_fusion"):
|
||||
norm_hidden_states = fused_rmsnorm_modulate(
|
||||
hidden_states,
|
||||
self.norm1.weight,
|
||||
scale_msa,
|
||||
shift_msa,
|
||||
adaln_indices,
|
||||
self.norm1.eps,
|
||||
)
|
||||
if (self.mk_tier and mk_kernels.HAVE_TRITON and not torch.compiler.is_compiling()
|
||||
and self.norm1.eps == _MK_NORM_EPS):
|
||||
norm_hidden_states = mk_kernels.rmsnorm_modulate(hidden_states, self.norm1.weight, scale_msa,
|
||||
shift_msa, adaln_indices)
|
||||
else:
|
||||
with nvtx_range("minimax_h3.transformer_block.modulate_fusion"):
|
||||
norm_hidden_states = fused_rmsnorm_modulate(
|
||||
hidden_states,
|
||||
self.norm1.weight,
|
||||
scale_msa,
|
||||
shift_msa,
|
||||
adaln_indices,
|
||||
self.norm1.eps,
|
||||
)
|
||||
elif use_exact_modulate:
|
||||
with nvtx_range("minimax_h3.transformer_block.exact_modulate"):
|
||||
norm_hidden_states = exact_kernels.modulate(self.norm1(hidden_states), scale_msa, shift_msa,
|
||||
@@ -735,17 +779,23 @@ class MiniMaxH3TransformerBlock(nn.Module):
|
||||
# overlaps three full-width activations during the residual fusion.
|
||||
del norm_hidden_states
|
||||
if use_modulate_fusion:
|
||||
with nvtx_range("minimax_h3.transformer_block.modulate_fusion"):
|
||||
hidden_states, norm_hidden_states = fused_residual_gate_rmsnorm_modulate(
|
||||
hidden_states,
|
||||
attention_output,
|
||||
gate_msa,
|
||||
self.norm2.weight,
|
||||
scale_mlp,
|
||||
shift_mlp,
|
||||
adaln_indices,
|
||||
self.norm2.eps,
|
||||
)
|
||||
if (self.mk_tier and mk_kernels.HAVE_TRITON and not torch.compiler.is_compiling()
|
||||
and self.norm2.eps == _MK_NORM_EPS):
|
||||
hidden_states, norm_hidden_states = mk_kernels.residual_gate_rmsnorm_modulate(
|
||||
hidden_states, attention_output, gate_msa, self.norm2.weight, scale_mlp, shift_mlp,
|
||||
adaln_indices)
|
||||
else:
|
||||
with nvtx_range("minimax_h3.transformer_block.modulate_fusion"):
|
||||
hidden_states, norm_hidden_states = fused_residual_gate_rmsnorm_modulate(
|
||||
hidden_states,
|
||||
attention_output,
|
||||
gate_msa,
|
||||
self.norm2.weight,
|
||||
scale_mlp,
|
||||
shift_mlp,
|
||||
adaln_indices,
|
||||
self.norm2.eps,
|
||||
)
|
||||
elif use_exact_modulate:
|
||||
with nvtx_range("minimax_h3.transformer_block.exact_modulate"):
|
||||
hidden_states = exact_kernels.gate_residual(hidden_states, gate_msa, attention_output, adaln_indices)
|
||||
@@ -761,6 +811,8 @@ class MiniMaxH3TransformerBlock(nn.Module):
|
||||
with nvtx_range("minimax_h3.transformer_block.feed_forward"), STAGES.span("feed_forward"):
|
||||
feed_forward_output = self.ff(norm_hidden_states)
|
||||
if use_modulate_fusion and not torch.compiler.is_compiling():
|
||||
if self.mk_tier and mk_kernels.HAVE_TRITON and not torch.compiler.is_compiling():
|
||||
return mk_kernels.gated_residual(hidden_states, gate_mlp, feed_forward_output, adaln_indices)
|
||||
return _gated_residual(hidden_states, gate_mlp, adaln_indices, feed_forward_output)
|
||||
if use_exact_modulate:
|
||||
return exact_kernels.gate_residual(hidden_states, gate_mlp, feed_forward_output, adaln_indices)
|
||||
@@ -821,6 +873,10 @@ class MiniMaxH3Transformer3DModel(BaseDiT):
|
||||
logger.warning(
|
||||
"FASTVIDEO_MINIMAX_H3_FUSIONS requested %s but Triton is unavailable; "
|
||||
"every forward stays on the eager path.", ",".join(sorted(self.enabled_fusions)))
|
||||
self.use_mk_kernels = _MINIMAX_H3_MK_MODE in self.enabled_fusions
|
||||
if self.use_mk_kernels and not mk_kernels.HAVE_TRITON:
|
||||
logger.warning("FASTVIDEO_MINIMAX_H3_FUSIONS=mk requested but the MotionKernel kernels are "
|
||||
"unavailable (Triton missing); every forward stays on the eager path.")
|
||||
self.enabled_exact_kernels = _enabled_minimax_h3_exact_kernels()
|
||||
if self.enabled_exact_kernels:
|
||||
if exact_kernels.HAVE_TRITON:
|
||||
@@ -935,6 +991,7 @@ class MiniMaxH3Transformer3DModel(BaseDiT):
|
||||
fa4_packed_varlen=envs.FASTVIDEO_MINIMAX_H3_FA4_PACKED_VARLEN.get(),
|
||||
exact_kernels=self.enabled_exact_kernels,
|
||||
vsa_fp4=config.vsa_fp4,
|
||||
mk_tier=self.use_mk_kernels,
|
||||
) for index in range(arch.num_layers)
|
||||
])
|
||||
self.norm_out = MiniMaxH3AdaLayerNormOut(
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""MotionKernel H3 kernels (opt-in through ``FASTVIDEO_MINIMAX_H3_FUSIONS=mk``).
|
||||
|
||||
These are the MotionKernel agent-discovered Triton kernels for the MiniMax-H3
|
||||
inference blocks, kept separate from the Sol-Engine kernels in the parent
|
||||
package so the default path stays byte-for-byte identical. Each kernel is a
|
||||
drop-in replacement for one Sol fusion or eager expression:
|
||||
|
||||
============================================ ======================================
|
||||
MotionKernel replaces
|
||||
============================================ ======================================
|
||||
``qkv_prologue`` 2x ``fused_qknorm_rope`` + the
|
||||
``torch.cat`` QKV stack
|
||||
``rmsnorm_modulate`` ``fused_rmsnorm_modulate``
|
||||
``residual_gate_rmsnorm_modulate`` ``fused_residual_gate_rmsnorm_modulate``
|
||||
``swiglu`` ``minimax_h3_swiglu``
|
||||
``gated_residual`` the compiled ``_gated_residual``
|
||||
============================================ ======================================
|
||||
|
||||
All five run in fp32 internally with a single rounding at the bf16/fp16 store
|
||||
and require the production H3 norm eps of ``1e-5`` (enforced at the call sites).
|
||||
"""
|
||||
|
||||
try:
|
||||
import triton # noqa: F401 (each module imports it directly)
|
||||
|
||||
from .gated_residual import gated_residual
|
||||
from .qkv_prologue import qkv_prologue
|
||||
from .residual_gate_rmsnorm_modulate import residual_gate_rmsnorm_modulate
|
||||
from .rmsnorm_modulate import rmsnorm_modulate
|
||||
from .swiglu import swiglu
|
||||
|
||||
HAVE_TRITON = True
|
||||
except ImportError: # pragma: no cover - environments without Triton
|
||||
gated_residual = None
|
||||
qkv_prologue = None
|
||||
residual_gate_rmsnorm_modulate = None
|
||||
rmsnorm_modulate = None
|
||||
swiglu = None
|
||||
HAVE_TRITON = False
|
||||
|
||||
__all__ = [
|
||||
"HAVE_TRITON",
|
||||
"gated_residual",
|
||||
"qkv_prologue",
|
||||
"residual_gate_rmsnorm_modulate",
|
||||
"rmsnorm_modulate",
|
||||
"swiglu",
|
||||
]
|
||||
@@ -0,0 +1,151 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""MotionKernel H3 kernel: Fused H3 row-indexed gated residual (hidden + gate[index] * branch).
|
||||
|
||||
Found with MotionKernel (agent-driven kernel search). Tuned on GB200 / sm_100 for the
|
||||
MiniMax-H3 inference shapes; opt-in through ``FASTVIDEO_MINIMAX_H3_FUSIONS=mk``.
|
||||
|
||||
Triton kernel for MiniMax H3's post-FFN row-indexed gated residual.
|
||||
|
||||
``out = hidden + gate[index] * branch`` with the math in fp32 and a single
|
||||
bf16 rounding at the store.
|
||||
|
||||
Design notes (GB200, sm_100a):
|
||||
* The op moves 3 activation-sized streams (2 reads + 1 write); it is purely
|
||||
bandwidth bound. A flat 1-D grid over ``tokens * hidden`` gives fully
|
||||
coalesced, 16-byte-vectorized accesses on all three streams with no lane
|
||||
padding (the 5376-wide rows are not padded to a power of two).
|
||||
* Split full-block / tail code paths. Every non-tail block runs completely
|
||||
unmasked, so the compiler emits unpredicated 128-bit loads/stores; only the
|
||||
final block pays for predicates. Measured 175.6 -> 173.6 us weighted over
|
||||
the five production shapes (~1.1%), consistently on every shape.
|
||||
* The gathered AdaLN gate row is tiny (<= 12 rows) so it stays resident in
|
||||
L2; the per-element row/column split needed to address it costs only ALU,
|
||||
of which this kernel uses ~1% of peak.
|
||||
* 32-bit offsets (all production shapes fit) halve index register pressure.
|
||||
* ``evict_last`` on the three loads is worth ~2%: it keeps the streamed
|
||||
lines in L2 long enough for the DRAM scheduler to coalesce the
|
||||
read/write turnaround (measured 269 -> 266 us at 59k tokens).
|
||||
* Roofline: a minimal unmasked 2-read/1-write Triton ``c = a + b`` kernel
|
||||
(same bytes, no gather) runs at 267.7 us / 7.11 TB/s at the largest shape;
|
||||
this kernel measures 263.2 us / 7.24 TB/s, i.e. it is at (slightly above)
|
||||
the measured 3-stream streaming roofline, so the body is not tunable
|
||||
further.
|
||||
"""
|
||||
|
||||
import torch
|
||||
import triton
|
||||
import triton.language as tl
|
||||
|
||||
|
||||
@triton.jit
|
||||
def _h3_gated_residual_kernel(
|
||||
out_ptr,
|
||||
hidden_ptr,
|
||||
branch_ptr,
|
||||
gate_ptr,
|
||||
index_ptr,
|
||||
n_elems,
|
||||
n_index,
|
||||
D: tl.constexpr,
|
||||
GS: tl.constexpr,
|
||||
BLOCK: tl.constexpr,
|
||||
NFULL: tl.constexpr,
|
||||
WIDE: tl.constexpr,
|
||||
):
|
||||
pid = tl.program_id(0)
|
||||
if WIDE:
|
||||
off = pid.to(tl.int64) * BLOCK + tl.arange(0, BLOCK)
|
||||
mask = off < n_elems
|
||||
row = off // D
|
||||
col = off - row * D
|
||||
table_row = tl.load(index_ptr + row % n_index, mask=mask, other=0).to(tl.int32)
|
||||
hidden = tl.load(hidden_ptr + off, mask=mask, eviction_policy="evict_last").to(tl.float32)
|
||||
branch = tl.load(branch_ptr + off, mask=mask, eviction_policy="evict_last").to(tl.float32)
|
||||
gate = tl.load(gate_ptr + table_row * GS + col, mask=mask, eviction_policy="evict_last").to(tl.float32)
|
||||
tl.store(
|
||||
out_ptr + off,
|
||||
(hidden + gate * branch).to(out_ptr.dtype.element_ty),
|
||||
mask=mask,
|
||||
)
|
||||
else:
|
||||
base = pid * BLOCK
|
||||
if base + BLOCK <= NFULL:
|
||||
off = base + tl.arange(0, BLOCK)
|
||||
row = off // D
|
||||
col = off - row * D
|
||||
table_row = tl.load(index_ptr + row % n_index).to(tl.int32)
|
||||
hidden = tl.load(hidden_ptr + off, eviction_policy="evict_last").to(tl.float32)
|
||||
branch = tl.load(branch_ptr + off, eviction_policy="evict_last").to(tl.float32)
|
||||
gate = tl.load(gate_ptr + table_row * GS + col, eviction_policy="evict_last").to(tl.float32)
|
||||
tl.store(out_ptr + off, (hidden + gate * branch).to(out_ptr.dtype.element_ty))
|
||||
else:
|
||||
off = base + tl.arange(0, BLOCK)
|
||||
mask = off < n_elems
|
||||
row = off // D
|
||||
col = off - row * D
|
||||
table_row = tl.load(index_ptr + row % n_index, mask=mask, other=0).to(tl.int32)
|
||||
hidden = tl.load(hidden_ptr + off, mask=mask, eviction_policy="evict_last").to(tl.float32)
|
||||
branch = tl.load(branch_ptr + off, mask=mask, eviction_policy="evict_last").to(tl.float32)
|
||||
gate = tl.load(
|
||||
gate_ptr + table_row * GS + col,
|
||||
mask=mask,
|
||||
eviction_policy="evict_last",
|
||||
).to(tl.float32)
|
||||
tl.store(
|
||||
out_ptr + off,
|
||||
(hidden + gate * branch).to(out_ptr.dtype.element_ty),
|
||||
mask=mask,
|
||||
)
|
||||
|
||||
|
||||
def gated_residual(
|
||||
hidden: torch.Tensor,
|
||||
gate: torch.Tensor,
|
||||
branch: torch.Tensor,
|
||||
index: torch.Tensor,
|
||||
) -> torch.Tensor:
|
||||
"""``hidden + gate[index] * branch`` in one pass."""
|
||||
if not hidden.is_cuda:
|
||||
raise ValueError("h3_gated_residual requires CUDA tensors")
|
||||
if branch.shape != hidden.shape:
|
||||
raise ValueError("branch must match hidden")
|
||||
hidden_size = hidden.shape[-1]
|
||||
if index.ndim != 1 or index.numel() != hidden.shape[-2]:
|
||||
raise ValueError("index must hold one table row per token")
|
||||
if gate.shape[-1] != hidden_size:
|
||||
raise ValueError("gate table must match the hidden width")
|
||||
flat_hidden = hidden.reshape(-1, hidden_size)
|
||||
flat_branch = branch.reshape(-1, hidden_size)
|
||||
if not flat_hidden.is_contiguous():
|
||||
flat_hidden = flat_hidden.contiguous()
|
||||
if not flat_branch.is_contiguous():
|
||||
flat_branch = flat_branch.contiguous()
|
||||
if gate.stride(-1) != 1:
|
||||
gate = gate.contiguous()
|
||||
gate_row_stride = gate.stride(0)
|
||||
index = index.contiguous()
|
||||
out = torch.empty_like(flat_hidden)
|
||||
|
||||
n_elems = flat_hidden.numel()
|
||||
if n_elems == 0:
|
||||
return out.view_as(hidden)
|
||||
block = 4096
|
||||
wide = n_elems >= 2**31
|
||||
nfull = (n_elems // block) * block
|
||||
grid = (triton.cdiv(n_elems, block), )
|
||||
_h3_gated_residual_kernel[grid](
|
||||
out,
|
||||
flat_hidden,
|
||||
flat_branch,
|
||||
gate,
|
||||
index,
|
||||
n_elems,
|
||||
index.numel(),
|
||||
D=hidden_size,
|
||||
GS=gate_row_stride,
|
||||
BLOCK=block,
|
||||
NFULL=nfull,
|
||||
WIDE=wide,
|
||||
num_warps=8,
|
||||
)
|
||||
return out.view_as(hidden)
|
||||
@@ -0,0 +1,190 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""MotionKernel H3 kernel: Fused H3 QKV prologue: per-head QK RMSNorm + partial RoPE + stacked QKV output.
|
||||
|
||||
Found with MotionKernel (agent-driven kernel search). Tuned on GB200 / sm_100 for the
|
||||
MiniMax-H3 inference shapes; opt-in through ``FASTVIDEO_MINIMAX_H3_FUSIONS=mk``.
|
||||
|
||||
Fused QKV prologue: per-head QK RMSNorm + partial RoPE on Q and K plus a straight V copy.
|
||||
|
||||
Production runs two Sol-Engine ``_qknorm_partial_rope_kernel`` launches (one Triton program per
|
||||
128-wide (token, head) row of Q and of K) plus ``torch.cat([q, k, v])``. Here a single launch
|
||||
produces Q, K and V together, so every byte is read and written exactly once.
|
||||
|
||||
Each program owns ``HB`` consecutive head rows of one token, i.e. a *contiguous flat* chunk of
|
||||
``HB * head_dim`` elements. Because the tile is flat and contiguous, each tensor costs exactly
|
||||
one wide (128-bit) load and one wide store per thread instead of a pile of per-sub-block
|
||||
addressing. The grid is ordered so that the head-group index is the *fastest* varying dimension:
|
||||
consecutive CTAs then cover one token's whole ``H * D`` slab in address order, which turns the
|
||||
six DRAM streams (q, k, v in; q, k, v out) into pure sequential scans (~7.1 vs ~6.6 TB/s).
|
||||
|
||||
All three input tiles are issued as loads *before* any store (q, k and v in flight together,
|
||||
then the stores), so the six streams overlap instead of being serialised by the store that
|
||||
follows each load. With ``HB=4`` (512 elements, one warp) this reaches the measured streaming
|
||||
roofline of this GPU: 7.08 TB/s versus 7.10 TB/s for a pure three-stream copy of the same bytes
|
||||
(the previous load/store/load/store schedule measured 7.00 TB/s).
|
||||
|
||||
The rotate-half partner ``x[i +/- rotary_dim/2]`` is materialised with an in-register
|
||||
``tl.gather`` along the head dim (warp-shuffle lowering for a single warp), so the whole row is
|
||||
loaded once: no gather loads from memory, no lane padding. All arithmetic is fp32 with one
|
||||
rounding at the bf16 store; V is copied verbatim.
|
||||
"""
|
||||
|
||||
import torch
|
||||
import triton
|
||||
import triton.language as tl
|
||||
|
||||
H3_NORM_EPS = 1e-5
|
||||
|
||||
|
||||
@triton.jit
|
||||
def _qkv_norm_rope_kernel(
|
||||
q_ptr,
|
||||
k_ptr,
|
||||
v_ptr,
|
||||
qw_ptr,
|
||||
kw_ptr,
|
||||
cos_ptr,
|
||||
sin_ptr,
|
||||
out_ptr,
|
||||
num_heads,
|
||||
seq_len,
|
||||
plane, # elements per output plane = B*S*H*D
|
||||
eps,
|
||||
HD: tl.constexpr,
|
||||
RD: tl.constexpr,
|
||||
HB: tl.constexpr,
|
||||
NHG: tl.constexpr,
|
||||
I64: tl.constexpr,
|
||||
):
|
||||
# 1-D grid: gridDim.y is capped at 65535, below the 73k-token shape.
|
||||
pid = tl.program_id(0)
|
||||
h0 = (pid % NHG) * HB
|
||||
bt = pid // NHG
|
||||
tok = bt % seq_len
|
||||
batch = bt // seq_len
|
||||
|
||||
NB: tl.constexpr = HB * HD
|
||||
off = tl.arange(0, NB)
|
||||
hrow = h0 + off // HD
|
||||
mask = hrow < num_heads
|
||||
|
||||
if I64:
|
||||
base = (batch.to(tl.int64) * seq_len + tok) * num_heads * HD + h0 * HD
|
||||
else:
|
||||
base = (batch * seq_len + tok) * num_heads * HD + h0 * HD
|
||||
|
||||
idx = tl.arange(0, HD)
|
||||
pidx = tl.where(idx < RD, tl.where(idx < RD // 2, idx + RD // 2, idx - RD // 2), idx)
|
||||
gidx = tl.broadcast_to(pidx[None, :], (HB, HD))
|
||||
c = tl.load(cos_ptr + tok * RD + idx, mask=idx < RD, other=1.0).to(tl.float32)[None, :]
|
||||
s = tl.load(sin_ptr + tok * RD + idx, mask=idx < RD, other=0.0).to(tl.float32)
|
||||
s = tl.where(idx < RD // 2, -s, s)[None, :]
|
||||
|
||||
wq = tl.load(qw_ptr + idx).to(tl.float32)[None, :]
|
||||
wk = tl.load(kw_ptr + idx).to(tl.float32)[None, :]
|
||||
|
||||
xq = tl.load(q_ptr + base + off, mask=mask, other=0.0).to(tl.float32)
|
||||
xk = tl.load(k_ptr + base + off, mask=mask, other=0.0).to(tl.float32)
|
||||
xv = tl.load(v_ptr + base + off, mask=mask, other=0.0)
|
||||
|
||||
tl.store(out_ptr + 2 * plane + base + off, xv, mask=mask)
|
||||
|
||||
y = tl.reshape(xq, (HB, HD))
|
||||
inv = tl.math.rsqrt(tl.sum(y * y, 1) / HD + eps)[:, None]
|
||||
y = y * inv * wq
|
||||
p = tl.gather(y, gidx, 1)
|
||||
y = y * c + p * s
|
||||
tl.store(out_ptr + base + off, tl.reshape(y, (NB, )).to(out_ptr.dtype.element_ty), mask=mask)
|
||||
|
||||
y = tl.reshape(xk, (HB, HD))
|
||||
inv = tl.math.rsqrt(tl.sum(y * y, 1) / HD + eps)[:, None]
|
||||
y = y * inv * wk
|
||||
p = tl.gather(y, gidx, 1)
|
||||
y = y * c + p * s
|
||||
tl.store(out_ptr + plane + base + off, tl.reshape(y, (NB, )).to(out_ptr.dtype.element_ty), mask=mask)
|
||||
|
||||
|
||||
def _pick_config(num_heads: int):
|
||||
"""(HB, num_warps) -- consecutive head rows per program.
|
||||
|
||||
One warp per program with 4 rows gives 16 elements (two 128-bit accesses) per thread per
|
||||
tensor, the measured streaming optimum for this GPU: every row stays inside one warp so the
|
||||
RMSNorm reduction and the rotate-half gather are pure warp shuffles, and 32 such CTAs per SM
|
||||
keep enough loads in flight to run at the copy roofline.
|
||||
"""
|
||||
if num_heads >= 4:
|
||||
return 4, 1
|
||||
hb = max(triton.next_power_of_2(max(num_heads, 1)), 1)
|
||||
return hb, 1
|
||||
|
||||
|
||||
def _launch(
|
||||
query: torch.Tensor,
|
||||
key: torch.Tensor,
|
||||
value: torch.Tensor,
|
||||
q_weight: torch.Tensor,
|
||||
k_weight: torch.Tensor,
|
||||
cos: torch.Tensor,
|
||||
sin: torch.Tensor,
|
||||
) -> torch.Tensor:
|
||||
b, s, h, d = query.shape
|
||||
out = torch.empty((3 * b, s, h, d), device=query.device, dtype=query.dtype)
|
||||
hb, warps = _pick_config(h)
|
||||
nhg = triton.cdiv(h, hb)
|
||||
_qkv_norm_rope_kernel[(b * s * nhg, )](
|
||||
query,
|
||||
key,
|
||||
value,
|
||||
q_weight,
|
||||
k_weight,
|
||||
cos,
|
||||
sin,
|
||||
out,
|
||||
h,
|
||||
s,
|
||||
b * s * h * d,
|
||||
H3_NORM_EPS,
|
||||
HD=d,
|
||||
RD=cos.shape[1],
|
||||
HB=hb,
|
||||
NHG=nhg,
|
||||
I64=out.numel() >= 2**31,
|
||||
num_warps=warps,
|
||||
num_stages=1,
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
def qkv_prologue(
|
||||
query: torch.Tensor,
|
||||
key: torch.Tensor,
|
||||
value: torch.Tensor,
|
||||
q_weight: torch.Tensor,
|
||||
k_weight: torch.Tensor,
|
||||
cos: torch.Tensor,
|
||||
sin: torch.Tensor,
|
||||
) -> torch.Tensor:
|
||||
"""Returns the [3*B, S, H, D] stack (rotated Q, rotated K, V), as FastVideo's attention path builds it."""
|
||||
if query.ndim != 4 or key.shape != query.shape or value.shape != query.shape:
|
||||
raise ValueError("query/key/value must share a [B, S, H, D] shape")
|
||||
if cos.shape != sin.shape or cos.shape[0] != query.shape[1]:
|
||||
raise ValueError("cos/sin must be [S, rotary_dim] tables")
|
||||
if not query.is_contiguous():
|
||||
query = query.contiguous()
|
||||
if not key.is_contiguous():
|
||||
key = key.contiguous()
|
||||
if not value.is_contiguous():
|
||||
value = value.contiguous()
|
||||
if not q_weight.is_contiguous():
|
||||
q_weight = q_weight.contiguous()
|
||||
if not k_weight.is_contiguous():
|
||||
k_weight = k_weight.contiguous()
|
||||
if cos.dtype != query.dtype:
|
||||
cos = cos.to(query.dtype)
|
||||
if sin.dtype != query.dtype:
|
||||
sin = sin.to(query.dtype)
|
||||
if not cos.is_contiguous():
|
||||
cos = cos.contiguous()
|
||||
if not sin.is_contiguous():
|
||||
sin = sin.contiguous()
|
||||
return _launch(query, key, value, q_weight, k_weight, cos, sin)
|
||||
@@ -0,0 +1,137 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""MotionKernel H3 kernel: Fused H3 residual gate + RMSNorm + AdaLN modulation.
|
||||
|
||||
Found with MotionKernel (agent-driven kernel search). Tuned on GB200 / sm_100 for the
|
||||
MiniMax-H3 inference shapes; opt-in through ``FASTVIDEO_MINIMAX_H3_FUSIONS=mk``.
|
||||
|
||||
Chunked gated residual + RMSNorm + AdaLN modulation (H3 post-attention).
|
||||
"""
|
||||
|
||||
import torch
|
||||
import triton
|
||||
import triton.language as tl
|
||||
|
||||
H3_NORM_EPS = 1e-5
|
||||
|
||||
_BLOCK = 1024
|
||||
_NUM_WARPS = 4
|
||||
_NUM_STAGES = 3
|
||||
|
||||
|
||||
@triton.jit
|
||||
def _residual_gate_rmsnorm_modulate_kernel(
|
||||
hidden_out_ptr,
|
||||
normed_out_ptr,
|
||||
residual_ptr,
|
||||
branch_ptr,
|
||||
gate_ptr,
|
||||
weight_ptr,
|
||||
scale_ptr,
|
||||
shift_ptr,
|
||||
index_ptr,
|
||||
n_cols,
|
||||
n_index,
|
||||
eps,
|
||||
stride_gate,
|
||||
stride_scale,
|
||||
stride_shift,
|
||||
BLOCK: tl.constexpr,
|
||||
IDENTITY_INDEX: tl.constexpr,
|
||||
):
|
||||
row = tl.program_id(0).to(tl.int64)
|
||||
cols = tl.arange(0, BLOCK)
|
||||
base = row * n_cols
|
||||
if IDENTITY_INDEX:
|
||||
table_row = tl.load(index_ptr + row).to(tl.int64)
|
||||
else:
|
||||
table_row = tl.load(index_ptr + (row % n_index)).to(tl.int64)
|
||||
gbase = table_row * stride_gate
|
||||
sbase = table_row * stride_scale
|
||||
hbase = table_row * stride_shift
|
||||
|
||||
acc = tl.zeros((BLOCK, ), tl.float32)
|
||||
for c in range(0, n_cols, BLOCK):
|
||||
cur = c + cols
|
||||
m = cur < n_cols
|
||||
off = base + cur
|
||||
residual = tl.load(residual_ptr + off, mask=m, other=0.0).to(tl.float32)
|
||||
branch = tl.load(branch_ptr + off, mask=m, other=0.0).to(tl.float32)
|
||||
gate = tl.load(gate_ptr + gbase + cur, mask=m, other=0.0).to(tl.float32)
|
||||
hidden = residual + gate * branch
|
||||
tl.store(
|
||||
hidden_out_ptr + off,
|
||||
hidden.to(hidden_out_ptr.dtype.element_ty),
|
||||
mask=m,
|
||||
)
|
||||
acc += hidden * hidden
|
||||
|
||||
rstd = tl.math.rsqrt(tl.sum(acc, axis=0) / n_cols + eps)
|
||||
|
||||
for c in range(0, n_cols, BLOCK):
|
||||
cur = c + cols
|
||||
m = cur < n_cols
|
||||
off = base + cur
|
||||
hidden = tl.load(hidden_out_ptr + off, mask=m, other=0.0, cache_modifier=".cg").to(tl.float32)
|
||||
weight = tl.load(weight_ptr + cur, mask=m, other=0.0).to(tl.float32)
|
||||
scale = tl.load(scale_ptr + sbase + cur, mask=m, other=0.0).to(tl.float32)
|
||||
shift = tl.load(shift_ptr + hbase + cur, mask=m, other=0.0).to(tl.float32)
|
||||
output = hidden * rstd * weight * (1.0 + scale) + shift
|
||||
tl.store(
|
||||
normed_out_ptr + off,
|
||||
output.to(normed_out_ptr.dtype.element_ty),
|
||||
mask=m,
|
||||
)
|
||||
|
||||
|
||||
def _row_addressable(table: torch.Tensor) -> torch.Tensor:
|
||||
return table if table.stride(-1) == 1 else table.contiguous()
|
||||
|
||||
|
||||
def 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,
|
||||
) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
"""Return ``(hidden, normed)`` exactly as FastVideo's optimized profile does."""
|
||||
if not residual.is_cuda:
|
||||
raise ValueError("h3_residual_gate_rmsnorm_modulate requires CUDA tensors")
|
||||
if branch.shape != residual.shape or branch.dtype != residual.dtype:
|
||||
raise ValueError("branch must match residual shape and dtype")
|
||||
hidden_size = residual.shape[-1]
|
||||
if index.ndim != 1 or index.numel() != residual.shape[-2]:
|
||||
raise ValueError("index must hold one table row per token")
|
||||
flat_residual = residual.reshape(-1, hidden_size).contiguous()
|
||||
flat_branch = branch.reshape(-1, hidden_size).contiguous()
|
||||
weight = weight.contiguous()
|
||||
gate = _row_addressable(gate)
|
||||
scale = _row_addressable(scale)
|
||||
shift = _row_addressable(shift)
|
||||
index = index.contiguous()
|
||||
hidden = torch.empty_like(flat_residual)
|
||||
modulated = torch.empty_like(flat_residual)
|
||||
_residual_gate_rmsnorm_modulate_kernel[(flat_residual.shape[0], )](
|
||||
hidden,
|
||||
modulated,
|
||||
flat_residual,
|
||||
flat_branch,
|
||||
gate,
|
||||
weight,
|
||||
scale,
|
||||
shift,
|
||||
index,
|
||||
hidden_size,
|
||||
index.numel(),
|
||||
H3_NORM_EPS,
|
||||
gate.stride(0),
|
||||
scale.stride(0),
|
||||
shift.stride(0),
|
||||
IDENTITY_INDEX=index.numel() == flat_residual.shape[0],
|
||||
BLOCK=_BLOCK,
|
||||
num_warps=_NUM_WARPS,
|
||||
num_stages=_NUM_STAGES,
|
||||
)
|
||||
return hidden.view_as(residual), modulated.view_as(residual)
|
||||
@@ -0,0 +1,238 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""MotionKernel H3 kernel: Fused H3 RMSNorm + row-indexed AdaLN modulation.
|
||||
|
||||
Found with MotionKernel (agent-driven kernel search). Tuned on GB200 / sm_100 for the
|
||||
MiniMax-H3 inference shapes; opt-in through ``FASTVIDEO_MINIMAX_H3_FUSIONS=mk``.
|
||||
|
||||
MiniMax H3 RMSNorm + row-indexed AdaLN modulation (two-kernel fast path).
|
||||
|
||||
Math (FastVideo ``minimax_h3.py`` lines 729-731), FP32 internally with one
|
||||
rounding at the store::
|
||||
|
||||
y = rms_norm(x, weight, eps) * (1 + scale[idx]) + shift[idx]
|
||||
|
||||
Shape notes that drive the design (GB200, hidden D = 5376 = 2**8 * 21, so D is
|
||||
*not* a power of two):
|
||||
|
||||
* The vendored Sol-Engine starter runs one program per row with ``BLOCK =
|
||||
next_pow2(D) = 8192`` and ``num_warps=16``; 34% of its lanes are masked off
|
||||
and the whole 8192-lane tile is live in registers.
|
||||
* The AdaLN tables are row-strided views (row stride ``6 * D``) with only
|
||||
``table_rows`` (<= 12) distinct rows, so every token re-reads the same
|
||||
``weight``, ``scale`` and ``shift`` vectors: 3 * D * 2 bytes of gather per
|
||||
row on top of the D * 2 bytes of activation.
|
||||
|
||||
This candidate keeps the public interface and splits the work:
|
||||
|
||||
1. ``_fuse_adaln_kernel`` (tiny: ``table_rows`` rows) folds the per-table-row
|
||||
modulation into ``a[t] = weight * (1 + scale[t])`` and ``b[t] = shift[t]``,
|
||||
stored in the activation dtype. The main pass then gathers two vectors per
|
||||
token instead of three, and they are read at the activation width.
|
||||
2. ``_rmsnorm_modulate_kernel`` is a chunked single-row program with no lane
|
||||
padding: ``ceil(D / BLOCK)`` chunks of ``BLOCK`` lanes (BLOCK = 2048 for
|
||||
D = 5376, i.e. three chunks with only the last one partially masked). It
|
||||
makes a first pass over the row to accumulate the sum of squares, then a
|
||||
second pass that re-reads the row (an L1/L2 hit, the row is 10.5 KiB) and
|
||||
applies the store. That keeps the live register tile at one chunk instead
|
||||
of a whole padded row, which is what the starter loses to.
|
||||
|
||||
Performance notes (GB200, measured against a minimal Triton streaming copy of
|
||||
the same byte count on the same shapes, i.e. the true roofline):
|
||||
|
||||
* copy roofline per production shape: 58.9 / 98.6 / 129.1 / 140.8 / 183.9 us
|
||||
(6.2-6.9 TB/s), mean 122.3 us.
|
||||
* this kernel: mean of five shapes ~126.3 us, i.e. ~94.5% of the copy
|
||||
roofline re-measured for this attempt (55.6 / 95.1 / 124.7 / 138.0 / 184.6 us,
|
||||
mean 119.6 us); on the 59k shape 188.2 us vs 184.6 us (98%).
|
||||
|
||||
The step that mattered most over the starter (134.8 us -> 128.5 us) was making
|
||||
the hidden width a ``tl.constexpr`` and unrolling the chunk loops with
|
||||
``tl.static_range``: the lane masks, the row offset and the reduction become
|
||||
static, so every activation/table/store access compiles to an aligned 16-byte
|
||||
vector access instead of scalar traffic on an unknown-alignment base pointer.
|
||||
"""
|
||||
|
||||
import torch
|
||||
import triton
|
||||
import triton.language as tl
|
||||
|
||||
H3_NORM_EPS = 1e-5
|
||||
|
||||
_MAIN_BLOCK = 2048
|
||||
_TABLE_BLOCK = 1024
|
||||
|
||||
|
||||
@triton.jit
|
||||
def _fuse_adaln_kernel(
|
||||
a_ptr,
|
||||
b_ptr,
|
||||
weight_ptr,
|
||||
scale_ptr,
|
||||
shift_ptr,
|
||||
stride_scale_row,
|
||||
stride_shift_row,
|
||||
NC: tl.constexpr,
|
||||
BLOCK: tl.constexpr,
|
||||
):
|
||||
"""One program per (table row, column chunk): a = w * (1 + scale), b = shift."""
|
||||
row = tl.program_id(0).to(tl.int64)
|
||||
cols = tl.program_id(1) * BLOCK + tl.arange(0, BLOCK)
|
||||
mask = cols < NC
|
||||
w = tl.load(weight_ptr + cols, mask=mask, other=0.0).to(tl.float32)
|
||||
s = tl.load(scale_ptr + row * stride_scale_row + cols, mask=mask, other=0.0).to(tl.float32)
|
||||
h = tl.load(shift_ptr + row * stride_shift_row + cols, mask=mask, other=0.0).to(tl.float32)
|
||||
out = row * NC + cols
|
||||
tl.store(a_ptr + out, (w * (1.0 + s)).to(a_ptr.dtype.element_ty), mask=mask)
|
||||
tl.store(b_ptr + out, h.to(b_ptr.dtype.element_ty), mask=mask)
|
||||
|
||||
|
||||
@triton.jit
|
||||
def _rmsnorm_modulate_kernel(
|
||||
out_ptr,
|
||||
x_ptr,
|
||||
a_ptr,
|
||||
b_ptr,
|
||||
index_ptr,
|
||||
n_index,
|
||||
eps,
|
||||
stride_a_row,
|
||||
stride_b_row,
|
||||
NC: tl.constexpr,
|
||||
BLOCK: tl.constexpr,
|
||||
NCHUNK: tl.constexpr,
|
||||
):
|
||||
"""One program per row: sumsq pass over the row, then an apply pass.
|
||||
|
||||
``NC`` (the hidden width) is a ``constexpr`` so that lane masks, row
|
||||
offsets and the reduction are fully static: every activation, table and
|
||||
store access is then a 16-byte vector access on an aligned base, which is
|
||||
what lifts this pass to the streaming-copy roofline. The row is 10.5 KiB,
|
||||
so the second pass re-reads it from L1 instead of DRAM.
|
||||
"""
|
||||
row = tl.program_id(0).to(tl.int64)
|
||||
cols = tl.arange(0, BLOCK)
|
||||
table_row = tl.load(index_ptr + row % n_index).to(tl.int64)
|
||||
base = row * NC
|
||||
|
||||
acc = tl.zeros([BLOCK], dtype=tl.float32)
|
||||
for chunk in tl.static_range(NCHUNK):
|
||||
off = chunk * BLOCK + cols
|
||||
x = tl.load(x_ptr + base + off, mask=off < NC, other=0.0, eviction_policy="evict_last").to(tl.float32)
|
||||
acc += x * x
|
||||
rstd = 1.0 / tl.sqrt(tl.sum(acc, axis=0) / NC + eps)
|
||||
|
||||
chunk = NCHUNK - 1
|
||||
off = chunk * BLOCK + cols
|
||||
mask = off < NC
|
||||
x = tl.load(x_ptr + base + off, mask=mask, other=0.0, eviction_policy="evict_last").to(tl.float32)
|
||||
a = tl.load(
|
||||
a_ptr + table_row * stride_a_row + off,
|
||||
mask=mask,
|
||||
other=0.0,
|
||||
eviction_policy="evict_last",
|
||||
).to(tl.float32)
|
||||
b = tl.load(
|
||||
b_ptr + table_row * stride_b_row + off,
|
||||
mask=mask,
|
||||
other=0.0,
|
||||
eviction_policy="evict_last",
|
||||
).to(tl.float32)
|
||||
y = x * rstd * a + b
|
||||
tl.store(
|
||||
out_ptr + base + off,
|
||||
y.to(out_ptr.dtype.element_ty),
|
||||
mask=mask,
|
||||
cache_modifier=".cs",
|
||||
)
|
||||
|
||||
for chunk in tl.static_range(NCHUNK - 1):
|
||||
off = chunk * BLOCK + cols
|
||||
mask = off < NC
|
||||
x = tl.load(x_ptr + base + off, mask=mask, other=0.0).to(tl.float32)
|
||||
a = tl.load(a_ptr + table_row * stride_a_row + off, mask=mask, other=0.0).to(tl.float32)
|
||||
b = tl.load(b_ptr + table_row * stride_b_row + off, mask=mask, other=0.0).to(tl.float32)
|
||||
y = x * rstd * a + b
|
||||
tl.store(
|
||||
out_ptr + base + off,
|
||||
y.to(out_ptr.dtype.element_ty),
|
||||
mask=mask,
|
||||
cache_modifier=".cs",
|
||||
)
|
||||
|
||||
|
||||
def _next_power_of_two(value: int) -> int:
|
||||
return 1 << (value - 1).bit_length()
|
||||
|
||||
|
||||
def _main_block(hidden: int) -> int:
|
||||
return min(_MAIN_BLOCK, _next_power_of_two(hidden))
|
||||
|
||||
|
||||
def _num_warps(block: int) -> int:
|
||||
if block >= 512:
|
||||
return 4
|
||||
return 2
|
||||
|
||||
|
||||
def _row_addressable(table: torch.Tensor) -> torch.Tensor:
|
||||
return table if table.stride(-1) == 1 else table.contiguous()
|
||||
|
||||
|
||||
def rmsnorm_modulate(
|
||||
x: torch.Tensor,
|
||||
weight: torch.Tensor,
|
||||
scale: torch.Tensor,
|
||||
shift: torch.Tensor,
|
||||
index: torch.Tensor,
|
||||
) -> torch.Tensor:
|
||||
"""RMSNorm + per-token AdaLN modulation with one activation read and write."""
|
||||
if not x.is_cuda:
|
||||
raise ValueError("h3_rmsnorm_modulate requires CUDA tensors")
|
||||
hidden = x.shape[-1]
|
||||
if index.ndim != 1 or index.numel() != x.shape[-2]:
|
||||
raise ValueError("index must hold one table row per token")
|
||||
if scale.shape[-1] != hidden or shift.shape[-1] != hidden:
|
||||
raise ValueError("modulation tables must match the hidden width")
|
||||
flat_x = x.reshape(-1, hidden).contiguous()
|
||||
weight = weight.contiguous()
|
||||
scale = _row_addressable(scale)
|
||||
shift = _row_addressable(shift)
|
||||
index = index.contiguous()
|
||||
output = torch.empty_like(flat_x)
|
||||
|
||||
table_rows = scale.shape[0]
|
||||
tab_dtype = flat_x.dtype if flat_x.dtype in (torch.bfloat16, torch.float16) else torch.float32
|
||||
fused_a = torch.empty((table_rows, hidden), dtype=tab_dtype, device=flat_x.device)
|
||||
fused_b = torch.empty((table_rows, hidden), dtype=tab_dtype, device=flat_x.device)
|
||||
|
||||
table_block = min(_TABLE_BLOCK, _next_power_of_two(hidden))
|
||||
_fuse_adaln_kernel[(table_rows, triton.cdiv(hidden, table_block))](
|
||||
fused_a,
|
||||
fused_b,
|
||||
weight,
|
||||
scale,
|
||||
shift,
|
||||
scale.stride(0),
|
||||
shift.stride(0),
|
||||
NC=hidden,
|
||||
BLOCK=table_block,
|
||||
num_warps=_num_warps(table_block),
|
||||
)
|
||||
|
||||
block = _main_block(hidden)
|
||||
_rmsnorm_modulate_kernel[(flat_x.shape[0], )](
|
||||
output,
|
||||
flat_x,
|
||||
fused_a,
|
||||
fused_b,
|
||||
index,
|
||||
index.numel(),
|
||||
H3_NORM_EPS,
|
||||
fused_a.stride(0),
|
||||
fused_b.stride(0),
|
||||
NC=hidden,
|
||||
BLOCK=block,
|
||||
NCHUNK=triton.cdiv(hidden, block),
|
||||
num_warps=_num_warps(block),
|
||||
)
|
||||
return output.view_as(x)
|
||||
@@ -0,0 +1,123 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""MotionKernel H3 kernel: Fused H3 SwiGLU over the packed gate/value halves.
|
||||
|
||||
Found with MotionKernel (agent-driven kernel search). Tuned on GB200 / sm_100 for the
|
||||
MiniMax-H3 inference shapes; opt-in through ``FASTVIDEO_MINIMAX_H3_FUSIONS=mk``.
|
||||
|
||||
MiniMax H3 value-first packed SwiGLU -- chunked, unpadded, streaming.
|
||||
|
||||
Math (FastVideo ``minimax_h3.py`` @ 7203b4976, lines 188-189)::
|
||||
|
||||
value, gate = hidden_states.chunk(2, dim=-1) # value first, gate second
|
||||
out = value * F.silu(gate)
|
||||
|
||||
Kept in FP32 throughout with one rounding at the store (matches Sol-Engine).
|
||||
|
||||
Why this beats the vendored Sol-Engine kernel: that kernel runs one program per
|
||||
row with ``BLOCK = next_pow2(ffn) = 16384`` lanes, so 12.5% of every lane group
|
||||
is idle and each program keeps two 16384-wide FP32 tiles live (heavy register
|
||||
pressure, low occupancy). F=14336 = 14 x 1024, so a chunked grid needs no
|
||||
padding at all. Each program handles one 1024-element column chunk of one row:
|
||||
one 128-bit vector load per thread per half, FP32 math, one store.
|
||||
|
||||
Launch-shape notes measured on GB200 (Pika shapes):
|
||||
* A flat 1D grid whose column chunk is ``pid % NTILES`` (so concurrently
|
||||
resident CTAs stream adjacent 2 KiB blocks of the same row) is the fastest
|
||||
enumeration: ~443 us at 37718 tokens = 7.33 TB/s, versus a measured
|
||||
~7.4 TB/s pure-read / 7.8 TB/s pure-write streaming roofline. Enumerating
|
||||
rows fastest costs ~8% (478 us); BLOCK=512 or BLOCK=2048 with more warps
|
||||
cost 2-28%.
|
||||
* The flat grid also removes the ``grid.y <= 65535`` limit that the 768p
|
||||
243-frame shape (73394 rows) exceeds: adding a row-group loop there cost
|
||||
4% (888 us vs 852 us).
|
||||
"""
|
||||
|
||||
import torch
|
||||
import triton
|
||||
import triton.language as tl
|
||||
|
||||
_BLOCK = 1024
|
||||
_NUM_WARPS = 4
|
||||
_I32_LIMIT = 0x7FFFFFFF
|
||||
|
||||
|
||||
@triton.jit
|
||||
def _h3_swiglu_kernel(
|
||||
out_ptr,
|
||||
x_ptr,
|
||||
ffn_dim,
|
||||
n_rows,
|
||||
stride_in_row,
|
||||
stride_out_row,
|
||||
NTILES: tl.constexpr,
|
||||
BLOCK: tl.constexpr,
|
||||
EXACT: tl.constexpr,
|
||||
GUARD: tl.constexpr,
|
||||
I64: tl.constexpr,
|
||||
):
|
||||
pid = tl.program_id(0)
|
||||
col = pid % NTILES
|
||||
row = pid // NTILES
|
||||
cols = col * BLOCK + tl.arange(0, BLOCK)
|
||||
if I64:
|
||||
row = row.to(tl.int64)
|
||||
if GUARD:
|
||||
if row < n_rows:
|
||||
_body(out_ptr, x_ptr, ffn_dim, row, cols, stride_in_row, stride_out_row, BLOCK, EXACT)
|
||||
else:
|
||||
_body(out_ptr, x_ptr, ffn_dim, row, cols, stride_in_row, stride_out_row, BLOCK, EXACT)
|
||||
|
||||
|
||||
@triton.jit
|
||||
def _body(out_ptr, x_ptr, ffn_dim, row, cols, stride_in_row, stride_out_row, BLOCK: tl.constexpr, EXACT: tl.constexpr):
|
||||
in_off = row * stride_in_row + cols
|
||||
if EXACT:
|
||||
value = tl.load(x_ptr + in_off).to(tl.float32)
|
||||
gate = tl.load(x_ptr + in_off + ffn_dim).to(tl.float32)
|
||||
out = value * (gate * tl.sigmoid(gate))
|
||||
tl.store(out_ptr + row * stride_out_row + cols, out.to(out_ptr.dtype.element_ty))
|
||||
else:
|
||||
mask = cols < ffn_dim
|
||||
value = tl.load(x_ptr + in_off, mask=mask, other=0.0).to(tl.float32)
|
||||
gate = tl.load(x_ptr + in_off + ffn_dim, mask=mask, other=0.0).to(tl.float32)
|
||||
out = value * (gate * tl.sigmoid(gate))
|
||||
tl.store(out_ptr + row * stride_out_row + cols, out.to(out_ptr.dtype.element_ty), mask=mask)
|
||||
|
||||
|
||||
def swiglu(x: torch.Tensor) -> torch.Tensor:
|
||||
"""Launch the chunked unpadded SwiGLU."""
|
||||
if not x.is_cuda:
|
||||
raise ValueError("h3_swiglu requires a CUDA tensor")
|
||||
packed_width = x.shape[-1]
|
||||
if packed_width == 0 or packed_width % 2:
|
||||
raise ValueError("h3_swiglu needs a positive even last dimension")
|
||||
ffn_dim = packed_width // 2
|
||||
flat = x.reshape(-1, packed_width)
|
||||
if not flat.is_contiguous():
|
||||
flat = flat.contiguous()
|
||||
output_shape = (*x.shape[:-1], ffn_dim)
|
||||
rows = flat.shape[0]
|
||||
if rows == 0:
|
||||
return torch.empty(output_shape, dtype=x.dtype, device=x.device)
|
||||
out = torch.empty((rows, ffn_dim), dtype=x.dtype, device=x.device)
|
||||
|
||||
block = min(_BLOCK, triton.next_power_of_2(ffn_dim))
|
||||
exact = (ffn_dim % block) == 0
|
||||
ntiles = (ffn_dim + block - 1) // block
|
||||
i64 = rows * packed_width > _I32_LIMIT
|
||||
num_warps = _NUM_WARPS if block >= 1024 else 2
|
||||
_h3_swiglu_kernel[(ntiles * rows, )](
|
||||
out,
|
||||
flat,
|
||||
ffn_dim,
|
||||
rows,
|
||||
flat.stride(0),
|
||||
out.stride(0),
|
||||
NTILES=ntiles,
|
||||
BLOCK=block,
|
||||
EXACT=exact,
|
||||
GUARD=False,
|
||||
I64=i64,
|
||||
num_warps=num_warps,
|
||||
)
|
||||
return out.view(output_shape)
|
||||
@@ -0,0 +1,40 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""``DistributedAttention.forward`` is the stacked path plus a ``torch.cat``."""
|
||||
import torch
|
||||
|
||||
from fastvideo.attention.layer import DistributedAttention
|
||||
|
||||
|
||||
class _Recorder:
|
||||
|
||||
def __init__(self):
|
||||
self.qkv = None
|
||||
self.kwargs = None
|
||||
|
||||
|
||||
def test_forward_cats_then_delegates_to_forward_stacked():
|
||||
rec = _Recorder()
|
||||
|
||||
def fake_forward_stacked(self, qkv, **kwargs):
|
||||
rec.qkv = qkv
|
||||
rec.kwargs = kwargs
|
||||
return torch.zeros(2, 3, 4, 5), None
|
||||
|
||||
original = DistributedAttention.forward_stacked
|
||||
DistributedAttention.forward_stacked = fake_forward_stacked
|
||||
try:
|
||||
q = torch.arange(2 * 3 * 4 * 5, dtype=torch.float32).reshape(2, 3, 4, 5)
|
||||
k = q + 1
|
||||
v = q + 2
|
||||
# Bypass __init__ (it builds a real backend); ``forward`` only needs the
|
||||
# compiler-policy flag and the patched ``forward_stacked``.
|
||||
dummy = object.__new__(DistributedAttention)
|
||||
dummy._compile_forward_enabled = False
|
||||
out, replicated = DistributedAttention.forward(dummy, q, k, v, original_seq_len=3)
|
||||
finally:
|
||||
DistributedAttention.forward_stacked = original
|
||||
|
||||
assert replicated is None
|
||||
assert rec.qkv.shape == (6, 3, 4, 5)
|
||||
assert torch.equal(rec.qkv[:2], q) and torch.equal(rec.qkv[2:4], k) and torch.equal(rec.qkv[4:], v)
|
||||
assert rec.kwargs["original_seq_len"] == 3
|
||||
@@ -0,0 +1,35 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""The MotionKernel ``mk`` tier is opt-in and never changes the default fusion set."""
|
||||
import pytest
|
||||
|
||||
from fastvideo.models.dits.minimax_h3 import (_MINIMAX_H3_FUSION_NAMES, _MINIMAX_H3_MK_MODE,
|
||||
_enabled_minimax_h3_fusions)
|
||||
|
||||
|
||||
def test_default_is_unchanged():
|
||||
for value in ("", "0", "none", None):
|
||||
assert _enabled_minimax_h3_fusions(value) == frozenset()
|
||||
|
||||
|
||||
def test_all_is_unchanged():
|
||||
assert _enabled_minimax_h3_fusions("all") == _MINIMAX_H3_FUSION_NAMES
|
||||
assert _enabled_minimax_h3_fusions("1") == _MINIMAX_H3_FUSION_NAMES
|
||||
|
||||
|
||||
def test_subset_is_unchanged():
|
||||
assert _enabled_minimax_h3_fusions("modulate,swiglu") == frozenset({"modulate", "swiglu"})
|
||||
|
||||
|
||||
def test_mk_enables_every_fusion_plus_the_tier_marker():
|
||||
assert _enabled_minimax_h3_fusions("mk") == _MINIMAX_H3_FUSION_NAMES | {_MINIMAX_H3_MK_MODE}
|
||||
assert _enabled_minimax_h3_fusions(" MK ") == _MINIMAX_H3_FUSION_NAMES | {_MINIMAX_H3_MK_MODE}
|
||||
|
||||
|
||||
def test_mk_cannot_be_combined_with_individual_fusions():
|
||||
with pytest.raises(ValueError, match="whole-tier mode"):
|
||||
_enabled_minimax_h3_fusions("mk,swiglu")
|
||||
|
||||
|
||||
def test_unknown_fusion_still_raises():
|
||||
with pytest.raises(ValueError, match="Unknown MiniMax H3 fusion"):
|
||||
_enabled_minimax_h3_fusions("modulate,nope")
|
||||
@@ -0,0 +1,120 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""A real H3 transformer block runs the MotionKernel tier and matches the Sol tier.
|
||||
|
||||
Small random-initialised block, SDPA backend, no checkpoint: this exercises the
|
||||
``mk_tier`` dispatch of every fused call site (including the stacked QKV
|
||||
prologue) on the actual module wiring rather than on standalone kernels.
|
||||
"""
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from fastvideo.forward_context import set_forward_context
|
||||
from fastvideo.models.dits.minimax_h3 import MiniMaxH3TransformerBlock
|
||||
from fastvideo.models.dits.minimax_h3_fusions import mk
|
||||
from fastvideo.platforms import AttentionBackendEnum
|
||||
|
||||
pytestmark = pytest.mark.skipif(not torch.cuda.is_available() or not mk.HAVE_TRITON,
|
||||
reason="MotionKernel H3 kernels need CUDA + Triton")
|
||||
|
||||
HIDDEN, HEADS, HEAD_DIM, ROTARY_DIM, FFN, TABLE_ROWS, TOKENS = 512, 4, 128, 96, 1024, 6, 256
|
||||
|
||||
|
||||
def _block(mk_tier: bool, fused: bool = True) -> MiniMaxH3TransformerBlock:
|
||||
torch.manual_seed(0)
|
||||
torch.set_default_dtype(torch.bfloat16)
|
||||
try:
|
||||
return MiniMaxH3TransformerBlock(
|
||||
hidden_size=HIDDEN,
|
||||
num_attention_heads=HEADS,
|
||||
attention_head_dim=HEAD_DIM,
|
||||
ffn_dim=FFN,
|
||||
time_embed_dim=128,
|
||||
norm_eps=1e-5,
|
||||
qk_norm_eps=1e-5,
|
||||
supported_attention_backends=(AttentionBackendEnum.TORCH_SDPA, ),
|
||||
quant_config=None,
|
||||
prefix="test",
|
||||
fuse_modulate=fused,
|
||||
fuse_qknorm_rope=fused,
|
||||
fuse_swiglu=fused,
|
||||
mk_tier=mk_tier,
|
||||
).cuda()
|
||||
finally:
|
||||
torch.set_default_dtype(torch.float32)
|
||||
|
||||
|
||||
def _inputs():
|
||||
torch.manual_seed(1)
|
||||
return (torch.randn(1, TOKENS, HIDDEN, device="cuda",
|
||||
dtype=torch.bfloat16), torch.randn(TABLE_ROWS, 128, device="cuda", dtype=torch.bfloat16),
|
||||
torch.randint(0, TABLE_ROWS, (TOKENS, ),
|
||||
device="cuda", dtype=torch.int64), (torch.randn(TOKENS,
|
||||
ROTARY_DIM,
|
||||
device="cuda",
|
||||
dtype=torch.bfloat16),
|
||||
torch.randn(TOKENS,
|
||||
ROTARY_DIM,
|
||||
device="cuda",
|
||||
dtype=torch.bfloat16)), TOKENS)
|
||||
|
||||
|
||||
def test_mk_tier_dispatches_and_matches_sol(monkeypatch, distributed_setup):
|
||||
calls = {
|
||||
name: 0
|
||||
for name in ("qkv_prologue", "rmsnorm_modulate", "residual_gate_rmsnorm_modulate", "swiglu", "gated_residual")
|
||||
}
|
||||
for name in calls:
|
||||
target = getattr(mk, name)
|
||||
|
||||
def counted(*args, _target=target, _name=name, **kwargs):
|
||||
calls[_name] += 1
|
||||
return _target(*args, **kwargs)
|
||||
|
||||
monkeypatch.setattr(mk, name, counted)
|
||||
|
||||
sol, mk_block, eager = _block(False), _block(True), _block(False, fused=False)
|
||||
# The loader zero-initialises ReplicatedLinear (weights arrive from the
|
||||
# checkpoint) and that would make the block an identity, hiding every kernel
|
||||
# difference. Give all three copies the same non-trivial weights instead.
|
||||
torch.manual_seed(2)
|
||||
for name, param in sol.named_parameters():
|
||||
if name.endswith(".weight") and param.dim() == 2 or name.endswith(".bias"):
|
||||
param.data.normal_(0.0, 0.02)
|
||||
elif name.endswith(".weight"):
|
||||
param.data.fill_(1.0)
|
||||
mk_block.load_state_dict(sol.state_dict())
|
||||
eager.load_state_dict(sol.state_dict())
|
||||
inputs = _inputs()
|
||||
with torch.inference_mode(), set_forward_context(current_timestep=0, attn_metadata=None):
|
||||
expected = sol(*inputs)
|
||||
actual = mk_block(*inputs)
|
||||
unfused = eager(*inputs)
|
||||
|
||||
assert all(count > 0 for count in calls.values()), calls
|
||||
base = torch.linalg.vector_norm(expected.float())
|
||||
|
||||
def rel(other):
|
||||
return (torch.linalg.vector_norm(other.float() - expected.float()) / base).item()
|
||||
|
||||
floor, rel = rel(unfused), rel(actual)
|
||||
print(f"eager-vs-sol block rel-L2 = {floor:.3e}; mk-vs-sol block rel-L2 = {rel:.3e}")
|
||||
assert rel <= 3 * floor, (rel, floor)
|
||||
|
||||
|
||||
def test_mk_tier_defers_to_sol_while_compiling(monkeypatch, distributed_setup):
|
||||
"""A traced block must keep using the compile-safe Sol custom ops."""
|
||||
calls = []
|
||||
for name in ("qkv_prologue", "rmsnorm_modulate", "residual_gate_rmsnorm_modulate", "swiglu", "gated_residual"):
|
||||
target = getattr(mk, name)
|
||||
|
||||
def counted(*args, _target=target, _name=name, **kwargs):
|
||||
calls.append(_name)
|
||||
return _target(*args, **kwargs)
|
||||
|
||||
monkeypatch.setattr(mk, name, counted)
|
||||
monkeypatch.setattr(torch.compiler, "is_compiling", lambda: True)
|
||||
|
||||
block = _block(True)
|
||||
with torch.inference_mode(), set_forward_context(current_timestep=0, attn_metadata=None):
|
||||
block(*_inputs())
|
||||
assert calls == []
|
||||
@@ -0,0 +1,94 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""MotionKernel H3 kernels match the in-tree Sol/eager paths on the production shapes.
|
||||
|
||||
GPU-only (Triton on CUDA); the shapes use H3's real hidden width, head layout and
|
||||
AdaLN table stride, only the token count is reduced to keep the test quick.
|
||||
"""
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from fastvideo.models.dits.minimax_h3_fusions import (fused_qknorm_rope, fused_residual_gate_rmsnorm_modulate,
|
||||
fused_rmsnorm_modulate, minimax_h3_swiglu)
|
||||
from fastvideo.models.dits.minimax_h3_fusions import mk
|
||||
|
||||
pytestmark = pytest.mark.skipif(not torch.cuda.is_available() or not mk.HAVE_TRITON,
|
||||
reason="MotionKernel H3 kernels need CUDA + Triton")
|
||||
|
||||
HIDDEN, HEADS, HEAD_DIM, ROTARY_DIM, FFN_DIM, EPS = 5376, 56, 128, 96, 14336, 1e-5
|
||||
TOKENS = 512
|
||||
|
||||
|
||||
def _rel_l2(actual, expected):
|
||||
return (torch.linalg.vector_norm(actual.float() - expected.float()) /
|
||||
torch.linalg.vector_norm(expected.float())).item()
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def production_inputs():
|
||||
torch.manual_seed(0)
|
||||
device = "cuda"
|
||||
mod = torch.randn(6, 6, HIDDEN, device=device, dtype=torch.bfloat16)
|
||||
# The production AdaLN tables are row-strided views of this [rows, 6*D] buffer.
|
||||
shift_msa, scale_msa, gate_msa, shift_mlp, scale_mlp, gate_mlp = (mod[:, i, :] for i in range(6))
|
||||
return {
|
||||
"x": torch.randn(TOKENS, HIDDEN, device=device, dtype=torch.bfloat16),
|
||||
"branch": torch.randn(TOKENS, HIDDEN, device=device, dtype=torch.bfloat16),
|
||||
"weight": torch.randn(HIDDEN, device=device, dtype=torch.bfloat16),
|
||||
"qk_weight": torch.randn(HEAD_DIM, device=device, dtype=torch.bfloat16),
|
||||
"scale_msa": scale_msa,
|
||||
"shift_msa": shift_msa,
|
||||
"gate_msa": gate_msa,
|
||||
"scale_mlp": scale_mlp,
|
||||
"shift_mlp": shift_mlp,
|
||||
"gate_mlp": gate_mlp,
|
||||
"index": torch.randint(0, 6, (TOKENS, ), device=device, dtype=torch.int64),
|
||||
}
|
||||
|
||||
|
||||
def test_rmsnorm_modulate_matches_sol(production_inputs):
|
||||
i = production_inputs
|
||||
expected = fused_rmsnorm_modulate(i["x"], i["weight"], i["scale_msa"], i["shift_msa"], i["index"], EPS)
|
||||
actual = mk.rmsnorm_modulate(i["x"], i["weight"], i["scale_msa"], i["shift_msa"], i["index"])
|
||||
assert actual.shape == expected.shape and actual.dtype == expected.dtype
|
||||
assert _rel_l2(actual, expected) < 5e-3
|
||||
|
||||
|
||||
def test_residual_gate_rmsnorm_modulate_matches_sol(production_inputs):
|
||||
i = production_inputs
|
||||
args = (i["branch"], i["gate_msa"], i["weight"], i["scale_mlp"], i["shift_mlp"], i["index"])
|
||||
exp_hidden, exp_normed = fused_residual_gate_rmsnorm_modulate(i["x"], *args, EPS)
|
||||
act_hidden, act_normed = mk.residual_gate_rmsnorm_modulate(i["x"], *args)
|
||||
assert _rel_l2(act_hidden, exp_hidden) < 1e-3
|
||||
assert _rel_l2(act_normed, exp_normed) < 5e-3
|
||||
|
||||
|
||||
def test_swiglu_matches_sol(production_inputs):
|
||||
packed = torch.randn(TOKENS, 2 * FFN_DIM, device="cuda", dtype=torch.bfloat16)
|
||||
assert _rel_l2(mk.swiglu(packed), minimax_h3_swiglu(packed)) < 1e-3
|
||||
|
||||
|
||||
def test_gated_residual_matches_eager(production_inputs):
|
||||
i = production_inputs
|
||||
expected = i["x"] + i["gate_mlp"].index_select(0, i["index"]) * i["branch"]
|
||||
assert _rel_l2(mk.gated_residual(i["x"], i["gate_mlp"], i["branch"], i["index"]), expected) < 5e-3
|
||||
|
||||
|
||||
def test_qkv_prologue_matches_two_sol_launches(production_inputs):
|
||||
i = production_inputs
|
||||
q = torch.randn(1, TOKENS, HEADS, HEAD_DIM, device="cuda", dtype=torch.bfloat16)
|
||||
k = torch.randn_like(q)
|
||||
v = torch.randn_like(q)
|
||||
cos = torch.randn(TOKENS, ROTARY_DIM, device="cuda", dtype=torch.bfloat16)
|
||||
sin = torch.randn(TOKENS, ROTARY_DIM, device="cuda", dtype=torch.bfloat16)
|
||||
expected = torch.cat(
|
||||
[fused_qknorm_rope(q, i["qk_weight"], cos, sin, EPS),
|
||||
fused_qknorm_rope(k, i["qk_weight"], cos, sin, EPS), v],
|
||||
dim=0)
|
||||
actual = mk.qkv_prologue(q, k, v, i["qk_weight"], i["qk_weight"], cos, sin)
|
||||
assert actual.shape == (3, TOKENS, HEADS, HEAD_DIM)
|
||||
assert _rel_l2(actual, expected) < 1e-4
|
||||
|
||||
|
||||
def test_kernels_reject_cpu_tensors():
|
||||
with pytest.raises(ValueError):
|
||||
mk.swiglu(torch.randn(4, 8))
|
||||
Reference in New Issue
Block a user