Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
69b4ffb
feat: thread tanh logit softcapping through FlashAttention (FA2, opt-…
nvegesna-netizen Aug 17, 2026
9839ae0
fix: use raise instead of assert for the ONNX+softcap guard
nvegesna-netizen Aug 17, 2026
eb215b5
[pre-commit.ci] auto fixes from pre-commit.com hooks
nvegesna-netizen Aug 17, 2026
2475521
refactor: move softcap reference into UnfusedDotProductAttention and …
nvegesna-netizen Aug 27, 2026
3c5eb4a
fix(pytorch): gate FA3 softcap on existing NVTE_FLASH_ATTN_V3
nvegesna-netizen Aug 27, 2026
900371a
fix(pytorch): disable FlashAttention 4 for softcap
nvegesna-netizen Aug 27, 2026
5ecabac
fix(pytorch): disable FlashAttention 2 for softcap with dropout
nvegesna-netizen Aug 27, 2026
818ce26
Merge branch 'main' into nvegesna/gemma2-softcap-core
nvegesna-netizen Aug 27, 2026
4cc2e9c
test: restore softcap dQ/dK/dV parity in the shared DPA harness
nvegesna-netizen Aug 27, 2026
13d65a1
test: add softcap no-op and closed-form reference coverage
nvegesna-netizen Aug 27, 2026
8be48f4
Merge branch 'main' into nvegesna/gemma2-softcap-core
nvegesna-netizen Aug 28, 2026
55a6ed9
Merge branch 'main' into nvegesna/gemma2-softcap-core
nvegesna-netizen Aug 28, 2026
8cb7f9c
Merge branch 'main' into nvegesna/gemma2-softcap-core
nvegesna-netizen Aug 31, 2026
9ba7803
fix(pytorch): align CP autograd backward arity with the softcap forwa…
nvegesna-netizen Aug 31, 2026
db6a119
test(pytorch): cover context parallelism with softcap
nvegesna-netizen Aug 31, 2026
49ede92
fix(pytorch): apply softcap before the additive bias, matching FlashA…
nvegesna-netizen Sep 1, 2026
18f67b7
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Sep 1, 2026
bce4d3f
Merge branch 'main' into nvegesna/gemma2-softcap-core
nvegesna-netizen Sep 1, 2026
4a3558a
Merge branch 'main' into nvegesna/gemma2-softcap-core
cyanguwa Sep 2, 2026
8e49940
fix(test): skip the softcap no-op check when FusedAttention is unavai…
nvegesna-netizen Sep 3, 2026
6387748
Merge remote-tracking branch 'origin/main' into nvegesna/gemma2-softc…
nvegesna-netizen Sep 3, 2026
42975ac
fix(lint): silence missing-function-docstring on _IdentityWithMaskedG…
nvegesna-netizen Sep 3, 2026
d288f88
Merge branch 'main' into nvegesna/gemma2-softcap-core
nvegesna-netizen Sep 3, 2026
7349579
Merge remote-tracking branch 'origin/main' into nvegesna/gemma2-softc…
nvegesna-netizen Sep 4, 2026
67277a0
feat: expose softcap on MultiheadAttention and TransformerLayer; trim…
nvegesna-netizen Sep 4, 2026
33fe732
fix: append softcap to the MHA and TransformerLayer forward signatures
nvegesna-netizen Sep 4, 2026
a7b5160
test: consolidate the softcap tests
nvegesna-netizen Sep 4, 2026
022a37b
test: assert TransformerLayer forwards softcap to both attention modules
nvegesna-netizen Sep 4, 2026
2df3668
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Sep 4, 2026
443f692
test: set explicit backend state in the softcap plumbing test
nvegesna-netizen Sep 4, 2026
733144d
Merge branch 'main' into nvegesna/gemma2-softcap-core
nvegesna-netizen Sep 4, 2026
146b2d5
Merge branch 'main' into nvegesna/gemma2-softcap-core
nvegesna-netizen Sep 6, 2026
cef71d8
chore(pytorch): tighten softcap comments and correct the FA4 attribution
nvegesna-netizen Sep 6, 2026
8805255
fix(pytorch): probe softcap on all three FA3 entry points
nvegesna-netizen Sep 6, 2026
dbb36c3
test: exercise softcap at head_dim 128, not only 64
nvegesna-netizen Sep 7, 2026
590c7ca
test: restore backend env vars in the softcap plumbing test
nvegesna-netizen Sep 7, 2026
f0cd672
fix(pytorch): reject a negative softcap instead of diverging silently
nvegesna-netizen Sep 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions tests/pytorch/attention/run_attention_with_cp.py
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,7 @@ def run_dpa_with_cp(
fa_pad_between_seqs="False",
deterministic="False",
load_balancing_strategy="DUAL_CHUNK_SWAP",
softcap="0.0",
log_level=logging.WARNING,
):
"""Test DotProductAttention module with context parallelism"""
Expand Down Expand Up @@ -281,6 +282,7 @@ def run_dpa_with_cp(
config.attn_mask_type = "padding_causal"
else:
config.attn_mask_type = "padding"
config.softcap = float(softcap)

# set up distributed group
rank = int(os.getenv("RANK", "0"))
Expand Down Expand Up @@ -342,6 +344,7 @@ def run_dpa_with_cp(
qkv_format=qkv_format,
attn_mask_type=config.attn_mask_type,
window_size=config.window_size,
softcap=config.softcap,
softmax_type=config.softmax_type,
return_max_logit=config.return_max_logit,
).cuda()
Expand Down
364 changes: 363 additions & 1 deletion tests/pytorch/attention/test_attention.py

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.

Large diffs are not rendered by default.

39 changes: 39 additions & 0 deletions tests/pytorch/attention/test_attention_with_cp.py
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,45 @@ def test_cp_with_flash_attention(cp_pool, dtype, model, qkv_format, cp_comm_type
)


@pytest.mark.skipif(
not FlashAttentionUtils.v2_6_0_plus, reason="CP softcap requires flash-attn 2.6.0+."
)
@pytest.mark.skipif(get_device_compute_capability() < (8, 0), reason="CP tests require sm80+.")
@pytest.mark.parametrize("cp_comm_type", ["p2p", "all_gather", "a2a"])
def test_cp_with_flash_attention_softcap(cp_pool, cp_comm_type):
"""Check softcap forward and dgrad against the non-CP reference.

One case per CP autograd function, since P2P, all-gather and A2A each thread softcap
through their own forward inputs and gradient slots.
"""
config = copy.deepcopy(model_configs_flash_attn["cp_2_0"])
config.context_parallel = True
config.cp_comm_type = cp_comm_type
# The runner's clamped-randn inputs put the scaled logits at O(1), so this cap sits in
# tanh's nonlinear region and a path that dropped it would diverge from the reference.
config.softcap = 0.5
available_backends, _, _ = get_available_attention_backends(
config,
qkv_dtype=torch.bfloat16,
qkv_layout="bshd_bshd_bshd",
is_training=True,
deterministic=_deterministic,
)
if not available_backends[0]:
pytest.skip("FlashAttention is unavailable.")
_submit(
cp_pool(2),
dtype="bf16",
model="cp_2_0",
qkv_format="bshd",
kernel_backend="FlashAttention",
cp_comm_type=cp_comm_type,
softcap=config.softcap,
deterministic=_deterministic,
log_level=pytest_logging_level,
)


model_configs_fused_attn = {
# test: ModelConfig(b, sq, hq, dqk)
"cp_1_0": ModelConfig(2, 4096, 12, 128, attn_mask_type="causal", return_max_logit=True), # MHA
Expand Down
3 changes: 3 additions & 0 deletions tests/pytorch/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,7 @@ def __init__(
alibi_type: str = "none",
bias_shape: str = "1hss",
window_size: Tuple[int, int] = (-1, -1),
softcap: float = 0.0,
context_parallel: bool = False,
cp_comm_type: str = "p2p",
return_max_logit=False,
Expand Down Expand Up @@ -312,6 +313,7 @@ def __init__(
self.attn_type = "self" if (self.max_seqlen_q == self.max_seqlen_kv) else "cross"
self.bias_shape = bias_shape
self.window_size = check_set_window_size(self.attn_mask_type, window_size)
self.softcap = softcap
self.context_parallel = context_parallel
self.cp_comm_type = cp_comm_type
self.return_max_logit = return_max_logit
Expand Down Expand Up @@ -390,6 +392,7 @@ def test():
head_dim_v=config.head_dim_v,
attn_mask_type=config.attn_mask_type,
window_size=config.window_size,
softcap=config.softcap,
alibi_slopes_shape=alibi_slopes_shape,
core_attention_bias_type=config.attn_bias_type,
core_attention_bias_shape=core_attention_bias_shape,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from contextlib import nullcontext
from importlib.metadata import version as get_pkg_version
from importlib.metadata import PackageNotFoundError
import inspect
import os
from typing import Any, Callable, Dict, List, Optional, Tuple, Union
import warnings
Expand Down Expand Up @@ -166,6 +167,18 @@

fa_utils.set_flash_attention_3_params()

# Older FA3 releases expose no `softcap` kwarg, so probe the API rather than the version.
# This cannot see a FLASHATTENTION_DISABLE_SOFTCAP build: that still exposes the kwarg and
# rejects a nonzero cap at dispatch.
try:
fa_utils.fa3_supports_softcap = (
"softcap" in inspect.signature(flash_attn_func_v3).parameters
and "softcap" in inspect.signature(flash_attn_varlen_func_v3).parameters
and "softcap" in inspect.signature(flash_attn_with_kvcache_v3).parameters
)
except (ValueError, TypeError):
fa_utils.fa3_supports_softcap = False

# Try to import Flash Attention v4
try:
fa_utils.fa4_version = PkgVersion(get_pkg_version("flash-attn-4"))
Expand Down Expand Up @@ -435,6 +448,7 @@ def _forward(
attention_mask: Optional[Union[torch.Tensor, Tuple[torch.Tensor, torch.Tensor]]] = None,
window_size: Optional[Tuple[int, int]] = None,
bottom_right_diagonal: Optional[bool] = None,
softcap: float = 0.0,
core_attention_bias_type: str = "no_bias",
core_attention_bias: Optional[torch.Tensor] = None,
alibi_slopes: Optional[torch.Tensor] = None,
Expand Down Expand Up @@ -626,6 +640,8 @@ def _forward(
key_layer = key_layer.reshape(output_size[3], output_size[0] * output_size[1], -1)

# Raw attention scores. [b * h, sq, sk]
# `post_scale_bias`/ALiBi are deferred until after the softcap below; see the cap.
deferred_bias = None
if core_attention_bias_type == "no_bias":
matmul_result = torch.baddbmm(
matmul_result,
Expand Down Expand Up @@ -669,9 +685,19 @@ def _forward(
beta=0.0,
alpha=scale,
)
matmul_result = (matmul_result.view(*output_size) + core_attention_bias).to(
dtype=query_layer.dtype
)
matmul_result = matmul_result.view(*output_size)
deferred_bias = core_attention_bias

# The cap lands on the scaled logits before `post_scale_bias`/ALiBi: FA2 caps right
# after the QK^T gemm and adds ALiBi afterwards, so capping those would diverge from it.
# `pre_scale_bias` is folded in before the scaling, so it stays inside the cap. qk layer
# scaling defers the layer_number factor to the softmax below, so divide it out here.
if softcap != 0.0:
cap = softcap / self.layer_number if apply_qk_layer_scaling else softcap
matmul_result = cap * torch.tanh(matmul_result / cap)

if deferred_bias is not None:
matmul_result = (matmul_result + deferred_bias).to(dtype=query_layer.dtype)

if fp8:
# quantize and dequantize dP to emulate FP8
Expand Down Expand Up @@ -894,6 +920,7 @@ def forward(
max_seqlen_kv: Optional[int] = None,
attn_mask_type: str = "causal",
window_size: Optional[Tuple[int, int]] = None,
softcap: float = 0.0,
alibi_slopes: Optional[torch.Tensor] = None,
cp_group: Optional[Union[dist_group_type, List[dist_group_type]]] = None,
cp_global_ranks: List[int] = None,
Expand Down Expand Up @@ -1110,6 +1137,11 @@ def forward(
assert (
alibi_slopes is None
), "Alibi slope bias addition is not supported with context parallelism."
if use_flash_attn_3 and softcap != 0.0:
raise NotImplementedError(
"softcap is not supported by the FlashAttention 3 backend in context "
"parallel. Please use FlashAttention 2 (>= 2.6.0) for softcap support."
)
with self.attention_dropout_ctx():
output = attn_forward_func_with_cp(
self.training,
Expand Down Expand Up @@ -1140,6 +1172,7 @@ def forward(
attn_mask_type=attn_mask_type,
deterministic=self.deterministic,
window_size=window_size,
softcap=softcap,
quantizers=quantizers,
pad_between_seqs=pad_between_seqs,
use_flash_attn_3=use_flash_attn_3,
Expand Down Expand Up @@ -1237,6 +1270,8 @@ def forward(
fa_optional_forward_kwargs["alibi_slopes"] = alibi_slopes
if fa_utils.v2_4_1_plus:
fa_optional_forward_kwargs["deterministic"] = self.deterministic
if fa_utils.v2_6_0_plus:
fa_optional_forward_kwargs["softcap"] = softcap
if inference_params is not None:
# use block_table kwarg to support thd_2bshd for non-paged
fa_optional_forward_kwargs["block_table"] = (
Expand All @@ -1257,9 +1292,17 @@ def forward(
**fa_optional_forward_kwargs,
)
else:
if softcap != 0.0 and not fa_utils.fa3_supports_softcap:
raise NotImplementedError(
"softcap is not supported by the installed FlashAttention 3 build. "
"Please use FlashAttention 2 (>= 2.6.0) for softcap support."
)
fa_3_optional_forward_kwargs = {}
fa_3_optional_forward_kwargs["window_size"] = window_size
fa_3_optional_forward_kwargs["num_splits"] = num_splits
if softcap != 0.0 and fa_utils.fa3_supports_softcap:
# FA3 entry points are autograd functions, so this drives the backward too.
fa_3_optional_forward_kwargs["softcap"] = softcap
if pad_between_seqs:
fa_3_optional_forward_kwargs["seqused_q"] = (
cu_seqlens_q[1:] - cu_seqlens_q[:-1]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1617,6 +1617,7 @@ def forward(
deterministic,
use_fused_attention,
return_max_logit,
softcap,
Comment thread
nvegesna-netizen marked this conversation as resolved.
fp8,
fp8_meta,
cp_group,
Expand Down Expand Up @@ -1906,7 +1907,7 @@ def forward(
if fa_utils.v2_5_7_plus and qkv_format == "thd":
fa_forward_kwargs["block_table"] = None
if fa_utils.v2_6_0_plus:
fa_forward_kwargs["softcap"] = 0.0
fa_forward_kwargs["softcap"] = softcap

# set up inputs for forward
q_inputs = [None, None]
Expand Down Expand Up @@ -2399,6 +2400,7 @@ def forward(
ctx.attn_bias_type = attn_bias_type
ctx.attn_bias_shape = None if attn_bias is None else attn_bias.shape
ctx.deterministic = deterministic
ctx.softcap = softcap
ctx.use_fused_attention = use_fused_attention
ctx.pad_between_seqs = pad_between_seqs
ctx.softmax_lse_in_packed_format = softmax_lse_in_packed_format
Expand Down Expand Up @@ -2705,7 +2707,7 @@ def backward(ctx, dout, *_args):
if fa_utils.v2_4_1_plus:
fa_backward_kwargs["deterministic"] = ctx.deterministic
if fa_utils.v2_6_0_plus:
fa_backward_kwargs["softcap"] = 0.0
fa_backward_kwargs["softcap"] = ctx.softcap

send_recv_reqs = []
for i in range(cp_size):
Expand Down Expand Up @@ -3212,6 +3214,7 @@ def backward(ctx, dout, *_args):
None,
None,
None,
None,
)


Expand Down Expand Up @@ -3289,6 +3292,7 @@ def forward(
deterministic,
use_fused_attention,
return_max_logit,
softcap,
window_size,
cp_group,
cp_stream,
Expand Down Expand Up @@ -3382,7 +3386,7 @@ def forward(
if fa_utils.v2_5_7_plus and qkv_format == "thd":
fa_forward_kwargs["block_table"] = None
if fa_utils.v2_6_0_plus:
fa_forward_kwargs["softcap"] = 0.0
fa_forward_kwargs["softcap"] = softcap

qkv_layout = qkv_format + "_" + qkv_format + "_" + qkv_format

Expand Down Expand Up @@ -3974,6 +3978,7 @@ def forward(
ctx.attn_bias_type = attn_bias_type
ctx.attn_mask_type = attn_mask_type
ctx.deterministic = deterministic
ctx.softcap = softcap
ctx.use_fused_attention = use_fused_attention
ctx.use_flash_attn_3 = use_flash_attn_3
ctx.use_flash_attn_4 = use_flash_attn_4
Expand Down Expand Up @@ -4183,7 +4188,7 @@ def backward(ctx, dout, *_args):
if fa_utils.v2_4_1_plus:
fa_backward_kwargs["deterministic"] = ctx.deterministic
if fa_utils.v2_6_0_plus:
fa_backward_kwargs["softcap"] = 0.0
fa_backward_kwargs["softcap"] = ctx.softcap
if (
ctx.qkv_format == "thd"
and ctx.load_balancing_strategy is CPLoadBalancingStrategy.NO_LOAD_BALANCE
Expand Down Expand Up @@ -4571,6 +4576,7 @@ def backward(ctx, dout, *_args):
None,
None,
None,
None,
)


Expand Down Expand Up @@ -4602,6 +4608,7 @@ def forward(
deterministic,
use_fused_attention,
return_max_logit,
softcap,
window_size,
fp8,
fp8_meta,
Expand Down Expand Up @@ -4703,7 +4710,7 @@ def forward(
if fa_utils.v2_5_7_plus and qkv_format == "thd":
fa_forward_kwargs["block_table"] = None
if fa_utils.v2_6_0_plus:
fa_forward_kwargs["softcap"] = 0.0
fa_forward_kwargs["softcap"] = softcap

assert isinstance(k, q.__class__) and isinstance(
v, q.__class__
Expand Down Expand Up @@ -5028,6 +5035,7 @@ def forward(
ctx.attn_mask_type = attn_mask_type
ctx.attn_bias_type = attn_bias_type
ctx.deterministic = deterministic
ctx.softcap = softcap
ctx.window_size = window_size
ctx.use_fused_attention = use_fused_attention
ctx.fp8_meta = fp8_meta
Expand Down Expand Up @@ -5178,7 +5186,7 @@ def backward(ctx, dout, *_args):
if fa_utils.v2_4_1_plus:
fa_backward_kwargs["deterministic"] = ctx.deterministic
if fa_utils.v2_6_0_plus:
fa_backward_kwargs["softcap"] = 0.0
fa_backward_kwargs["softcap"] = ctx.softcap

dq_fp8, dk_fp8, dv_fp8 = None, None, None
if ctx.use_fused_attention:
Expand Down Expand Up @@ -5406,6 +5414,7 @@ def backward(ctx, dout, *_args):
None,
None,
None,
None,
d_softmax_offset,
None,
)
Expand Down Expand Up @@ -5435,6 +5444,7 @@ def attn_forward_func_with_cp(
deterministic=False,
use_fused_attention=False,
window_size=None,
softcap=0.0,
fp8=False,
fp8_meta=None,
quantizers=None,
Expand Down Expand Up @@ -5621,6 +5631,7 @@ def attn_forward_func_with_cp(
deterministic,
use_fused_attention,
return_max_logit,
softcap,
]

if cp_comm_type in ["p2p", "a2a+p2p"]:
Expand Down
Loading
Loading