Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 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
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
360 changes: 359 additions & 1 deletion tests/pytorch/attention/test_attention.py

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 @@ -405,6 +405,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,19 @@

fa_utils.set_flash_attention_3_params()

# Probe whether this FA3 build exposes a `softcap` parameter on BOTH entry points. FA3's Hopper
# (sm90) kernels DO implement tanh logit softcapping in fwd AND bwd (dedicated
# flash_{fwd,bwd}_hdim256_bf16_softcap_sm90 instantiations, off only behind a compile-time
# DISABLE_SOFTCAP flag), so this is a mature path. Still fail-closed and additionally
# gated on head_dim <= 256 + non-CP in get_attention_backend.
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
)
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 +449,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 +641,9 @@ def _forward(
key_layer = key_layer.reshape(output_size[3], output_size[0] * output_size[1], -1)

# Raw attention scores. [b * h, sq, sk]
# An additive `post_scale_bias`/ALiBi term is deferred until after the softcap below, so
# that the cap applies to the bare scaled logits (see the softcap comment for why).
deferred_bias = None
if core_attention_bias_type == "no_bias":
matmul_result = torch.baddbmm(
matmul_result,
Expand Down Expand Up @@ -669,9 +687,25 @@ 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

# Cap the scaled logits -- softcap * tanh(scores * scale / softcap) -- matching how
# FlashAttention folds softmax_scale into its tanh argument. The cap is applied to the
# bare scaled logits, before any additive bias: FA2 softcaps immediately after the QK^T
# gemm and only then adds ALiBi (its alibi_slope is pre-divided by scale_softmax, which
# softcapping sets to `softcap`, so the bias lands outside the tanh). Capping the bias
# too would silently diverge from FA2, which is selectable alongside this backend for
# ALiBi -- the one bias type flash supports (pre/post_scale_bias disable it outright).
# `pre_scale_bias` is folded in before the scaling by construction, so it is necessarily
# inside the cap. qk layer scaling defers the layer_number factor to the softmax below,
# so it is divided out of the cap 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 +928,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 +1145,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 +1180,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 +1278,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 +1300,24 @@ def forward(
**fa_optional_forward_kwargs,
)
else:
# Fail-loud net: get_attention_backend only keeps FA3 for softcap on a
# softcap-capable build (signature probe) + Hopper (FA3 is sm90-only upstream)
# + head_dim <= 256. If FA3 is still reached with softcap while the build lacks
# support (force-selected / regressed path), raise rather than silently drop the
# cap. The non-CP FA3 entry points
# (flash_attn_func_v3 / flash_attn_varlen_func_v3) are self-contained autograd
# functions, so threading `softcap` into the forward call also drives the
# matching FA3 softcap backward kernel. (CP + FA3 + softcap stays blocked above.)
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:
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