FP8 / FP4 Attention¶
FFPA's CUDA backend can run the attention computation in FP8 (e4m3) or NVFP4 (e2m1) on the forward pass: activations stay fp16/bf16 in global memory, and Q/K/V are quantized inside the kernel (smooth → scale → low-precision MMA), with exact scale folding so that the only error source is the quantization rounding itself.
from functools import partial
from ffpa_attn import CUDABackend, ffpa_attn_func
fp8_backend = CUDABackend(forward=True, backward=False, enable_fp8=True)
fp4_backend = CUDABackend(forward=True, backward=False, enable_fp4=True)
o_fp8 = ffpa_attn_func(q, k, v, forward_backend=fp8_backend)
o_fp4 = ffpa_attn_func(q, k, v, forward_backend=fp4_backend)
enable_fp8 and enable_fp4 are mutually exclusive, and FP8/FP4 attention
is forward only. Use the Triton or
CuTe-DSL backend when you need backward.
Support at a glance¶
| FP8 | NVFP4 | |
|---|---|---|
| GPUs | sm_120{a,f} (TMA family); sm_89 (Ada, cp.async family) | sm_120{a,f} only |
| Input dtypes | fp16 / bf16 (quantized in-kernel) | fp16 / bf16 |
| Headdim (default build) | 64 ~ 224 (persist-D) |
{64, 128, 192, 256} (persist-D) |
| Headdim (full build) | 64 ~ 1024 |
64 ~ 512 |
Any D % 8 == 0 |
✔ via 32-aligned pad | ✔ via 64-aligned pad |
| causal / GQA / MQA / cross | ✔ | ✔ |
attn_mask |
✔ | ✔ |
| dropout | ✖ | ✖ |
| NHD / strided-NHD zero-copy | ✔ | ✔ |
| hybrid (fp16 early rows) | ✔ | ✔ |
| hadamard (WHT Q/K rotation) | ✔ | ✔ |
| smooth-K / smooth-V | ✔ / ✔ | forced / optional |
Note
Build-time coverage. --ext all compiles only the persist-D quantized
windows (fp8 D<=224, fp4 64<=D<=256) — the split-D / M4N2 quantized
families are opt-in via ENABLE_FFPA_QUANT_SPLIT_D=1. Calls past the
compiled window raise a rebuild with ENABLE_FFPA_QUANT_SPLIT_D=1 error
instead of silently dispatching elsewhere.
FP8 knobs¶
| Knob | Default | Meaning |
|---|---|---|
fp8_smooth_k |
True |
Subtract the per-(b,h) K sequence mean before quantization (softmax shift-invariance; only lse is corrected). |
fp8_smooth_v |
False |
Subtract the per-(b,h) V channel mean (fp8_v_quant_method="per_channel" required); the epilogue adds it back. |
fp8_q_quant_method / fp8_k_quant_method |
"per_block" |
"per_block" (128-row tile) or "per_thread" (MMA-fragment aligned). Must match. |
fp8_v_quant_method |
"per_channel" |
"per_channel" (SageAttention-style, recommended for outlier-heavy V) or "per_block". |
fp8_pv_acc_type |
"f32" |
PV accumulator: "f32" or "f16" (faster, minor precision loss; the sm_89 family is f16-only). |
fp8_qk_mm_type |
"fp8" |
QK^T MMA: "fp8" (e4m3) or "int8" (s8xs8→s32). int8 tends to win at large N. |
fp8_hadamard |
False |
Rotate Q/K by an orthogonal Walsh-Hadamard matrix before quantization (outlier spreading; exact in fp32 math). |
fp8_hybrid |
auto | None = auto-on for is_causal=True: leading fp8_hybrid_n_early rows (multiple of 128, default 256) are computed in fp16, the rest in fp8. |
NVFP4 specifics¶
- NVFP4 = e2m1 data + ue4m3 scale factors per 16-element microscaling block; the blockscale MMA consumes the scales directly.
- Q/K smoothing is mandatory (not optional) — e2m1's ±6 dynamic range
requires the mean-removal +
delta_srank-1 correction to stay accurate. fp4_pv_mm_type="fp8"selects the MXFP8 PV variant (QK stays NVFP4); MXFP8 covers persist-DD<=192and split-D, and is rejected on the M4N2 family (its atom K=128 exceeds the M4N2 tile's kBc=64).fp4_smooth_vmirrorsfp8_smooth_vfor the V side.fp4_hadamardrotates Q/K (pow2 D rotates fused inside the quantize kernel, other D falls back to a standalone WHT kernel).
Precision notes¶
Low-precision attention trades a bounded amount of accuracy for throughput. Two effects dominate and are worth knowing before you tune:
- Causal early rows. The first rows of a causal sequence attend to one or two keys, so their outputs have effective sample size ≈ 1 and roughly 3σ amplitude. A ~5% (e4m3) relative error therefore shows up as a large absolute error on those rows — the relative error itself is the same as in dense rows. Use the hybrid mode (default auto for causal) or per-channel V scales to protect them.
- P is online-quantized. Q/K/V can be calibrated offline (smoothing,
per-block/per-channel scales), but the softmax probabilities
Pare produced inside the kernel and can only use fixed/conservative scales. FP8 uses a fixed1/448full-scale emission; NVFP4 uses the two-level1/(448·6)scheme from SageAttention-3.
For FP4 the error structure is one order of magnitude larger (e2m1 has a
~12.5% relative step): on randn(σ=1) inputs, causal spike rows show
max_abs ≈ 0.5-0.75 while the mean_abs stays around 0.014-0.03.
Measure cosine similarity / mean_abs (the SageAttention-3 evaluation
convention) rather than peak max_abs when judging FP4 quality.
Hybrid: early rows in fp16¶
backend = CUDABackend(
forward=True, backward=False,
enable_fp4=True,
fp4_hybrid=True, # None = auto (causal + enable_fp4)
fp4_hybrid_n_early=256, # multiple of 128
)
Hybrid runs the first n_early query rows through the fp16 kernel and the
remaining rows through fp8/fp4, with a q_start_row offset — zero
redundancy, mathematically row-exact, and compatible with all layouts. It
is the recommended setting for causal workloads.
Benchmarking against SageAttention¶
Install SageAttention-2/3
first, then build FFPA with the quantized kernels
(bash ./build.sh --arch sm_120f --ext all --headdim 128,192,256) and run:
python bench/bench_fp8.py --N 16384,32768 # FFPA-FP8 vs SageAttention-2 vs SDPA
python bench/bench_fp4.py --N 16384,32768 # FFPA-FP4 vs SageAttention-3 vs SDPA
bench_fp8.py --preset ships configurations matching common deployments:
| Preset | Configuration |
|---|---|
default |
int8 QK + f16 PV acc, per_thread Q/K + per_channel V + smooth-K |
cache-dit |
same as default with hybrid n_early=256 — matches cache-dit's ffpa_fp8 backend |
perblock |
int8 QK + f16 PV acc, per_block Q/K/V + smooth-K (the sm_89 cp.async scope) |
FFPA FP8 is comparable to or slightly faster than SageAttention-2 at D=128, and FFPA FP4 is significantly faster than SageAttention-3 at D=128 on NVIDIA RTX PRO 5000/6000/5090. Reproduce the plots with the commands in Bench.
Debug / A-B switches¶
These exist only in debug builds (--debug-ext fp8 / fp4), see
Env Vars:
| Env | Effect |
|---|---|
FFPA_FP8_FORCE_KERNEL=split_d\|m4n2 |
Force one fp8 split-D kernel family for A/B (224 < D <= 1024). |
FFPA_FP8_PQUANT_PER_ROW=1 |
Per-row P quantization (full-scale; incompatible with lazy rescale). |
FFPA_FP4_PAD_TORCH=1 |
Materialize the fp4 head-dim pad with torch instead of the fused path. |
FFPA_BIAS_TILE_KEEP=1 |
Restore the fp4 M4N2 bias tile modes (pinned to the gmem-direct mode by default). |
End-to-end in Cache-DiT¶
FP8/FP4 attention is integrated into Cache-DiT
(--attn ffpa_fp8 / ffpa_fp4), covering self / cross / causal / attn-mask
/ GQA-MQA with zero-copy NHD layouts:
python3 -m cache_dit.generate flux --attn ffpa_fp8 --seed 42 --height 1024 --width 1024
python3 -m cache_dit.generate flux --attn ffpa_fp4 --seed 42 --height 1024 --width 1024
FP8/FP4 attention is not a universal win (small models and short seqlens spend more time in the quantization pre-pass than they save in the MMA); evaluate precision and speed on your own workload.