Skip to content

Thread tanh logit softcapping through FlashAttention (FA2 and FA3) - #3391

Open
nvegesna-netizen wants to merge 37 commits into
NVIDIA:mainfrom
nvegesna-netizen:nvegesna/gemma2-softcap-core
Open

Thread tanh logit softcapping through FlashAttention (FA2 and FA3)#3391
nvegesna-netizen wants to merge 37 commits into
NVIDIA:mainfrom
nvegesna-netizen:nvegesna/gemma2-softcap-core

Conversation

@nvegesna-netizen

@nvegesna-netizen nvegesna-netizen commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Adds a softcap kwarg to DotProductAttention so 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):

  • Megatron-LM: exposes TransformerConfig.attn_logit_softcapping and maps it to this softcap kwarg
  • Megatron-Bridge: adds an opt-in Gemma2 attention path that sets attn_logit_softcapping

What changed

  • DotProductAttention (init + forward) and AttentionParams gain a softcap: float = 0.0 kwarg. softcap=0.0 is a no-op — existing behavior is unchanged.
  • Threaded through the FA2 non-CP path and all three context-parallel autograd functions (forward + ctx-saved backward).
  • UnfusedDotProductAttention applies cap * 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 the layer_number factor is divided out of the cap, since that path defers scaling to the softmax.
  • get_attention_backend: when softcap != 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 a NotImplementedError at runtime.
  • FA3 (Hopper only): gated on a build-capability probe (softcap present on all three FA3 entry points TE dispatches to), max(head_dim_qk, head_dim_v) <= 256, and non-CP; forward threads softcap into FA3's kwargs, backward is handled automatically by the existing Hopper autograd function. No new env var — FA3 eligibility is governed by the existing NVTE_FLASH_ATTN_V3 (default 1). 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=0 steers to FA2.
  • Tests: three tests in tests/pytorch/attention/test_attention.py. test_dpa_softcap sweeps the available backends through the existing test_dot_product_attention harness (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_selection asserts softcap=0.0 leaves FusedAttention selectable and a nonzero cap does not. test_dpa_softcap_vs_reference compares forward and dQ/dK/dV against a closed-form pure-PyTorch oracle one backend at a time, so UnfusedDotProductAttention stays covered on machines without flash-attn; it uses its own randn inputs because the shared harness's 0.1 * randn puts 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

  • FA2, non-CP — supported with flash-attn >= 2.6.0 (first FA2 release exposing a softcap kwarg). 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.
  • FA2 + context parallelism — supported for all four cp_comm_type values: p2p, all_gather, a2a, a2a+p2p (p2p and a2a+p2p share one autograd function). Same flash-attn >= 2.6.0 requirement; forward and backward both carry softcap.
  • FA3, non-CP — requires an FA3 build whose flash_attn_func, flash_attn_varlen_func and flash_attn_with_kvcache all expose softcap (signature probe, fail-closed) and max(head_dim_qk, head_dim_v) <= 256. FA3 is Hopper (sm90)-only upstream and governed by the existing NVTE_FLASH_ATTN_V3 (default 1). 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.
  • UnfusedDotProductAttention, non-CP — supported; the reference/fallback path, used when no flash backend is eligible (no version floor). Unfused attention does not support context parallelism at all, independent of softcap.

Not supported:

  • FusedAttention (cuDNN) — disqualified in get_attention_backend when softcap != 0.0, so selection steers to FA2 (or unfused) rather than silently dropping the cap.
  • FA3 + CP — raises NotImplementedError (use FA2).
  • FA4 — disqualified in get_attention_backend when softcap != 0.0. FA4 does implement softcap, but TE does not plumb the cap into the FA4 call path (fa_4_optional_forward_kwargs never carries it), so without this filter FA4 would be selected on SM100 (NVTE_FLASH_ATTN_V4 defaults to 1) and silently drop it. See the follow-up section below.
  • FA2 + nonzero attention dropout, while training — flash-attn rejects a nonzero softcap combined with nonzero dropout at dispatch ("Softcapping does not support dropout for now", csrc/flash_attn/flash_api.cpp), so FA2 is disqualified and selection steers to unfused. Dropout reaches the kernel as 0.0 in 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 softcap via torch.tanh (exportable as the ONNX Tanh op), 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

  • Unit: test_dpa_softcap — forward and backward parity against the unfused reference, across whichever backends are available on the test machine.
  • Unit: test_dpa_softcap_vs_reference — forward and dQ/dK/dV against a closed-form reference, for softcap in {0.0, 0.5}, with logits at O(1) so tanh runs in its saturating region. softcap=0.0 compares against a reference that never applies tanh, 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.
  • Unit: test_dpa_softcap_zero_backend_selection — the softcap=0.0 no-op claim for backend selection, which is the half the filter actually changed.
  • End-to-end: exercised via the companion Megatron-LM/Megatron-Bridge changes above — multi-step training runs on both a Hopper and a Blackwell target, confirmed numerically consistent with the prior unfused path, and confirmed via kernel-level profiling that the fused flash kernels (not the fallback path) were selected at runtime.

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

Wiring softcap through to FA4 is left to a separate PR rather than folded in here. FA4 exposes a softcap argument on the non-CP entry points TE calls and implements it for forward and backward, so this is a plumbing task on the TE side rather than a missing kernel. Three Blackwell shapes are excluded: the SM100 forward and the SM100 backward at head_dim=256, and the MLA path with head_dim_v=512. Until that plumbing lands, get_attention_backend disqualifies FA4 whenever the cap is nonzero, so it can never be selected and silently drop the cap.

@greptile-apps

greptile-apps Bot commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Adds tanh attention-logit softcapping throughout TransformerLayer, MultiheadAttention, and DotProductAttention, with backend capability filtering and reference coverage.

  • Threads softcap through FA2, eligible FA3 paths, and all context-parallel FlashAttention autograd functions.
  • Implements softcapping in the unfused backend, including ONNX-compatible behavior and qk-layer-scaling handling.
  • Prevents unsupported fused, FA4, legacy FA2, FA3-context-parallel, and training-dropout configurations from silently ignoring the cap.
  • Adds backend-selection, numerical, gradient, context-parallel, and high-level plumbing tests.

Confidence Score: 5/5

The PR appears safe to merge; no actionable regressions remain in the reviewed changes.

No new correctness, security, or repository-rule violations were identified. All previous findings were manually resolved, and the latest change consistently rejects negative softcap values before backend dispatch.

Important Files Changed

Filename Overview
transformer_engine/pytorch/attention/dot_product_attention/backends.py Implements unfused softcapping and passes the cap to supported FA2 and FA3 entry points.
transformer_engine/pytorch/attention/dot_product_attention/context_parallel.py Carries softcap through the three context-parallel FA2 autograd implementations in forward and backward.
transformer_engine/pytorch/attention/dot_product_attention/dot_product_attention.py Adds constructor and forward softcap plumbing with non-negative input validation.
transformer_engine/pytorch/attention/dot_product_attention/utils.py Adds softcap to backend parameters and filters backends that cannot honor it.
transformer_engine/pytorch/attention/multi_head_attention.py Propagates constructor and per-forward softcap values into DotProductAttention.
transformer_engine/pytorch/transformer.py Propagates softcap to both self-attention and decoder cross-attention.
tests/pytorch/attention/test_attention.py Adds numerical, gradient, backend-selection, bias-ordering, layer-scaling, and high-level plumbing coverage.
tests/pytorch/attention/test_attention_with_cp.py Adds context-parallel softcap coverage across the distinct FA2 communication implementations.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[DotProductAttention with softcap] --> B{softcap is zero?}
    B -->|Yes| C[Existing backend selection]
    B -->|No| D{Eligible FA3 build and shape and non-CP?}
    D -->|Yes| E[FlashAttention 3]
    D -->|No| F{FA2 >= 2.6 and supported dropout mode?}
    F -->|Yes| G[FlashAttention 2]
    F -->|No| H{Context parallel?}
    H -->|No| I[Unfused attention]
    H -->|Yes| J[No eligible backend: raise]
Loading

Reviews (36): Last reviewed commit: "fix(pytorch): reject a negative softcap ..." | Re-trigger Greptile

Comment thread transformer_engine/pytorch/attention/dot_product_attention/utils.py
Comment thread tests/pytorch/attention/test_softcap.py Outdated
@nvegesna-netizen
nvegesna-netizen force-pushed the nvegesna/gemma2-softcap-core branch from a6a793b to 5917f0d Compare August 17, 2026 21:21
@nvegesna-netizen
nvegesna-netizen force-pushed the nvegesna/gemma2-softcap-core branch from 19a21eb to 5ae46ce Compare August 17, 2026 21:35
Comment thread tests/pytorch/attention/test_softcap.py Outdated
Comment thread transformer_engine/pytorch/attention/dot_product_attention/utils.py Outdated
Comment thread transformer_engine/pytorch/attention/dot_product_attention/utils.py
@cyanguwa cyanguwa added the 2.20 label Aug 27, 2026
@cyanguwa

Copy link
Copy Markdown
Collaborator

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

nvegesna-netizen and others added 7 commits August 27, 2026 09:57
…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>
@nvegesna-netizen
nvegesna-netizen force-pushed the nvegesna/gemma2-softcap-core branch from 82a2bf4 to 5ecabac Compare August 27, 2026 16:57
nvegesna-netizen and others added 9 commits August 27, 2026 10:00
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>
@cyanguwa

cyanguwa commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

/te-ci pytorch L0 L1 L3

…lable

test_dpa_softcap_zero_backend_selection asserted that FusedAttention is
selectable at softcap=0.0. Whether it is available at all depends on the arch
and on NVTE_ALLOW_NONDETERMINISTIC_ALGO, not on softcap: the A100 and L40 CI
jobs fail the assert in their deterministic pass, where cuDNN offers no fused
backend for this config, while the same test passes on H100 and B200.

The test is about the softcap filter, so make the availability half a skip.
The meaningful direction -- a nonzero softcap must disable FusedAttention --
still runs wherever a fused backend exists, and skipping keeps the comparison
from being silently vacuous where one does not.

Signed-off-by: Nitin Vegesna <nvegesna@nvidia.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@nvegesna-netizen

nvegesna-netizen commented Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

Triaged the te-ci failures. One was ours; the rest reproduce on unrelated PRs. Fix pushed in 8e49940e.

Ours: L0_pytorch_unittest--A100_1GPU (and part of --L40_1GPU)

FAILED test_attention.py::test_dpa_softcap_zero_backend_selection[softcap_1_0-...]
>   assert fused_off, "softcap=0.0 must not disable FusedAttention"
E   AssertionError: softcap=0.0 must not disable FusedAttention
Error: sub-test failed: NVTE_ALLOW_NONDETERMINISTIC_ALGO=0 test_attention.py

Only the deterministic pass fails; the normal test_attention.py run passes on the same machine (5865 passed). The test asserted that FusedAttention is available at softcap=0.0, but availability depends on arch and on NVTE_ALLOW_NONDETERMINISTIC_ALGO, not on softcap — cuDNN offers no fused backend for this config on A100/L40 in deterministic mode, so the assert fires. It passes on H100 and B200 because a fused backend exists there.

That half is now a pytest.skip rather than an assert. The direction the test actually exists to check — a nonzero softcap must disable FusedAttention — still runs wherever a fused backend is available, and skipping prevents the comparison from being silently vacuous where one isn't.

Not ours

job failure evidence
L0_pytorch_unittest--L40_1GPU test_numerics.py::test_layernorm_linear_accuracy[...] numeric drift (e.g. -2.5 vs -2.765625) also fails on #3466, #3465, #3457
L1_pytorch_distributed_unittest--{H100_4GPU,B200_8GPU} test_fusible_ops.py::test_distributed_fuser_ops[2] also fails on #3466, #3457
L1_pytorch_mcore_fsdp_integration--B200_8GPU No route to host on the c10d TCPStore rendezvous infrastructure
L0_pytorch_lint <EMPTY LOG> no output produced

None of those touch attention.

…ap-core

# Conflicts:
#	tests/pytorch/attention/test_attention.py
#	transformer_engine/pytorch/attention/dot_product_attention/dot_product_attention.py
@nvegesna-netizen

Copy link
Copy Markdown
Contributor Author

/te-ci pytorch L0 L1 L3

…radient

The Lint job fails on this branch with

  dot_product_attention.py:243:4: C0116: Missing function or method docstring
  dot_product_attention.py:248:4: C0116: Missing function or method docstring

_IdentityWithMaskedGradient came from NVIDIA#3274 and is byte-identical to main; the
same two errors fail Lint on that PR's own branch (desh/mixed-thd-pr-minimal),
which merged before the check went green, so this branch inherited a red Lint
by merging main. Not introduced here, but it blocks this PR.

Use the same `# pylint: disable=missing-function-docstring` the other autograd
Functions in this package use (e.g. context_parallel.py) rather than inventing
docstrings. Drop this commit if NVIDIA#3274 is fixed upstream first.

Signed-off-by: Nitin Vegesna <nvegesna@nvidia.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment thread transformer_engine/pytorch/attention/dot_product_attention/utils.py Outdated
Comment thread transformer_engine/pytorch/attention/dot_product_attention/backends.py Outdated
Comment thread transformer_engine/pytorch/attention/dot_product_attention/backends.py Outdated

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

softcap is only exposed through DotProductAttention right now. Could you please also thread it through MultiheadAttention and TransformerLayer? Thanks.

@nvegesna-netizen nvegesna-netizen Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added to both. Each takes a softcap constructor argument stored on the module plus a forward override that falls back to it, matching how window_size is threaded. TransformerLayer passes it to both self_attention and inter_attention, so decoder cross attention is capped too. Resolution sits outside the thd_attention_policies branch at both levels, since softcap is not mask specific. Defaults are 0.0 and None, so existing callers are unaffected.

Two things worth flagging.

I appended softcap to the end of both forward signatures rather than grouping it with the other attention arguments. Inserting it mid signature shifted encoder_output and is_first_microbatch, so a downstream caller passing those positionally would have silently bound the wrong value. Nothing in the repo does that, but both classes are public API. Constructors keep it grouped with window_size, since it sits past every positional tuple used to build these modules.

I also added test_transformer_layer_softcap_plumbing, which hooks both core_attention modules in a decoder layer and asserts each receives the cap, from the constructor and from a forward override. Writing it showed that passing softcap through common_attention_kwargs was partly masking rather than redundant: it pre-set each MultiheadAttention's self.softcap, so deleting the cross attention pass through still satisfied the constructor assertion, since the fallback picked the value back up. The forward override assertion still failed, so the test caught it either way, but only on one of its two halves. Removing that entry makes both halves load bearing, and leaves softcap matching window_size exactly.

The test is more than you asked for and there is no precedent for it here, so easy to drop. My reason for adding it is that a merge from main during this branch's life already silently dropped softcap from attention_params_kwargs, and only a manual check caught it.

Validated on an H100 since writing the above. With the softcap suite green as the baseline, 26 passed, deleting the cross attention pass through fails test_transformer_layer_softcap_plumbing, and restoring it returns the suite to green. The other two arms behave the same way: capping the bias fails the bias_outside_cap variant, and dropping the cap / layer_number division fails the qk_layer_scaling variant. So this is measured rather than inferred.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for thoroughly designing the tests, but I wonder if we could consolidate them a little bit, without losing meaningful test signals. How about this:

  • keep only 2 configs in model_configs_softcap: softcap_1_0 for the high softcap value and to test flash_attn_func API; softcap_3_1 (with "padding_causal" mask though) for a low softcap value and to test flash_attn_varlen_func API
  • fold model_configs_softcap_reference from three configs to two, by merging GQA into the causal config; they are orthogonal to softcapping
  • merge test_dpa_softcap_vs_reference, test_dpa_softcap_bias_ordering and test_dpa_softcap_qk_layer_scaling into one test, parametrized over variants {"plain", "bias_outside_cap", "qk_layer_scaling"}; we can extract a helper function to do the repeated work: force a backend via env vars, build randn q/k/v, construct the module with the cap, compute a right and a wrong variant, assert the two differ by more than 10 * atol, assert TE matches the right one, return the DPA kwargs, forward kwargs and the two closures
  • drop the causal variant from the bias-ordering and qk-layer-scaling cases; the other tests should be sufficient for testing whether the additive bias sits outside the tanh and whether the cap is divided by layer_number

Thanks!

@nvegesna-netizen nvegesna-netizen Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in a7b5160, following all four bullets.

model_configs_softcap is down to softcap_1_0 and softcap_3_1, the latter switched to padding_causal as you suggested so it goes through flash_attn_varlen_func. model_configs_softcap_reference is down to two, with GQA merged into the causal config. The three tests are now one test_dpa_softcap_vs_reference parametrized over {plain, bias_outside_cap, qk_layer_scaling}, with a _softcap_variant_spec helper returning the DPA kwargs, the forward kwargs and a right and a wrong reference closure. The shared work of forcing the backend, building inputs, and asserting the two references differ by more than 10 * atol now happens once. The bias variant reuses _softcap_reference_attention via new bias and cap_includes_bias arguments instead of duplicating its GQA and causal handling, which is what made the merged version shorter than the three it replaces. Causal is dropped from both special variants, as you asked.

The matrix is 48 cases on a bf16 capable GPU, since param_types picks up bf16 when available, with 19 running and 29 skipping and each special variant running once on the non causal reference config. qk_layer_scaling is fp16 only. On an fp16 only machine it is 24, 10 and 14.

Two things I checked before making the change, since consolidating tests can quietly remove the signal they exist for. Both variants still fail if their bug is reintroduced: capping the bias separates the two references by about 0.98, and dropping the cap / layer_number division by about 0.44, against the 10 * atol threshold, which is 0.20 in fp16 and 0.40 in bf16. Those margins hold because the surviving softcap_ref_1_0 is the same shape the originals ran on. I also hoisted layer_number to _SOFTCAP_QK_LAYER_NUMBER = 8 with a note that 3 is too small to separate, because that value is load bearing and would otherwise look arbitrary to the next person.

On head dimensions, the two surviving configs cover 64 and 128: dbb36c33 moves softcap_3_1 to head_dim 128, since FA2 and FA3 compile a separate softcap kernel per head_dim and the input scale is head_dim invariant.

I kept backward on the plain variant only, matching what runs today. Extending it to all three would be more coverage, but it is behaviour that has never run on hardware and I would rather not fold that in under a consolidation.

nvegesna-netizen and others added 2 commits September 4, 2026 12:29
…ap-core

# Conflicts:
#	transformer_engine/pytorch/attention/dot_product_attention/dot_product_attention.py
… comments

Review feedback from NVIDIA#3391.

Thread softcap through MultiheadAttention and TransformerLayer, following the
window_size pattern: a constructor argument stored on the module and a forward
argument that falls back to it. TransformerLayer passes softcap via
common_attention_kwargs, so it reaches cross-attention as well as
self-attention, matching how softmax_type (also not mask-specific) is handled;
scoping it to self-attention would silently leave decoder cross-attention
uncapped. Resolution sits outside the thd_attention_policies branch at both
levels, since softcap is not mask-specific.

Drop "only supported by the FlashAttention and UnfusedDotProductAttention
backends" from the softcap docstrings. It is inaccurate, since support depends
on the FlashAttention version, and get_attention_backend() is the single place
that should describe backend eligibility. The same wording appeared in the
DotProductAttention class and forward docstrings, so all three now describe
only what softcap does.

Shorten the softcap comment in UnfusedDotProductAttention, keeping why the cap
precedes the additive bias and why it is divided by layer_number, as each of
those was a real bug. Remove the fail-loud block comment in the FA3 branch,
whose selection criteria get_attention_backend() already documents, and keep a
single line noting that the FA3 entry points are autograd functions and so the
forward kwarg drives the backward too.

Signed-off-by: Nitin Vegesna <nvegesna@nvidia.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
nvegesna-netizen and others added 6 commits September 4, 2026 14:34
softcap was inserted mid-signature in MultiheadAttention.forward and
TransformerLayer.forward, next to the other attention arguments. That shifts
every parameter after it, so an external caller passing encoder_output or
is_first_microbatch positionally would silently bind it to softcap instead.
Both accept arbitrary objects, so it would be a wrong answer rather than a
TypeError. Nothing in this repo calls either forward with enough positional
arguments to hit it, but both classes are public API.

Move softcap to the end of both forward signatures. Diffing against main
confirms no existing parameter changes index in either. The constructors are
unaffected and keep softcap grouped with window_size and
bottom_right_diagonal, since it sits past every positional tuple used to
construct these modules.

Signed-off-by: Nitin Vegesna <nvegesna@nvidia.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Review feedback from NVIDIA#3391.

Cut model_configs_softcap from six configs to two: softcap_1_0 for a high cap
through flash_attn_func, and softcap_3_1 with padding_causal for a low cap
through flash_attn_varlen_func. Fold model_configs_softcap_reference from three
to two by merging GQA into the causal config, since GQA is orthogonal to
softcapping.

Merge test_dpa_softcap_vs_reference, test_dpa_softcap_bias_ordering and
test_dpa_softcap_qk_layer_scaling into one test parametrized over
{plain, bias_outside_cap, qk_layer_scaling}. A helper returns the DPA kwargs,
the forward kwargs and a right and a wrong reference closure per variant, so
the shared work of forcing a backend, building inputs and asserting the two
references differ by more than the tolerance happens once. The bias variant
reuses _softcap_reference_attention through new bias and cap_includes_bias
arguments rather than duplicating its GQA and causal handling.

Both mutation signals are preserved. The special variants run on
softcap_ref_1_0, the same shape the original margins were measured on, so
capping the bias still separates the two references by about 0.98 and dropping
the cap / layer_number division by about 0.44, against a threshold of 0.20.
layer_number is hoisted to _SOFTCAP_QK_LAYER_NUMBER with a note that 3 is too
small, since that value is load-bearing and would otherwise look arbitrary.

Backward stays on the plain variant only, matching the previous coverage.

Signed-off-by: Nitin Vegesna <nvegesna@nvidia.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds test_transformer_layer_softcap_plumbing. It builds a decoder
TransformerLayer with a softcap, hooks both self_attention.core_attention and
inter_attention.core_attention with a forward pre-hook, and asserts each sees
the value, once from the constructor and once from a forward override. Only
arrival is checked; the numerics are covered by the DotProductAttention tests.

Cross-attention is the half worth asserting. It is reached through a separate
call site from self-attention, so a refactor can drop the cap there while
self-attention keeps working, and nothing else in the suite would notice. Main
restructured both forward methods during this branch's life, so that is a live
risk rather than a hypothetical one.

Writing the test showed that passing softcap through common_attention_kwargs
was not merely redundant but harmful: it pre-set each MultiheadAttention's
self.softcap, so deleting the cross-attention pass-through still produced the
right answer on the constructor path and the assertion could not fail. Removing
that entry makes both assertions catch the regression, and leaves softcap
matching how window_size is threaded, stored on self and passed per call rather
than at construction.

Signed-off-by: Nitin Vegesna <nvegesna@nvidia.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The test inherited NVTE_FLASH_ATTN / NVTE_FUSED_ATTN / NVTE_UNFUSED_ATTN from
whichever softcap test ran before it, since none of them restore those vars. A
nonzero cap also drops FusedAttention and FA4 in the backend filter, so a
leftover NVTE_UNFUSED_ATTN=0 could leave no eligible backend at all and the
forward would raise "No dot product attention backend is available" rather than
fail an assertion. The outcome also depended on which parametrization of the
preceding test happened to run last.

Set the three variables explicitly, matching the convention of the other tests
in this file, leaving flash and unfused both enabled so at least one backend is
available with or without flash-attn. The assertions stay backend agnostic
because the hook fires on DotProductAttention itself.

Signed-off-by: Nitin Vegesna <nvegesna@nvidia.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@nvegesna-netizen

Copy link
Copy Markdown
Contributor Author

/te-ci pytorch

@nvegesna-netizen

nvegesna-netizen commented Sep 6, 2026

Copy link
Copy Markdown
Contributor Author

Tightened the softcap comments in backends.py and utils.py in cef71d85. Comment lines this PR adds to those two files go from 35 to 15, though total added lines only fall from 102 to 82, and the cut is uneven: utils.py loses about three quarters of its comment lines, backends.py about a third. I kept the two facts that are not inferable from the code, that FA2 adds ALiBi outside the cap and why the cap is divided by layer_number.

Two corrections to things this PR previously asserted, and one further fix.

FA4 does implement softcap. Both non-CP entry points TE calls expose it, lowered to a score mod for forward and backward. It is excluded on three Blackwell shapes rather than one: SM100 forward and SM100 backward at head_dim=256, and the MLA head_dim_v=512 path. TE disqualifies FA4 for softcap because fa_4_optional_forward_kwargs never carries the cap, so the filter is correct, but the log line attributed the limitation to the library and I have fixed it. Also, TE does not pin an FA4 version; 4.0.0b11 is only what the install hint suggests, and backends.py has explicit handling for 4.0.0b24.

The FA3 probe establishes API presence, not build configuration. It reads the Python wrapper, so a FLASHATTENTION_DISABLE_SOFTCAP build still passes it and rejects a nonzero cap at dispatch instead.

The probe now covers all three FA3 entry points (8805255b). The cap is injected into fa_3_optional_forward_kwargs without regard to which entry point was selected, and flash_attn_with_kvcache_v3 is chosen on the padding or thd branch when inference_params is set, so it was previously validated only by proxy. No shipped build changes behaviour: all three gained softcap in 2.7.3/hopper and none had it before. For precision, utils.py notes that 2.7.3+ has different APIs but enforces no FA3 minimum, so that is a documented API break rather than a version floor TE checks.

The two numerical variants and the plumbing test are validated on an H100 by mutation, with the suite green as the baseline: capping the bias, removing the cap / layer_number division, and dropping the cross attention pass through each fail their corresponding test, and restoring returns the suite to green. Both numerical variants run on UnfusedDotProductAttention only, so they pin TE's reference arithmetic rather than the flash kernels.


Collecting the open items in one place, since they are spread across several threads.

  1. FA3 is the default softcap path on Hopper, where FA3 is sm90 only. Dropping NVTE_FA3_SOFTCAP at your suggestion means FA3 is preferred over FA2 whenever the build exposes softcap, max(head_dim_qk, head_dim_v) <= 256, and context parallelism is off. Under CP it still falls back to FA2. The title said "opt-in FA3" and no longer matched, so I corrected it. One consequence worth your view: on a FLASHATTENTION_DISABLE_SOFTCAP FA3 build there is now no escape hatch short of NVTE_FLASH_ATTN_V3=0, because the probe cannot detect that build. Happy to reinstate a subordinate opt-in if you would rather have one.

  2. The FA2 dropout gate should stay. I offered on 08-27, in the thread on 5ecabac, to drop it if you wanted this PR narrower. Withdrawing that offer. Before this PR, softcap was hardcoded to 0.0 at every FA call site, so softcap combined with dropout was unreachable. The gate closes a failure mode this PR itself creates rather than an unrelated one. Without it, that configuration raises RuntimeError("Softcapping does not support dropout for now") from a host side TORCH_CHECK in flash_api.cpp, on a combination get_attention_backend reports as supported.

    A related gap I should flag rather than leave implicit: FA2 has no build probe at all, unlike FA3. A FLASHATTENTION_DISABLE_SOFTCAP FA2 build passes the v2_6_0_plus check and then rejects at dispatch. Happy to add a probe there for symmetry if you want it.

  3. test_transformer_layer_softcap_plumbing is more than you asked for, and easy to drop. Removing the common_attention_kwargs entry is what makes both of its assertions load bearing, matching how window_size is threaded. Note the test asserts on captured kwargs rather than numerics, so it is hardware independent. I have also wrapped it in try/finally in 590c7ca0 to restore the backend env vars, which it was leaking into the rest of the suite.

  4. Head dimension, already committed in dbb36c33, so revert it if you disagree. I widened softcap_3_1 from 64 to 128 rather than adding a third config, so the count stays at the two you named and it costs no new cases. Being precise, this is a swap rather than a pure gain: model_configs_softcap now has no head_dim 64 varlen case. I think 128 is the better of the two, since FA2 and FA3 instantiate separate kernels per head dim and 128 is what Gemma 2 27B and Grok 1 use, whereas nothing that softcaps uses 64. Your call. Neither of your justifications for the pair changes: head_dim plays no part in the flash_attn_func versus flash_attn_varlen_func dispatch, and the 0.01 cap rationale is head dim invariant because softmax_scale cancels the sqrt(d) exactly.

    head_dim 256, which Gemma 2 2B and 9B use, stays uncovered. A third config would cost two cases on bf16 capable hardware. I am not proposing it, since the end to end Megatron runs exercise 256 on both FA2 and FA3, and it would take the dict back to three configs after you asked for two.

nvegesna-netizen and others added 2 commits September 7, 2026 00:09
Halve the comment volume added to backends.py and utils.py, keeping only the
facts that are not inferable from the code: that FA2 adds ALiBi outside the cap,
and why the cap is divided by layer_number. Which backend supports what is
already expressed by get_attention_backend and its debug messages.

Also correct two inaccuracies found while auditing the surviving text. FA4 does
implement softcap on both entry points TE calls, forward and backward, so it is
disqualified because TE does not plumb the cap to it, not because the library
lacks it; the log message said otherwise. And the FA3 signature probe detects
whether the installed release exposes the kwarg at all, not whether the build
set FLASHATTENTION_DISABLE_SOFTCAP, which still exposes it and rejects a nonzero
cap at dispatch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Nitin Vegesna <nvegesna@nvidia.com>
softcap is injected into fa_3_optional_forward_kwargs without regard to which
entry point was selected, so the probe should cover every function the cap can
reach. flash_attn_with_kvcache is the one chosen when inference_params is set,
and it was validated only by proxy through the other two.

No shipped FA3 build changes behaviour: all three entry points gained softcap in
2.7.3/hopper, which is TE's documented FA3 floor. Before that none of them had
it. The symbol is already imported and None-guarded, so the probe cannot fire on
an uninstalled FA3.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Nitin Vegesna <nvegesna@nvidia.com>
@nvegesna-netizen nvegesna-netizen changed the title Thread tanh logit softcapping through FlashAttention (FA2, opt-in FA3) Thread tanh logit softcapping through FlashAttention (FA2 and FA3) Sep 7, 2026
@nvegesna-netizen
nvegesna-netizen force-pushed the nvegesna/gemma2-softcap-core branch from 639f601 to 8805255 Compare September 7, 2026 07:12
nvegesna-netizen and others added 3 commits September 7, 2026 10:14
Cutting model_configs_softcap to two configs dropped head_dim 128 as a side
effect, leaving every softcap config at 64. FA2 and FA3 compile a separate
softcap kernel per head_dim bucket, and above 128 FA2 additionally forces the
non-even-MN predicated path, so a bug in those kernels is invisible at 64.

Widen the existing padding config instead of adding a third, so the count stays
at two. Neither justification for the pair changes: head_dim plays no part in
the flash_attn_func vs flash_attn_varlen_func dispatch, and the 0.01 cap
rationale is head_dim invariant because softmax_scale cancels the sqrt(d) growth
of the dot product.

128 rather than 256 because get_attention_backend disables FA3 for deterministic
backward at head_dim >= 256, which would leave only UnfusedDotProductAttention
and skip the comparison in the NVTE_ALLOW_NONDETERMINISTIC_ALGO=0 CI pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Nitin Vegesna <nvegesna@nvidia.com>
The test set NVTE_FLASH_ATTN, NVTE_FUSED_ATTN and NVTE_UNFUSED_ATTN and never
restored them, leaking NVTE_FUSED_ATTN=0 into every test that ran after it. Its
own comment names that hazard as the reason it sets them explicitly, then
reproduces it.

Wrap the body in try/finally, restore the previous values, and invalidate the
backend selection cache on the way out.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Nitin Vegesna <nvegesna@nvidia.com>
tanh is odd, so UnfusedDotProductAttention's `cap * tanh(x / cap)` gives a
bit-identical result for -c and +c, i.e. it caps at the absolute value.
FlashAttention only enables capping under `softcap > 0.0`, so it applies no cap
at all. get_attention_backend gates on `softcap != 0.0`, so a negative value
passed every filter and the two backends silently disagreed, which is the exact
failure mode the rest of this PR exists to prevent.

Raise on a negative cap where softcap is resolved, which is the choke point every
caller reaches, including MultiheadAttention and TransformerLayer.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Nitin Vegesna <nvegesna@nvidia.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

2.20 community-contribution PRs from external contributor outside the core maintainers, representing community-driven work.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants