[feat]: NVFP4 FA4 deploy path applies exact Q/K preconditioning and per-block BF16 attention opt-out

This commit is contained in:
Aryan Kumar
2026-11-01 02:22:35 -08:00
parent 3d4f02673d
commit 47bb2539f8
7 changed files with 226 additions and 12 deletions
+31 -1
View File
@@ -17,6 +17,12 @@ parallel VAE tiles) before the generator starts. ``--nvfp4-decoder`` and
``--nvfp4-dit h3_dit`` loads a packed NVFP4 DiT export (e.g.
FastVideo/Pika-H3-Flex-NVFP4-QAD). They change weights, not the six-forward sampler. Kernel/VSA work is separate.
``--nvfp4-attention`` turns on FlashAttention-4 NVFP4 attention (Q/K in NVFP4, P/V in BF16; Blackwell and FA4 only)
with the named exact Q/K preconditioning, which must match the ``qk_transform`` the checkpoint was QAD-trained with
(``smooth_k+hadamard`` for Pika-H3-Flex-NVFP4-QAD). ``--nvfp4-attention-bf16-blocks 45,49`` keeps those
``transformer_blocks`` on BF16 attention, as in training. ``none`` quantizes Q/K without preconditioning.
Without ``--nvfp4-attention`` attention stays BF16.
``--resolution`` (480p/768p), ``--aspect`` (one of Pika's eleven buckets), and
``--duration`` (5-15 s) pick the bucket and the ``17n + 5`` frame count;
``--height``/``--width``/``--num-frames`` override them with explicit values.
@@ -48,7 +54,7 @@ import os
from pathlib import Path
from typing import Any
from fastvideo import VideoGenerator
from fastvideo import VideoGenerator, envs
from fastvideo.api import (
CompileConfig,
ComponentConfig,
@@ -63,6 +69,7 @@ from fastvideo.api import (
QuantizationConfig,
SamplingConfig,
)
from fastvideo.attention.utils.nvfp4_qk_precondition import QK_TRANSFORMS, parse_block_indices
from fastvideo.models.vision_utils import load_image
from fastvideo.pipelines.basic.minimax_h3 import MiniMaxH3Reference
from fastvideo.pipelines.basic.pika_h3_flex.buckets import (
@@ -135,8 +142,20 @@ def parse_args() -> argparse.Namespace:
parser.add_argument("--nvfp4-dit",
choices=("h3_dit", "h3_dit_ffn"),
help="load a packed NVFP4 DiT export with this layer profile (h3_dit: attention + FFN linears)")
parser.add_argument("--nvfp4-attention",
choices=tuple(QK_TRANSFORMS),
help="FA4 NVFP4 Q/K attention with this exact Q/K preconditioning (default: BF16 attention)")
parser.add_argument("--nvfp4-attention-bf16-blocks",
default="",
help="comma-separated transformer_blocks indices that keep BF16 attention, e.g. 45,49")
parser.add_argument("--output", default="outputs/pika_h3_flex")
args = parser.parse_args()
if args.nvfp4_attention_bf16_blocks and not args.nvfp4_attention:
parser.error("--nvfp4-attention-bf16-blocks needs --nvfp4-attention")
try:
parse_block_indices(args.nvfp4_attention_bf16_blocks)
except ValueError as error:
parser.error(f"--nvfp4-attention-bf16-blocks: {error}")
if args.task in ("i2va", "fl2va") and not args.image:
parser.error(f"--task {args.task} needs --image")
@@ -210,8 +229,19 @@ def _pipeline(args: argparse.Namespace) -> PipelineSelection:
)
def _apply_nvfp4_attention(args: argparse.Namespace) -> None:
"""Workers inherit the environment, so set it before VideoGenerator starts."""
if not args.nvfp4_attention:
return
envs.FASTVIDEO_NVFP4_FA4.set(True)
envs.FASTVIDEO_NVFP4_FA4_QK_TRANSFORM.set(args.nvfp4_attention)
envs.FASTVIDEO_NVFP4_FA4_BF16_BLOCKS.set(args.nvfp4_attention_bf16_blocks)
envs.setdefault_external("CUTE_DSL_ENABLE_TVM_FFI", "1")
def main() -> None:
args = parse_args()
_apply_nvfp4_attention(args)
if args.lossless_accel:
for key, value in LOSSLESS_ACCEL_ENV.items():
os.environ.setdefault(key, value)
+11 -1
View File
@@ -27,6 +27,7 @@ from fastvideo.attention.backends.abstract import (
PackedSequenceMetadata,
)
from fastvideo.attention.backends.attn_qat_infer import _resolve_fa4_pv_mode
from fastvideo.attention.utils.nvfp4_qk_precondition import parse_qk_transform, precondition_qk
from fastvideo.logger import init_logger
logger = init_logger(__name__)
@@ -279,6 +280,12 @@ class FlashAttentionImpl(AttentionImpl):
if nvfp4_fa4 is None:
nvfp4_fa4 = envs.FASTVIDEO_NVFP4_FA4.get()
self.nvfp4_fa4 = bool(nvfp4_fa4)
# Exact Q/K preconditioning ahead of NVFP4 quantization, shared with the QAD emulator
# (fastvideo.attention.utils.nvfp4_qk_precondition). Same arg-over-env precedence as nvfp4_fa4.
qk_transform = extra_impl_args.get("nvfp4_qk_transform")
if qk_transform is None:
qk_transform = envs.FASTVIDEO_NVFP4_FA4_QK_TRANSFORM.get()
self.nvfp4_smooth_k, self.nvfp4_hadamard = parse_qk_transform(qk_transform)
# PV-mode knob for the FA4-FP4 path. Resolved + validated
# unconditionally (shared helper) so a typo fails at construction, not
# at the first forward on a Blackwell box, and so the ATTN_QAT_INFER
@@ -291,7 +298,8 @@ class FlashAttentionImpl(AttentionImpl):
assert cap in [(10, 0), (10, 3)], (f"NVFP4 FA4 requires Blackwell (sm100a/sm103a), got sm{cap[0]}{cap[1]}")
assert _FA4_FP4_AVAILABLE, ("NVFP4 FA4 requires flash-attention-fp4 (flash_attn.cute). "
"Install via instructions in docs/inference/optimizations.md")
logger.info("NVFP4 FA4 enabled for FlashAttentionImpl (quant_qk, pv_mode=%s)", self.fa4_pv_mode)
logger.info("NVFP4 FA4 enabled for FlashAttentionImpl (quant_qk, pv_mode=%s, smooth_k=%s, hadamard=%s)",
self.fa4_pv_mode, self.nvfp4_smooth_k, self.nvfp4_hadamard)
def forward(
self,
@@ -522,6 +530,8 @@ class FlashAttentionImpl(AttentionImpl):
orig_seqlen_q = query.shape[1]
orig_seqlen_k = key.shape[1]
query, key = precondition_qk(query, key, smooth_k=self.nvfp4_smooth_k, hadamard=self.nvfp4_hadamard)
# Quantize Q/K to FP4 (internally pads to multiple of 128 for SF layout)
q_fp4, q_sf = _nvfp4_quantize_for_fa4(query)
k_fp4, k_sf = _nvfp4_quantize_for_fa4(key)
@@ -35,6 +35,18 @@ def parse_qk_transform(name: str | None) -> tuple[bool, bool]:
return QK_TRANSFORMS[key]
def parse_block_indices(raw: str | None) -> frozenset[int]:
"""``"45, 49"`` -> ``frozenset({45, 49})``; empty/``None`` -> empty set."""
items = [item.strip() for item in (raw or "").split(",") if item.strip()]
try:
indices = frozenset(int(item) for item in items)
except ValueError as error:
raise ValueError(f"expected a comma-separated list of block indices, got {raw!r}") from error
if any(index < 0 for index in indices):
raise ValueError(f"block indices must be non-negative, got {raw!r}")
return indices
@lru_cache(maxsize=8)
def _hadamard(n: int, device: torch.device) -> torch.Tensor:
if n <= 0 or n & (n - 1):
@@ -64,4 +76,4 @@ def precondition_qk(query: torch.Tensor, key: torch.Tensor, *, smooth_k: bool,
return q.to(query.dtype), k.to(key.dtype)
__all__ = ["QK_TRANSFORMS", "hadamard_matrix", "parse_qk_transform", "precondition_qk"]
__all__ = ["QK_TRANSFORMS", "hadamard_matrix", "parse_block_indices", "parse_qk_transform", "precondition_qk"]
+13
View File
@@ -368,6 +368,19 @@ FASTVIDEO_NVFP4_FA4 = EnvBool(
category="attention",
doc="FlashAttention-4 quantizes Q and K to NVFP4. An explicit nvfp4_fa4 attention implementation argument "
"takes precedence.")
FASTVIDEO_NVFP4_FA4_QK_TRANSFORM = EnvChoice(
"none",
choices=("none", "smooth_k", "hadamard", "smooth_k+hadamard"),
category="attention",
doc="Exact Q/K preconditioning applied right before NVFP4 quantization on the FlashAttention-4 NVFP4 path: "
"smooth_k subtracts each head's token-mean of K, hadamard rotates Q and K by the head-dim Hadamard matrix. "
"Must match the qk_transform the model was QAD-trained with. An explicit nvfp4_qk_transform attention "
"implementation argument takes precedence.")
FASTVIDEO_NVFP4_FA4_BF16_BLOCKS = EnvStr(
"",
category="attention",
doc="Comma-separated MiniMax-H3 transformer_blocks indices whose attention stays in BF16 while "
"FASTVIDEO_NVFP4_FA4 quantizes the rest, for example 45,49.")
FASTVIDEO_FA4_PV_MODE = EnvChoice(
"bf16",
choices=("bf16", "fp8"),
+13
View File
@@ -19,6 +19,7 @@ from fastvideo.attention.backends.abstract import AttentionMetadata, layer_idx_f
from fastvideo.attention.backends.video_sparse_attn_h3 import MiniMaxH3VSAMetadata
from fastvideo.attention.layer import DistributedAttention_VSA
from fastvideo.attention.selector import get_attn_backend
from fastvideo.attention.utils.nvfp4_qk_precondition import parse_block_indices
from fastvideo.configs.models.dits.minimax_h3 import MiniMaxH3Config
from fastvideo.distributed.communication_op import (
sequence_model_parallel_all_gather_with_unpad,
@@ -207,6 +208,7 @@ class MiniMaxH3Attention(nn.Module):
fa4_packed_varlen: bool = False,
exact_rope: bool = False,
vsa_fp4: dict[str, Any] | None = None,
bf16_attention: bool = False,
) -> None:
super().__init__()
self.num_attention_heads = num_attention_heads
@@ -254,6 +256,8 @@ class MiniMaxH3Attention(nn.Module):
supported_attention_backends=supported_attention_backends)
use_vsa = resolved_backend.get_name() == "VIDEO_SPARSE_ATTN_H3"
attention_cls = DistributedAttention_VSA if use_vsa else DistributedAttention
# Blocks listed in FASTVIDEO_NVFP4_FA4_BF16_BLOCKS opt out of the process-wide NVFP4 FA4 Q/K quantization.
precision_args: dict[str, Any] = {"nvfp4_fa4": False} if bf16_attention else {}
self.distributed_attention = attention_cls(
num_heads=num_attention_heads,
head_size=attention_head_dim,
@@ -261,6 +265,7 @@ class MiniMaxH3Attention(nn.Module):
supported_attention_backends=supported_attention_backends,
prefix=prefix,
fa4_packed_varlen=fa4_packed_varlen,
**precision_args,
)
# Opt-in inference route: VSA-H3 selection on the block-sparse FP4
# kernel (see minimax_h3_vsa_fp4); grad and compile keep the generic path.
@@ -658,6 +663,7 @@ class MiniMaxH3TransformerBlock(nn.Module):
fa4_packed_varlen: bool = False,
exact_kernels: frozenset[str] = frozenset(),
vsa_fp4: dict[str, Any] | None = None,
bf16_attention: bool = False,
) -> None:
super().__init__()
self.norm1 = nn.RMSNorm(hidden_size, eps=norm_eps)
@@ -673,6 +679,7 @@ class MiniMaxH3TransformerBlock(nn.Module):
fa4_packed_varlen=fa4_packed_varlen,
exact_rope="rope" in exact_kernels,
vsa_fp4=vsa_fp4,
bf16_attention=bf16_attention,
)
self.norm2 = nn.RMSNorm(hidden_size, eps=norm_eps)
self.ff = MiniMaxH3FeedForward(
@@ -916,6 +923,11 @@ class MiniMaxH3Transformer3DModel(BaseDiT):
config.quant_config,
prefix=f"{config.prefix}.token_refiner",
)
bf16_blocks = parse_block_indices(envs.FASTVIDEO_NVFP4_FA4_BF16_BLOCKS.get())
unknown_blocks = sorted(index for index in bf16_blocks if index >= arch.num_layers)
if unknown_blocks:
raise ValueError(f"FASTVIDEO_NVFP4_FA4_BF16_BLOCKS {unknown_blocks} exceed the {arch.num_layers} "
"transformer_blocks")
self.transformer_blocks = nn.ModuleList([
MiniMaxH3TransformerBlock(
arch.hidden_size,
@@ -935,6 +947,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,
bf16_attention=index in bf16_blocks,
) for index in range(arch.num_layers)
])
self.norm_out = MiniMaxH3AdaLayerNormOut(
@@ -0,0 +1,129 @@
# SPDX-License-Identifier: Apache-2.0
"""Deployed NVFP4 FA4 Q/K preconditioning and per-block BF16 opt-out.
The transform must be applied to Q/K right before NVFP4 quantization with the same function the QAD emulator uses,
and MiniMax-H3 blocks listed in ``FASTVIDEO_NVFP4_FA4_BF16_BLOCKS`` must build BF16 attention impls.
"""
from unittest.mock import MagicMock, patch
import pytest
torch = pytest.importorskip("torch")
import fastvideo.envs as envs # noqa: E402
from fastvideo.attention.utils.nvfp4_qk_precondition import ( # noqa: E402
parse_block_indices, parse_qk_transform, precondition_qk,
)
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 _impl(**extra) -> "FlashAttentionImpl":
return FlashAttentionImpl(num_heads=2, head_size=64, causal=False, softmax_scale=0.125, num_kv_heads=2, **extra)
@pytest.fixture
def fa4_runtime_available():
with (
patch.object(flash_attn_module, "_FA4_FP4_AVAILABLE", True),
patch("torch.cuda.get_device_capability", return_value=(10, 0)),
):
yield
def test_block_list_parsing() -> None:
assert parse_block_indices("") == frozenset()
assert parse_block_indices(None) == frozenset()
assert parse_block_indices(" 45, 49,45 ") == frozenset({45, 49})
for bad in ("a,1", "-1", "4.5"):
with pytest.raises(ValueError):
parse_block_indices(bad)
def test_transform_env_default_and_validation(env_overrides) -> None:
env_overrides.enter_context(envs.FASTVIDEO_NVFP4_FA4_QK_TRANSFORM.override(None))
assert envs.FASTVIDEO_NVFP4_FA4_QK_TRANSFORM.get() == "none"
assert _impl().nvfp4_smooth_k is False and _impl().nvfp4_hadamard is False
env_overrides.enter_context(envs.FASTVIDEO_NVFP4_FA4_QK_TRANSFORM.override("Smooth_K+Hadamard"))
impl = _impl()
assert (impl.nvfp4_smooth_k, impl.nvfp4_hadamard) == (True, True)
env_overrides.enter_context(envs.FASTVIDEO_NVFP4_FA4_QK_TRANSFORM.override("bogus"))
with pytest.raises(envs.EnvVarError):
envs.FASTVIDEO_NVFP4_FA4_QK_TRANSFORM.get()
def test_impl_arg_wins_over_transform_env(env_overrides) -> None:
env_overrides.enter_context(envs.FASTVIDEO_NVFP4_FA4_QK_TRANSFORM.override("hadamard"))
impl = _impl(nvfp4_qk_transform="smooth_k")
assert (impl.nvfp4_smooth_k, impl.nvfp4_hadamard) == (True, False)
with pytest.raises(ValueError):
_impl(nvfp4_qk_transform="bogus")
@pytest.mark.parametrize("name", ["none", "smooth_k", "hadamard", "smooth_k+hadamard"])
def test_transform_applied_before_quantization(name, env_overrides, fa4_runtime_available) -> None:
env_overrides.enter_context(envs.FASTVIDEO_NVFP4_FA4.override(None))
impl = _impl(nvfp4_fa4=True, nvfp4_qk_transform=name)
torch.manual_seed(0)
q = torch.randn(1, 20, 2, 64, dtype=torch.bfloat16) + 3.0
k = torch.randn(1, 20, 2, 64, dtype=torch.bfloat16) + 3.0
v = torch.randn(1, 20, 2, 64, dtype=torch.bfloat16)
seen = []
def fake_quant(tensor):
seen.append(tensor.clone())
return tensor, tensor
fp4 = MagicMock(side_effect=lambda *a, **kw: a[2])
with (
patch.object(flash_attn_module, "_nvfp4_quantize_for_fa4", side_effect=fake_quant),
patch.object(flash_attn_module, "flash_attn_fp4_func", fp4),
):
impl._forward_nvfp4(q, k, v)
smooth_k, hadamard = parse_qk_transform(name)
expected_q, expected_k = precondition_qk(q, k, smooth_k=smooth_k, hadamard=hadamard)
assert torch.equal(seen[0], expected_q) and torch.equal(seen[1], expected_k)
assert (name == "none") == (torch.equal(seen[0], q) and torch.equal(seen[1], k))
def test_smooth_k_removes_mean_and_preserves_attention() -> None:
torch.manual_seed(1)
q, k, v = (torch.randn(1, 32, 2, 64) for _ in range(3))
k = k + 5.0
qt, kt = precondition_qk(q, k, smooth_k=True, hadamard=True)
ref = torch.nn.functional.scaled_dot_product_attention(*(t.transpose(1, 2) for t in (q, k, v)))
out = torch.nn.functional.scaled_dot_product_attention(*(t.transpose(1, 2) for t in (qt, kt, v)))
torch.testing.assert_close(out, ref, atol=1e-4, rtol=1e-4)
def _build_h3_attention(bf16_attention: bool):
h3 = pytest.importorskip("fastvideo.models.dits.minimax_h3")
backend = MagicMock()
backend.get_name.return_value = "FLASH_ATTN"
distributed = MagicMock()
with (
patch.object(h3, "get_attn_backend", return_value=backend),
patch.object(h3, "get_compute_dtype", return_value=torch.bfloat16),
patch.object(h3, "ReplicatedLinear", side_effect=lambda *a, **kw: torch.nn.Identity()),
patch.object(h3, "DistributedAttention", distributed),
):
h3.MiniMaxH3Attention(64, 2, 32, 1e-6, (), None, prefix="transformer_blocks.3.attn",
bf16_attention=bf16_attention)
return distributed.call_args.kwargs
def test_h3_attention_bf16_opt_out_wiring() -> None:
assert _build_h3_attention(True)["nvfp4_fa4"] is False
assert "nvfp4_fa4" not in _build_h3_attention(False)
def test_bf16_block_impl_ignores_env_opt_in(env_overrides, fa4_runtime_available) -> None:
env_overrides.enter_context(envs.FASTVIDEO_NVFP4_FA4.override(True))
assert _impl(nvfp4_fa4=_build_h3_attention(True)["nvfp4_fa4"]).nvfp4_fa4 is False
assert _impl(**{k: v for k, v in _build_h3_attention(False).items() if k == "nvfp4_fa4"}).nvfp4_fa4 is True # env wins
@@ -1,11 +1,13 @@
# SPDX-License-Identifier: Apache-2.0
"""Parity of the dense FP4 attention emulator (QAD training) with FlashAttention-4 NVFP4 (deployment).
Both should compute Q/K in NVFP4 (per-16 E4M3 block scales, unit global scale) with P and V in BF16. Reports the
relative L2 distance of the emulator to the FA4 NVFP4 kernel next to the FP4-vs-BF16 distance: the emulator is a
With ``--qk-transform`` both sides precondition Q/K first (the emulator through ``qk_transform=``, the deployed
``FlashAttentionImpl._forward_nvfp4`` through the ``nvfp4_qk_transform`` impl arg). Both should compute Q/K in NVFP4
(per-16 E4M3 block scales, unit global scale) with P and V in BF16. Reports the relative L2 distance of the emulator to the FA4 NVFP4 kernel next to the FP4-vs-BF16 distance: the emulator is a
faithful training target when the first is a small fraction of the second. Needs a Blackwell GPU with FA4 FP4.
python scripts/distill/pika_h3_flex_qad/check_fp4_attention_parity.py --seq-len 8269 --heads 14
python scripts/distill/pika_h3_flex_qad/check_fp4_attention_parity.py --seq-len 8269 --heads 14 \\
--qk-transform smooth_k+hadamard
"""
from __future__ import annotations
@@ -23,28 +25,33 @@ def main() -> None:
parser.add_argument("--seq-len", type=int, default=8269, help="not a multiple of 128 on purpose")
parser.add_argument("--heads", type=int, default=14)
parser.add_argument("--dim", type=int, default=128)
parser.add_argument("--qk-transform", default="none", help="none, smooth_k, hadamard or smooth_k+hadamard")
parser.add_argument("--k-bias", type=float, default=4.0, help="per-channel K offset that smooth_k removes")
parser.add_argument("--outlier-scale", type=float, default=8.0, help="scale of a few outlier K channels")
args = parser.parse_args()
from fastvideo.attention.backends import flash_attn as fa
from fastvideo.attention.backends.fp4_dense_qat import FP4DenseAttention
from fastvideo.attention.backends.fp4_vsa_qat import FP4AttentionNumerics
from fastvideo.attention.utils.nvfp4_qk_precondition import parse_qk_transform
torch.manual_seed(0)
shape = (1, args.seq_len, args.heads, args.dim)
q, k, v = (torch.randn(shape, device="cuda", dtype=torch.bfloat16) for _ in range(3))
k[..., :4] *= args.outlier_scale
k += args.k_bias * torch.randn(1, 1, args.heads, args.dim, device="cuda", dtype=torch.bfloat16)
transform = parse_qk_transform(args.qk_transform)
scale = args.dim**-0.5
q_fp4, q_sf = fa._nvfp4_quantize_for_fa4(q)
k_fp4, k_sf = fa._nvfp4_quantize_for_fa4(k)
fa4_fp4 = fa.flash_attn_fp4_func(q_fp4[:, :args.seq_len], k_fp4[:, :args.seq_len], v, q_sf, k_sf,
softmax_scale=scale, causal=False)
fa4_fp4 = fa4_fp4[0] if isinstance(fa4_fp4, tuple) else fa4_fp4
impl = fa.FlashAttentionImpl(num_heads=args.heads, head_size=args.dim, causal=False, softmax_scale=scale,
num_kv_heads=args.heads, nvfp4_fa4=True, nvfp4_qk_transform=args.qk_transform)
fa4_fp4 = impl._forward_nvfp4(q, k, v)
bf16 = torch.nn.functional.scaled_dot_product_attention(*(t.transpose(1, 2) for t in (q, k, v)),
scale=scale).transpose(1, 2)
emulator = FP4DenseAttention(FP4AttentionNumerics(quantize=True, quantize_pv=False))(q, k, v, None)
numerics = FP4AttentionNumerics(quantize=True, quantize_pv=False)
emulator = FP4DenseAttention(numerics, qk_transform=transform)(q, k, v, None)
print(f"qk_transform={args.qk_transform} k_bias={args.k_bias} outlier_scale={args.outlier_scale}")
fp4_vs_bf16 = rel(fa4_fp4, bf16)
emu_vs_fa4 = rel(emulator, fa4_fp4)
print(f"FA4 NVFP4 vs BF16 attention: rel L2 {fp4_vs_bf16:.5f}")