[perf]: use packed-varlen FA4 for MiniMax H3

This commit is contained in:
Will Lin
2026-08-22 04:26:14 +00:00
parent 172d1b5dfc
commit 99cd355a24
4 changed files with 191 additions and 0 deletions
@@ -10,6 +10,14 @@ from fastvideo.attention.utils.flash_attn_default import (
flash_attn_func_compilable,
)
if fa_version == "4":
# The FA4 varlen wrapper is already a compile-safe custom op. Keep the
# import conditional so FA2/FA3 environments do not need flash_attn.cute.
from fastvideo.attention.utils.flash_attn_cute import (
flash_attn_varlen_func as flash_attn_varlen_func_compilable, )
else:
flash_attn_varlen_func_compilable = None
from fastvideo.attention.backends.abstract import (
AttentionBackend,
AttentionImpl,
@@ -38,6 +46,15 @@ except ImportError:
_FA4_FP4_AVAILABLE = False
_FA4_QUANT_OPS: tuple | None = None
_FA4_PACKED_VARLEN_RECEIPT_LOGGED = False
def _log_fa4_packed_varlen_receipt() -> None:
"""Emit one grep-friendly route receipt per worker process."""
global _FA4_PACKED_VARLEN_RECEIPT_LOGGED
if not _FA4_PACKED_VARLEN_RECEIPT_LOGGED:
logger.info("MiniMax-H3 dense attention: FA4 packed-varlen inference route enabled")
_FA4_PACKED_VARLEN_RECEIPT_LOGGED = True
def _import_fa4_quant_ops() -> tuple:
@@ -228,6 +245,12 @@ class FlashAttentionImpl(AttentionImpl):
) -> None:
self.causal = causal
self.softmax_scale = softmax_scale
# MiniMax-H3's dense DiT explicitly enables this faster FA4 entry
# point. It remains off for every other model and for the H3 text
# refiner; grad-enabled calls stay on the established fixed path.
self.fa4_packed_varlen = bool(extra_impl_args.get("fa4_packed_varlen", False))
if self.fa4_packed_varlen and fa_version == "4":
_log_fa4_packed_varlen_receipt()
# An explicit ``nvfp4_fa4`` impl arg wins over the process-wide
# FASTVIDEO_NVFP4_FA4 env opt-in, so precision-sensitive layers (e.g.
# the FP32-pinned H3 VAE attention) can force-disable FP4 Q/K
@@ -331,6 +354,29 @@ class FlashAttentionImpl(AttentionImpl):
elif self.nvfp4_fa4:
output = self._forward_nvfp4(query, key, value)
elif (self.fa4_packed_varlen and fa_version == "4" and not torch.is_grad_enabled() and query.shape[0] == 1
and query.shape[1] == key.shape[1] == value.shape[1]):
# FA4's packed-varlen entry point is materially faster for H3's
# long, single-document self-attention. Flatten only the batch
# dimension and describe that one sequence with CUDA int32
# cumulative lengths; the existing custom-op wrapper keeps this
# route traceable under torch.compile(fullgraph=True).
assert flash_attn_varlen_func_compilable is not None
sequence_length = query.shape[1]
cu_seqlens = torch.arange(2, dtype=torch.int32, device=query.device) * sequence_length
output = flash_attn_varlen_func_compilable(
query.squeeze(0),
key.squeeze(0),
value.squeeze(0),
cu_seqlens,
cu_seqlens,
sequence_length,
sequence_length,
dropout_p=0.0,
softmax_scale=self.softmax_scale,
causal=self.causal,
).unsqueeze(0)
else:
# Route through the compilable wrapper so dynamo sees a
# registered op (no graph break) for FA2/FA3; identical
+6
View File
@@ -144,6 +144,7 @@ class MiniMaxH3Attention(nn.Module):
quant_config: QuantizationConfig | None,
prefix: str,
fuse_qknorm_rope: bool = False,
fa4_packed_varlen: bool = False,
) -> None:
super().__init__()
self.num_attention_heads = num_attention_heads
@@ -196,6 +197,7 @@ class MiniMaxH3Attention(nn.Module):
causal=False,
supported_attention_backends=supported_attention_backends,
prefix=prefix,
fa4_packed_varlen=fa4_packed_varlen,
)
self.to_gate_compress: ReplicatedLinear | None = None
# None = unchecked; the first forward tests the loaded weight once and
@@ -465,6 +467,10 @@ class MiniMaxH3TransformerBlock(nn.Module):
quant_config,
prefix=f"{prefix}.attn",
fuse_qknorm_rope=fuse_qknorm_rope,
# The packed multimodal document is a single long self-attention
# sequence. FA4's varlen API is substantially faster for this
# shape; the backend keeps grad/training and non-FA4 calls fixed.
fa4_packed_varlen=True,
)
self.norm2 = nn.RMSNorm(hidden_size, eps=norm_eps)
self.ff = MiniMaxH3FeedForward(
@@ -0,0 +1,103 @@
# SPDX-License-Identifier: Apache-2.0
"""Routing guards for MiniMax-H3's packed-varlen FA4 inference path."""
from __future__ import annotations
import pytest
import torch
flash_attn_module = pytest.importorskip(
"fastvideo.attention.backends.flash_attn",
reason="no usable flash-attention package installed",
exc_type=ImportError,
)
FlashAttentionImpl = flash_attn_module.FlashAttentionImpl
def _build_impl(*, fa4_packed_varlen: bool) -> FlashAttentionImpl:
return FlashAttentionImpl(
num_heads=2,
head_size=8,
causal=False,
softmax_scale=0.125,
num_kv_heads=2,
fa4_packed_varlen=fa4_packed_varlen,
)
def _qkv(batch: int = 1) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
query = torch.randn(batch, 7, 2, 8)
return query, torch.randn_like(query), torch.randn_like(query)
def test_h3_fa4_no_grad_uses_single_sequence_varlen(monkeypatch: pytest.MonkeyPatch) -> None:
calls = []
def varlen(query, key, value, cu_q, cu_k, max_q, max_k, **kwargs):
calls.append((query.shape, key.shape, value.shape, cu_q.tolist(), cu_k.tolist(), max_q, max_k, kwargs))
return value + 1
def unexpected_fixed(*args, **kwargs):
raise AssertionError("H3 FA4 inference took the fixed-length path")
monkeypatch.setattr(flash_attn_module, "fa_version", "4")
monkeypatch.setattr(flash_attn_module, "flash_attn_varlen_func_compilable", varlen)
monkeypatch.setattr(flash_attn_module, "flash_attn_func_compilable", unexpected_fixed)
query, key, value = _qkv()
with torch.no_grad():
output = _build_impl(fa4_packed_varlen=True)._forward_impl(query, key, value, None)
torch.testing.assert_close(output, value + 1)
assert calls == [
(
torch.Size([7, 2, 8]),
torch.Size([7, 2, 8]),
torch.Size([7, 2, 8]),
[0, 7],
[0, 7],
7,
7,
{"dropout_p": 0.0, "softmax_scale": 0.125, "causal": False},
)
]
@pytest.mark.parametrize(
("fa4_packed_varlen", "version", "batch", "requires_grad"),
[
(False, "4", 1, False),
(True, "3", 1, False),
(True, "4", 2, False),
(True, "4", 1, True),
],
)
def test_h3_varlen_guard_preserves_fixed_path(
monkeypatch: pytest.MonkeyPatch,
fa4_packed_varlen: bool,
version: str,
batch: int,
requires_grad: bool,
) -> None:
def unexpected_varlen(*args, **kwargs):
raise AssertionError("packed-varlen route escaped its H3 FA4 inference guard")
def fixed(query, key, value, **kwargs):
del key, value, kwargs
return query + 2
monkeypatch.setattr(flash_attn_module, "fa_version", version)
monkeypatch.setattr(flash_attn_module, "flash_attn_varlen_func_compilable", unexpected_varlen)
monkeypatch.setattr(flash_attn_module, "flash_attn_func_compilable", fixed)
query, key, value = _qkv(batch)
query.requires_grad_(requires_grad)
context = torch.enable_grad() if requires_grad else torch.no_grad()
with context:
output = _build_impl(fa4_packed_varlen=fa4_packed_varlen)._forward_impl(query, key, value, None)
torch.testing.assert_close(output, query + 2)
if requires_grad:
output.sum().backward()
torch.testing.assert_close(query.grad, torch.ones_like(query))
@@ -9,6 +9,42 @@ import torch
from fastvideo.platforms import AttentionBackendEnum
def test_only_packed_minimax_h3_blocks_enable_fa4_varlen(monkeypatch: pytest.MonkeyPatch) -> None:
import fastvideo.models.dits.minimax_h3 as h3
routed: list[bool] = []
class RecordingAttention(torch.nn.Module):
def __init__(self, *args, fa4_packed_varlen: bool = False, **kwargs) -> None:
super().__init__()
del args, kwargs
routed.append(fa4_packed_varlen)
monkeypatch.setattr(h3, "MiniMaxH3Attention", RecordingAttention)
common = dict(
hidden_size=8,
num_attention_heads=1,
attention_head_dim=8,
ffn_dim=16,
norm_eps=1e-5,
qk_norm_eps=1e-5,
supported_attention_backends=(AttentionBackendEnum.FLASH_ATTN, ),
quant_config=None,
)
h3.MiniMaxH3TransformerBlock(
**common,
time_embed_dim=4,
prefix="minimax_h3.transformer_blocks.0",
)
h3.MiniMaxH3TokenRefinerBlock(
**common,
prefix="minimax_h3.token_refiner.refiner_blocks.0",
)
assert routed == [True, False]
@pytest.mark.parametrize(
("raw", "expected"),
[