Thread tanh logit softcapping through FlashAttention (FA2, opt-in FA3) - #3391
Thread tanh logit softcapping through FlashAttention (FA2, opt-in FA3)#3391nvegesna-netizen wants to merge 18 commits into
Conversation
Greptile SummaryThe PR adds configurable tanh attention-logit softcapping and threads it through FlashAttention 2, eligible FlashAttention 3 configurations, context-parallel execution, and the unfused fallback.
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[DotProductAttention request] --> B{softcap nonzero?}
B -- No --> C[Existing backend selection]
B -- Yes --> D{Supported configuration}
D -->|FA3 capable, non-CP, head dim <= 256| E[FlashAttention 3]
D -->|FA2 >= 2.6, supported dropout mode| F[FlashAttention 2]
D -->|Context parallel| F
D -->|No eligible flash backend| G[Unfused attention]
E --> H[Softcapped attention output]
F --> H
G --> H
Reviews (18): Last reviewed commit: "Merge branch 'main' into nvegesna/gemma2..." | Re-trigger Greptile |
a6a793b to
5917f0d
Compare
19a21eb to
5ae46ce
Compare
|
Please follow this link to fix the DCO of this PR as well. Thanks! https://github.com/NVIDIA/TransformerEngine/pull/3391/checks?check_run_id=97902991433 |
…in FA3) Add a user `softcap` value (tanh logit softcapping, `softcap*tanh(x/softcap)`) to DotProductAttention so models like Gemma2 can run on the fused flash path instead of an unfused/FlexAttention kernel. - Add `softcap` to DotProductAttention (init+forward) and AttentionParams; thread it into the FA2 non-CP kwargs and all three context-parallel autograd functions (forward + ctx-saved backward). softcap=0.0 reproduces prior behavior. - get_attention_backend: when softcap != 0, disable FusedAttention/unfused and steer to FA2 -- disable FA3/FA4, and disable FA2 < 2.6.0 -- so the cap is never silently dropped (FA2 < 2.6.0) or hit at runtime as NotImplementedError (FA3/FA4). Also disable FA3 under context parallelism (its CP path hard-rejects nonzero softcap) so CP+softcap steers to FA2, which supports it, instead of crashing. - FA3 softcap opt-in: NVTE_FA3_SOFTCAP=1, Hopper (sm90) hd<=256, non-CP only, gated on a fail-closed signature probe (fa3_supports_softcap). Forward threads softcap into fa_3_optional_forward_kwargs; the existing Hopper autograd function carries it into backward automatically. Default off; unchanged behavior steers to FA2. - ONNX export: fail loudly (assert) rather than silently drop softcap -- export unconditionally force-selects UnfusedDotProductAttention, which has no softcap support, so this previously exported models with softcapping silently omitted. - Tests: test_softcap.py (FA2 fwd/bwd parity vs pure-PyTorch reference), wired into qa/L0_pytorch_unittest/test.sh. FA4 softcap opt-in is deliberately NOT included here -- see follow-up PR. On Blackwell (SM100), FA4's dedicated head_dim=256 forward kernel has no score_mod support at all (kernel constructor asserts `score_mod is None`), so there is currently no FA4 kernel path this could opt into; adding the scaffolding now would just be inert code with nothing to exercise. Addresses review findings: CP+FA3 softcap selection crash, ONNX silent drop, and the missing CI wiring for test_softcap.py. Signed-off-by: Nitin Vegesna <nvegesna@nvidia.com> Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
python -O / PYTHONOPTIMIZE strips assert statements, which would silently reopen the ONNX export softcap-drop bug the previous commit fixed (ONNX mode would again force-select UnfusedDotProductAttention with softcap silently omitted, with no error). Switch to an explicit if/raise ValueError, which survives optimized execution. Signed-off-by: Nitin Vegesna <nvegesna@nvidia.com> Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
for more information, see https://pre-commit.ci (reapplied after a force-push rebase clobbered pre-commit.ci's original 19a21eb commit; same content, restored by hand) Signed-off-by: Nitin Vegesna <nvegesna@nvidia.com>
…fold test into test_attention.py UnfusedDotProductAttention now applies softcap * tanh(scores / softcap) to the already-scaled logits, matching how FlashAttention folds softmax_scale into its tanh argument, so it can serve as the softcap reference backend. Backend selection therefore no longer disqualifies unfused attention for softcap, and the ONNX-export guard is dropped since the export path force-selects unfused and torch.tanh is exportable. test_softcap.py is replaced by a model_configs_softcap dict and test_dpa_softcap in test_attention.py, which reuses test_dot_product_attention for backend sweeping. Signed-off-by: Nitin Vegesna <nvegesna@nvidia.com>
Drop the redundant NVTE_FA3_SOFTCAP opt-in. `use_flash_attention_3` already derives from NVTE_FLASH_ATTN_V3, so the existing flag governs the FA3 softcap path and NVTE_FLASH_ATTN_V3=0 disables it. Correctness stays established by the build-capability probe, head_dim <= 256, and the non-CP requirement. Signed-off-by: Nitin Vegesna <nvegesna@nvidia.com>
FA4 exposes no softcap kwarg and its head_dim=256 kernel asserts score_mod is None, so there is no kernel to route the cap through. The FA4 call path in backends.py passes no softcap, so an FA4 selection with a nonzero softcap silently dropped the cap instead of failing closed. NVTE_FLASH_ATTN_V4 defaults to enabled, so this was reachable on SM100+ with flash-attn v4 installed and no context parallelism. Signed-off-by: Nitin Vegesna <nvegesna@nvidia.com>
flash-attn rejects a nonzero softcap combined with nonzero dropout at dispatch: "Softcapping does not support dropout for now" in csrc/flash_attn/flash_api.cpp, present in mha_fwd and mha_varlen_fwd from v2.6.0 (the earliest version TE allows softcap on) onwards. Backend selection did not model this, so a softcap + attention-dropout config passed selection, routed to FA2, and crashed inside flash-attn. Dropout only reaches the kernel while training, since backends.py passes `self.attention_dropout if self.training else 0.0`, so the gate is on `attention_dropout != 0.0 and is_training` to avoid blocking valid inference configs. UnfusedDotProductAttention supports both softcap and dropout and stays available, so this steers rather than hard-fails. Signed-off-by: Nitin Vegesna <nvegesna@nvidia.com>
82a2bf4 to
5ecabac
Compare
test_dot_product_attention forced is_training=False whenever FusedAttention could not train a config, so that backends only available for inference could still be compared. softcap always disables FusedAttention, so test_dpa_softcap silently degraded to a forward-only comparison and the PR's backward-parity claim -- the FA2 softcap backward kernel included -- went untested. Add fwd_only_without_fused_attn (default True, so every other caller is byte-for-byte unchanged) and opt test_dpa_softcap out, which pairs FlashAttention against UnfusedDotProductAttention with is_training=True and restores the dgrad comparison. Signed-off-by: Nitin Vegesna <nvegesna@nvidia.com>
Two gaps remained after folding test_softcap.py into test_attention.py. softcap=0.0 no-op: every model_configs_softcap entry uses a nonzero cap, so nothing asserted the backward-compatibility claim. The half that the PR actually changed is backend selection, and a filter that fired at 0.0 would silently remove FusedAttention and FA4 from other tests rather than fail one. test_dpa_softcap_zero_backend_selection asserts FusedAttention survives softcap=0.0 and is disabled by a nonzero cap. Unfused coverage and tanh's nonlinear region: test_dpa_softcap needs two TE backends, so it skips entirely without flash-attn even though UnfusedDotProductAttention now implements softcap and is the reference for everything else. It also cannot detect a dropped cap at all: 0.1 * randn inputs put the logits at O(1e-2), where the reference output moves by 9e-9 at cap=50 and 2e-4 at cap=0.01. test_dpa_softcap_vs_reference compares forward and dQ/dK/dV against a pure-PyTorch oracle one backend at a time, so it runs with unfused alone, and uses randn inputs so the cap moves the output by O(1). An assertion on that displacement keeps the test from going vacuous if the config drifts. Signed-off-by: Nitin Vegesna <nvegesna@nvidia.com>
Resolve conflict in context_parallel.py: keep the softcap threading into FA2 backward kwargs alongside the new no-load-balance THD zero_tensors guard from NVIDIA#3438. The two changes are independent. Signed-off-by: Nitin Vegesna <nvegesna@nvidia.com>
…rd input Threading `softcap` through the three context-parallel autograd functions added a forward input to each without adding the matching gradient slot to the corresponding backward return tuple, leaving every CP backward one gradient short of its forward inputs. Because `softcap` sits mid-signature, the omission also shifted every later slot in AttnFuncWithCPAndQKVOA2A: `d_softmax_offset` was being returned in `softmax_type`'s position. Insert the missing slot at the `softcap` position in all three tuples. Signed-off-by: Nitin Vegesna <nvegesna@nvidia.com>
No test exercised CP together with softcap, which is why a backward that returned one gradient fewer than its forward inputs went unnoticed in all three CP autograd functions. Thread softcap through the CP runner so it reaches DotProductAttention, and add one case per CP autograd function -- p2p, all_gather and a2a -- checking the softcapped forward and dgrad against the non-CP reference. The cap sits in tanh's nonlinear region so a path that dropped it diverges rather than matching a numerically linear reference. Signed-off-by: Nitin Vegesna <nvegesna@nvidia.com>
…ttention UnfusedDotProductAttention applied the tanh cap after adding post_scale_bias / ALiBi, computing cap(scale*QK + bias). FlashAttention-2 computes cap(scale*QK) + bias: its kernel softcaps immediately after the QK^T gemm and only then adds ALiBi, and it pre-divides alibi_slope by scale_softmax -- which softcapping sets to `softcap` -- so the bias deliberately lands outside the tanh (csrc/flash_attn/src/flash_fwd_kernel.h, mask.h, flash_api.cpp). ALiBi is the one bias type flash supports, so with softcap + ALiBi the unfused and flash paths returned different numerics depending only on whether a suitable flash-attn was installed. Defer the additive bias until after the cap so the two agree. pre_scale_bias is folded in before the scaling by construction and stays inside the cap; flash does not support it. softcap = 0.0 remains a bit-exact no-op for every bias type. Add two tests, both forcing UnfusedDotProductAttention: - test_dpa_softcap_bias_ordering pins cap(scores) + bias against cap(scores + bias), using post_scale_bias to drive the same branch ALiBi uses without needing slope machinery in the reference. - test_dpa_softcap_qk_layer_scaling covers softcap under NVTE_APPLY_QK_LAYER_SCALING, where the cap must be divided by layer_number; omitting that leaves an effective cap of softcap * layer_number. Both carry anti-vacuity asserts, and both were verified by mutation on an H100: reintroducing either bug makes the corresponding test fail, and the existing softcap suite still passes. Signed-off-by: Nitin Vegesna <nvegesna@nvidia.com> Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Pushed one more fix found while re-reviewing the The bug. Softcap was applied after the additive bias, i.e. ALiBi is the one bias type flash supports (pre/post_scale_bias disable it), so The fix defers the additive bias until after the cap. Two tests added, both forcing
Verified by mutation on an H100, not just by passing: reintroducing either bug makes the corresponding test fail, and restoring makes it pass. The existing softcap suite (including the CP tests) still passes alongside. Both tests carry anti-vacuity asserts — the first draft of the layer-scaling test had too small a margin and would have failed, which the mutation run is what caught. |
Adds a
softcapkwarg toDotProductAttentionso models with attention-logit soft-capping (cap·tanh(x/cap), e.g. Gemma2) can run on TE's fused flash-attention kernels instead of falling back to an unfused/non-TE path.Companion PRs (needed together for an end-to-end model to pick this up):
TransformerConfig.attn_logit_softcappingand maps it to thissoftcapkwargattn_logit_softcappingWhat changed
DotProductAttention(init + forward) andAttentionParamsgain asoftcap: float = 0.0kwarg.softcap=0.0is a no-op — existing behavior is unchanged.UnfusedDotProductAttentionappliescap * tanh(scores / cap)to the already-scaled logits, so it serves as the in-tree reference implementation (and the numerical reference the tests compare the flash backends against). With qk-layer-scaling thelayer_numberfactor is divided out of the cap, since that path defers scaling to the softmax.get_attention_backend: whensoftcap != 0, FusedAttention (cuDNN), FA4, and FA3 (unless the checks below pass) are disqualified and selection steers to FA2 or unfused (also disqualifies FA2 builds too old to carry the softcap kernel). This is a deliberate safety net — softcap must never be silently dropped, nor hit aNotImplementedErrorat runtime.softcappresent in both FA3 entry points),max(head_dim_qk, head_dim_v) <= 256, and non-CP; forward threadssoftcapinto FA3's kwargs, backward is handled automatically by the existing Hopper autograd function. No new env var — FA3 eligibility is governed by the existingNVTE_FLASH_ATTN_V3(default1). Since TE already prefers FA3 over FA2 on sm90, FA3 is the default softcap backend on Hopper when a softcap-capable FA3 build is installed; that is intentional.NVTE_FLASH_ATTN_V3=0steers to FA2.tests/pytorch/attention/test_attention.py.test_dpa_softcapsweeps the available backends through the existingtest_dot_product_attentionharness (forward + backward parity against the unfused reference); softcap always disqualifies FusedAttention, so it opts out of the harness's fused-unavailable fallback to keep the dQ/dK/dV comparison.test_dpa_softcap_zero_backend_selectionassertssoftcap=0.0leaves FusedAttention selectable and a nonzero cap does not.test_dpa_softcap_vs_referencecompares forward and dQ/dK/dV against a closed-form pure-PyTorch oracle one backend at a time, soUnfusedDotProductAttentionstays covered on machines without flash-attn; it uses its ownrandninputs because the shared harness's0.1 * randnputs logits at O(1e-2), where a cap of 50 moves the output by ~1e-8 and a dropped cap would be undetectable.Supported configurations
flash-attn >= 2.6.0(first FA2 release exposing asoftcapkwarg). The default path wherever FA3 is not eligible (i.e. off sm90, or no softcap-capable FA3 build). Requires zero attention dropout while training — see below.cp_comm_typevalues:p2p,all_gather,a2a,a2a+p2p(p2panda2a+p2pshare one autograd function). Sameflash-attn >= 2.6.0requirement; forward and backward both carrysoftcap.flash_attn_func/flash_attn_varlen_funcboth exposesoftcap(signature probe, fail-closed) andmax(head_dim_qk, head_dim_v) <= 256. FA3 is Hopper (sm90)-only upstream and governed by the existingNVTE_FLASH_ATTN_V3(default1). When eligible it takes precedence over FA2 on Hopper — deliberate; both paths were exercised on Hopper (see Validation). Any check failing steers to FA2 (>= 2.6.0) or unfused.Not supported:
get_attention_backendwhensoftcap != 0.0, so selection steers to FA2 (or unfused) rather than silently dropping the cap.NotImplementedError(use FA2).get_attention_backendwhensoftcap != 0.0; without that it would be selected on SM100 (NVTE_FLASH_ATTN_V4defaults to1) and silently drop the cap. See the follow-up section below.csrc/flash_attn/flash_api.cpp), so FA2 is disqualified and selection steers to unfused. Dropout reaches the kernel as0.0in eval, so inference configs are unaffected. Since unfused does not support CP, CP + softcap + dropout while training has no eligible backend and raises rather than crashing inside flash-attn.ONNX export force-selects the unfused backend, which now honors
softcapviatorch.tanh(exportable as the ONNXTanhop), so export is expected to work — but there is no ONNX softcap test in this PR.softcap = 0.0(the default) disables softcapping: backend selection and numerics are identical to today.Validation
test_dpa_softcap— forward and backward parity against the unfused reference, across whichever backends are available on the test machine.test_dpa_softcap_vs_reference— forward and dQ/dK/dV against a closed-form reference, forsoftcapin{0.0, 0.5}, with logits at O(1) sotanhruns in its saturating region.softcap=0.0compares against a reference that never appliestanh, and the nonzero case asserts the cap moves the reference output by more than the comparison tolerance, so the test cannot pass an implementation that drops the cap.test_dpa_softcap_zero_backend_selection— thesoftcap=0.0no-op claim for backend selection, which is the half the filter actually changed.On the Hopper target both the FA2 and the FA3 softcap paths were exercised. FA3 taking precedence over FA2 there is an intentional design choice.
Follow-up, not in this PR: FA4
FA4 softcap support is deliberately excluded here rather than included as dead scaffolding. On Blackwell (SM100), FA4's dedicated
head_dim=256forward kernel has noscore_mod/softcap fusion logic in it at all — the kernel constructor assertsscore_mod is None. So there's currently no FA4 kernel path capable of serving this shape; adding an opt-in flag now would just be inert code with nothing to opt into. This follows as its own PR (stacked on this branch) once — or if — an FA4 kernel with softcap fusion forhead_dim=256lands.