v1.4 - FlashAttention Fork: 2.9.0 → 2.9.1 Release Notes
Package: flash_attn (this fork)
Upstream baseline: FlashAttention-2 feature line at version 2.9.0
This release: 2.9.1 (patch release — same features, corrected packaging metadata)
This document describes the meaning, files changed, full modified source, and behavioral impact of the 2.9.0 → 2.9.1 correction on this fork. It covers the A-1 (rescale-threshold skip) and A-2 (packed fma.rn.f32x2 in scale_apply_exp2) refinements that justify the 2.9.0 feature label, plus the version bump to 2.9.1.
Table of contents
- Why 2.9.1 exists
- Modified files
- Package version (
flash_attn/__init__.py) - CUDA:
csrc/flash_attn/src/softmax.h - CUDA:
csrc/flash_attn/src/utils.h - Tooling:
bench/check_sass_gates.py - Call sites (unchanged, for context)
- Build, install, and verification
- References
1. Why 2.9.1 exists
1.1 Fork versioning vs upstream FlashAttention
| Layer | Meaning |
|---|---|
| Upstream FA2 | Independent project (e.g. 2.8.x on PyPI upstream). |
| This fork’s feature line | Tracks 2.9.0 when A-1 (rescale-threshold skip in forward softmax) and A-2 (packed FMA in scale_apply_exp2 on sm_100+) are present in the built kernels. |
| 2.9.1 | Patch release on that line: no new features, only a version string bump so wheels, pip show, and import flash_attn report 2.9.1 after a rebuild. |
The kernel code for A-1/A-2 was already integrated before this bump. 2.9.1 does not change numerical behavior relative to the tree at commit time; it changes __version__ only (unless you rebuild wheels, in which case the binary reflects whatever is currently compiled).
1.2 What A-1 and A-2 do (summary)
| Plan | Name | Purpose |
|---|---|---|
| A-1 | Rescale-threshold skip | When updating online softmax across K blocks, if the change in row max is tiny (scaled_diff >= -0.01 in log₂ scale), skip rescaling O and row_sum — error bounded by ~0.7% on normalized output. |
| A-2 | Packed FMA in scale_apply_exp2 |
On sm_100+ (Hopper / Blackwell), compute two exp2(scale*t - max_scaled) lanes per step using one fma.rn.f32x2 instead of scalar fmaf + exp2f. |
Refinement tasks (R1–R6, A1-R1–R3) are documented in AI/A1_A2_REFINEMENTS_PLAN.md and md/FA2_CHANGES_v1.2.md.
1.3 What 2.9.1 changes vs 2.9.0 (this release)
| Item | Changed? |
|---|---|
flash_attn/__init__.py __version__ |
Yes → "2.9.1" |
setup.py / wheel build scripts |
No (user constraint: do not edit build program without explicit order) |
csrc/flash_attn/src/softmax.h |
Already contains A-1/A-2 refinements |
csrc/flash_attn/src/utils.h |
Already contains A-2 fma_f32x2 + UNFUSE_FMA message |
bench/check_sass_gates.py |
New validation script (A-2-R5) |
2. Modified files
| File | Role in 2.9.1 |
|---|---|
flash_attn/__init__.py |
Authoritative package version read by setup.py via get_package_version() |
csrc/flash_attn/src/softmax.h |
A-1 threshold constant, compile-time guards, scale_apply_exp2 packed path, comments |
csrc/flash_attn/src/utils.h |
fma_f32x2 helper + UNFUSE_FMA compile-time warning |
bench/check_sass_gates.py |
SASS gate: FFMA.X2 inside scale_apply_exp2 on sm_120 builds |
Not modified for this release: setup.py, WindowsWhlBuilder_cuda.bat, CUDA arch lists, TORCH_CUDA_ARCH_LIST, or other build configuration.
3. Package version (flash_attn/__init__.py)
3.1 Meaning
setup.py does not hardcode the version. It reads __version__ from this file:
def get_package_version():
with open(Path(this_dir) / "flash_attn" / "__init__.py", "r") as f:
version_match = re.search(r"^__version__\s*=\s*(.*)$", f.read(), re.MULTILINE)
public_version = ast.literal_eval(version_match.group(1))
...
return str(public_version)So pip install and import flash_attn; flash_attn.__version__ report 2.9.1 after install. Optional env FLASH_ATTN_LOCAL_VERSION appends a local suffix (e.g. 2.9.1+dev).
Wheel filenames may still contain 2.9.0 until you run bdist_wheel again; the installed metadata uses __init__.py.
3.2 Full file (modified lines only)
__copyright__ = "Copyright (c) 2023, Tri Dao"
__version__ = "2.9.1"
from flash_attn.flash_attn_interface import (
flash_attn_func,
flash_attn_kvpacked_func,
flash_attn_qkvpacked_func,
flash_attn_varlen_func,
flash_attn_varlen_kvpacked_func,
flash_attn_varlen_qkvpacked_func,
flash_attn_with_kvcache,
)4. CUDA: csrc/flash_attn/src/softmax.h
This file is the single source of truth for FA2 forward softmax on this fork. Below: full text of every function/region touched by A-1/A-2 refinements (not the entire 470-line file).
4.1 scale_apply_exp2 (A-2 core + comment A-2-R6)
Purpose: After computing per-row max in log₂ space, apply exp2(scale * t - max_scaled) to score tiles before the final softmax normalization. On sm_100+, pairs of columns use fma.rn.f32x2 via fma_f32x2, then scalar exp2f.
// Apply the exp to all the elements.
// Note: this function is shared by:
// - fwd via Softmax::softmax_rescale_o (both Is_first branches), and
// - bwd via flash_bwd_kernel.h::compute_dq_dk_dv (Scale_max=false branch).
// Both paths benefit equally from the sm_100+ packed-FMA inner loop below.
template <bool Scale_max=true, typename Engine0, typename Layout0, typename Engine1, typename Layout1>
__forceinline__ __device__ void scale_apply_exp2(Tensor<Engine0, Layout0> &tensor, Tensor<Engine1, Layout1> const &max, const float scale) {
static_assert(Layout0::rank == 2, "Only support 2D Tensor");
static_assert(Layout1::rank == 1, "Only support 1D Tensor");
CUTE_STATIC_ASSERT_V(size<0>(max) == size<0>(tensor));
#pragma unroll
for (int mi = 0; mi < size<0>(tensor); ++mi) {
// If max is -inf, then all elements must have been -inf (possibly due to masking).
// We don't want (-inf - (-inf)) since that would give NaN.
// If we don't have float around M_LOG2E the multiplication is done in fp64.
const float max_scaled = max(mi) == -INFINITY ? 0.f : max(mi) * (Scale_max ? scale : float(M_LOG2E));
#if __CUDA_ARCH__ >= 1000 && !defined(UNFUSE_FMA)
// Plan A-2: on sm_100+ (Blackwell), pair-process columns via fma.rn.f32x2.
// The pre-exp2f term is fma(t, scale, -max_scaled); fma.rn.f32x2 does two
// such FMAs in one instruction. exp2f itself stays scalar (MUFU.EX2 has
// no f32x2 form). Rounding mode .rn matches scalar fmaf default.
const float neg_max_scaled = -max_scaled;
constexpr int N1 = decltype(size<1>(tensor))::value;
static_assert(N1 % 2 == 0,
"scale_apply_exp2 packed-FMA path assumes N1 is even; "
"if a new MMA atom produces odd N1, restore the scalar tail loop.");
#pragma unroll
for (int ni = 0; ni < N1; ni += 2) {
float t0 = tensor(mi, ni);
float t1 = tensor(mi, ni + 1);
float r0, r1;
fma_f32x2(r0, r1, t0, t1, scale, scale, neg_max_scaled, neg_max_scaled);
tensor(mi, ni) = exp2f(r0);
tensor(mi, ni + 1) = exp2f(r1);
}
#else
#pragma unroll
for (int ni = 0; ni < size<1>(tensor); ++ni) {
// Instead of computing exp(x - max), we compute exp2(x * log_2(e) -
// max * log_2(e)) This allows the compiler to use the ffma
// instruction instead of fadd and fmul separately.
// The following macro will disable the use of fma.
// See: https://github.com/pytorch/pytorch/issues/121558 for more details
// This macro is set in PyTorch and not FlashAttention
#ifdef UNFUSE_FMA
tensor(mi, ni) = exp2f(__fmul_rn(tensor(mi, ni), scale) - max_scaled);
#else
tensor(mi, ni) = exp2f(tensor(mi, ni) * scale - max_scaled);
#endif
}
#endif
}
}Line-by-line (sm_100+ path):
| Lines | Meaning |
|---|---|
| 67–69 | Documents that forward (softmax_rescale_o) and backward (flash_bwd_kernel.h, Scale_max=false) both call this helper. |
| 81–99 | __CUDA_ARCH__ >= 1000 && !UNFUSE_FMA: use fma_f32x2 + paired exp2f; else scalar loop with optional UNFUSE_FMA PyTorch workaround. |
| 88–90 | static_assert(N1 % 2 == 0) — odd N1 is a compile error, not a silent scalar tail (A-2-R2). |
| 91–98 | Loop ni += 2: two FMAs + two exp2f per iteration. |
| 100–114 | Pre-sm_100 fallback: same math, scalar fmaf / exp2f; UNFUSE_FMA forces unfused path. |
fma_f32x2 was removed from this file and lives in utils.h (A-2-R3).
4.2 Threshold constant and softmax_rescale_o (A-1)
// Threshold below which softmax_rescale_o skips the O / row_sum rescale.
// scores_scale = exp2(scaled_diff); scaled_diff is the negative excursion of
// row_max from one iteration to the next, in units of softmax_scale_log2.
// At -0.01f, scores_scale >= ~0.993 (worst-case relative error <= 0.7%).
inline constexpr float kSoftmaxRescaleSkipThreshold = -0.01f; template<bool Is_first, bool Check_inf=false, bool Use_rescale_threshold=false, typename Tensor0, typename Tensor1>
__forceinline__ __device__ void softmax_rescale_o(Tensor0 &acc_s, Tensor1 &acc_o, float softmax_scale_log2) {
static_assert(!(Is_first && Use_rescale_threshold),
"Use_rescale_threshold has no effect when Is_first=true; "
"remove the flag from the Is_first=true call site.");
// ... reshape scores, acc_o_rowcol ...
} else {
// ...
#pragma unroll
for (int mi = 0; mi < size(row_max); ++mi) {
float scores_max_cur = !Check_inf
? row_max(mi)
: (row_max(mi) == -INFINITY ? 0.0f : row_max(mi));
float scaled_diff = (scores_max_prev(mi) - scores_max_cur) * softmax_scale_log2;
// Optionally skip the O / row_sum rescale when the new row_max is virtually
// the same as the previous one (scaled_diff is a very small negative number,
// so scores_scale = exp2(scaled_diff) ~= 1.0).
// This skip does not compound across iterations: normalize_softmax_lse
// divides acc_o by row_sum, so the skipped factor cancels between
// numerator and denominator, bounding the relative error by the threshold.
if constexpr (Use_rescale_threshold) {
if (scaled_diff >= kSoftmaxRescaleSkipThreshold) { continue; }
}
float scores_scale = exp2f(scaled_diff);
row_sum(mi) *= scores_scale;
#pragma unroll
for (int ni = 0; ni < size<1>(acc_o_rowcol); ++ni) { acc_o_rowcol(mi, ni) *= scores_scale; }
}
FLASH_NAMESPACE::scale_apply_exp2(scores, row_max, softmax_scale_log2);
// ...
}A-1 behavior:
Use_rescale_threshold=trueonly onIs_first=falsecall sites (see §7).- When
scaled_diff >= -0.01f, skip updatingrow_sumandacc_ofor that row — saves work when max barely moves. normalize_softmax_lsestill divides byrow_sum, so skipped mass cancels in numerator/denominator (no drift across blocks).
5. CUDA: csrc/flash_attn/src/utils.h
5.1 fma_f32x2 (full)
// Packed FMA helper (Plan A-2): computes (d0,d1) = (a0*b0+c0, a1*b1+c1).
// On sm_100+ (Blackwell), emits a single fma.rn.f32x2 instruction.
// Falls back to two plain FMAs on older arches and when UNFUSE_FMA is set.
__forceinline__ __device__ void fma_f32x2(
float &d0, float &d1,
float a0, float a1,
float b0, float b1,
float c0, float c1) {
#if __CUDA_ARCH__ >= 1000 && !defined(UNFUSE_FMA)
asm volatile(
"{\n\t"
".reg .b64 ra, rb, rc, rd;\n\t"
"mov.b64 ra, {%2, %3};\n\t"
"mov.b64 rb, {%4, %5};\n\t"
"mov.b64 rc, {%6, %7};\n\t"
"fma.rn.f32x2 rd, ra, rb, rc;\n\t"
"mov.b64 {%0, %1}, rd;\n\t"
"}\n"
: "=f"(d0), "=f"(d1)
: "f"(a0), "f"(a1), "f"(b0), "f"(b1), "f"(c0), "f"(c1));
#else
d0 = fmaf(a0, b0, c0);
d1 = fmaf(a1, b1, c1);
#endif
}PTX: single fma.rn.f32x2 with .rn (round-to-nearest-even), matching default fmaf rounding.
5.2 UNFUSE_FMA compile-time message (A-2-R4)
#if defined(UNFUSE_FMA) && defined(__CUDA_ARCH__) && (__CUDA_ARCH__ >= 1000)
#pragma message("UNFUSE_FMA is defined while compiling for sm_100+; " \
"the Blackwell-only fma.rn.f32x2 path in scale_apply_exp2 is disabled. " \
"This is the documented ablation path (see AI/FA2_BACKPORT_FROM_FA4_PLAN.md " \
"section 8.2 ablation row 'A-2 off' and section 10.2 rollback). " \
"Remove UNFUSE_FMA to re-enable the packed FMA path.")
#endifMeaning: If you define UNFUSE_FMA (PyTorch workaround for issue #121558), you silently lose A-2 on sm_100+ unless you remove the macro. The message makes that visible at compile time.
6. Tooling: bench/check_sass_gates.py
Purpose: Enforce the Phase-1 exit criterion from AI/FA2_BACKPORT_FROM_FA4_PLAN.md §6: on a sm_120 build, FFMA.X2 must appear inside demangled scale_apply_exp2* symbols.
Full script:
#!/usr/bin/env python3
"""SASS hard-gate checker for Flash-Attention A-1/A-2 optimizations.
Parses ``cuobjdump --dump-sass`` output to verify the presence or absence
of specific SASS instructions within ``scale_apply_exp2`` symbols.
Exit codes:
0 gate passes
1 gate fails
2 usage / environment error
"""
# ... (see repository file for complete implementation: argparse, wheel/pyd extraction,
# cuobjdump auto-detect, streaming parser, gates a2 and b1)Gate a2: symbol matches scale_apply_exp2, instruction FFMA.X2, must be present.
Usage example:
python bench/check_sass_gates.py \
--whl dist/flash_attn-2.9.0+cu132torch2.12.0-cp312-cp312-win_amd64.whl \
--arch sm_120 \
--gate a2Manual procedure is also documented in md/2.9.0_COMPLETE_TEST_AND_VALIDATION_GUIDE.md §11.
7. Call sites (unchanged, for context)
A-1’s Use_rescale_threshold flag is wired only at these four template instantiations in flash_fwd_kernel.h:
| Line (approx.) | Is_first |
Use_rescale_threshold |
|---|---|---|
| ~344 | false |
true |
| ~407 | false |
true |
| ~918 | false |
true |
| ~985 | false |
true |
Is_first=true paths use default Use_rescale_threshold=false (correct: no prior max to compare on first tile).
Backward uses scale_apply_exp2<Scale_max=false> in flash_bwd_kernel.h — benefits from A-2 on sm_100+ without a separate template flag.
8. Build, install, and verification
8.1 Version bump only (no rebuild)
If you only change __init__.py to 2.9.1 without rebuilding the extension:
import flash_attn→__version__is2.9.1- The
.pyd/.soon disk is still the old binary (still 2.9.0 feature code if that is what was built) - Wheel filename may still say
flash_attn-2.9.0+...until the nextbdist_wheel
8.2 Rebuild wheel (recommended for consistent 2.9.1 artifacts)
Use your existing Windows workflow (WindowsWhlBuilder_cuda.bat or equivalent). Do not ask the assistant to edit setup.py unless you explicitly want arch or dependency changes.
After build:
pip install --force-reinstall dist/flash_attn-2.9.1+...whl(or your naming convention)python -c "import flash_attn; print(flash_attn.__version__)"→2.9.1python tests/test_a2_smoke.py(if GPU available)python bench/check_sass_gates.py --whl <wheel> --arch sm_120 --gate a2
8.3 What this release does not include
- Thor sm_101 / sm_110 in default
TORCH_CUDA_ARCH_LIST(separate user/setup decision; see commit65cc9dbhistory on this fork) - Changes to
md/CHANGELOG.mdormd/FA2_CHANGES_v1.2.md(reverted when user requested no unsolicited doc edits) - FA3 (
hopper/) or FA4 (flash_attn/cute/) trees — out of scope for this FA2 fork doc
9. References
| Document | Path |
|---|---|
| A-1/A-2 refinement plan | AI/A1_A2_REFINEMENTS_PLAN.md |
| FA2 backport plan (A-1/A-2 source) | AI/FA2_BACKPORT_FROM_FA4_PLAN.md |
| Fork change log | md/FA2_CHANGES_v1.2.md |
| Test & validation guide | md/2.9.0_COMPLETE_TEST_AND_VALIDATION_GUIDE.md |
| SASS gate procedure | md/2.9.0_COMPLETE_TEST_AND_VALIDATION_GUIDE.md §11 |
End of document. Package version for this feature line: 2.9.1 (flash_attn/__init__.py).