Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions qa/L0_pytorch_unittest/test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ NVTE_ALLOW_UNSAFE_PICKLE_EXTRA_STATE=1 NVTE_ALLOW_NONDETERMINISTIC_ALGO=0 python
python3 -m pytest --tb=auto --junitxml=$XML_LOG_DIR/pytest_test_linear_mxfp8_attention.xml $TE_PATH/tests/pytorch/attention/test_linear_mxfp8_attention.py || test_fail "test_linear_mxfp8_attention.py"
python3 -m pytest --tb=auto --junitxml=$XML_LOG_DIR/pytest_test_fused_mla_q_uproj.xml $TE_PATH/tests/pytorch/attention/test_fused_mla_q_uproj.py || test_fail "test_fused_mla_q_uproj.py"
python3 -m pytest --tb=auto --junitxml=$XML_LOG_DIR/pytest_test_kv_cache.xml $TE_PATH/tests/pytorch/attention/test_kv_cache.py || test_fail "test_kv_cache.py"
python3 -m pytest --tb=auto --junitxml=$XML_LOG_DIR/pytest_test_softcap.xml $TE_PATH/tests/pytorch/attention/test_softcap.py || test_fail "test_softcap.py"
python3 -m pytest --tb=auto --junitxml=$XML_LOG_DIR/pytest_test_cu_seqlens_cache.xml $TE_PATH/tests/pytorch/attention/test_cu_seqlens_cache.py || test_fail "test_cu_seqlens_cache.py"
python3 -m pytest --tb=auto --junitxml=$XML_LOG_DIR/pytest_test_hf_integration.xml $TE_PATH/tests/pytorch/test_hf_integration.py || test_fail "test_hf_integration.py"
export NVTE_TEST_CHECKPOINT_ARTIFACT_PATH=$TE_PATH/artifacts/tests/pytorch/test_checkpoint
Expand Down
147 changes: 147 additions & 0 deletions tests/pytorch/attention/test_softcap.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
# Copyright (c) 2022-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
#
# See LICENSE for license information.
"""Isolation numerics test for tanh logit softcapping in DotProductAttention.

The reference implements softcapping in pure PyTorch:

scores = (Q @ K^T) * scale
scores = softcap * tanh(scores / softcap) # only when softcap != 0.0
scores = scores + mask
attn = softmax(scores)
out = attn @ V

and is compared against ``DotProductAttention(..., softcap=...)`` forced onto the
FlashAttention backend, for both the forward output and the input gradients
(dQ/dK/dV obtained via autograd).
"""

import sys
import pathlib

import pytest
import torch
from packaging.version import Version as PkgVersion

from transformer_engine.pytorch import DotProductAttention
from transformer_engine.pytorch.attention.dot_product_attention import _attention_backends

_current_file = pathlib.Path(__file__).resolve()
sys.path = [str(_current_file.parent.parent)] + sys.path
from utils import reset_rng_states # pylint: disable=wrong-import-position


def _flash_attn_2_6_available() -> bool:
"""Whether flash-attn >= 2.6.0 (the first version exposing ``softcap``) is installed."""
try:
import flash_attn # pylint: disable=import-outside-toplevel
except ImportError:
return False
return PkgVersion(flash_attn.__version__) >= PkgVersion("2.6.0")


# Softcapping through DotProductAttention is only wired through the FlashAttention 2
# backend (>= 2.6.0), and requires CUDA tensors.
pytestmark = [
pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA is required."),
pytest.mark.skipif(not _flash_attn_2_6_available(), reason="flash-attn >= 2.6.0 is required."),
]


def _force_flash_backend() -> None:
"""Force DotProductAttention to select the FlashAttention backend."""
import os # pylint: disable=import-outside-toplevel

os.environ["NVTE_FLASH_ATTN"] = "1"
os.environ["NVTE_FUSED_ATTN"] = "0"
os.environ["NVTE_UNFUSED_ATTN"] = "0"
_attention_backends["backend_selection_requires_update"] = True


def _reference_attention(q, k, v, scale, softcap, causal):
"""Pure-PyTorch reference for softcapped scaled dot product attention.

q, k, v are in ``bshd`` layout. GQA is supported: ``k``/``v`` may have fewer
heads than ``q``.
"""
# bshd -> bhsd
qt = q.transpose(1, 2).float()
kt = k.transpose(1, 2).float()
vt = v.transpose(1, 2).float()

num_heads = qt.shape[1]
num_gqa_groups = kt.shape[1]
if num_heads != num_gqa_groups:
assert num_heads % num_gqa_groups == 0
repeats = num_heads // num_gqa_groups
kt = kt.repeat_interleave(repeats, dim=1)
vt = vt.repeat_interleave(repeats, dim=1)

scores = torch.matmul(qt, kt.transpose(-2, -1)) * scale
if softcap != 0.0:
scores = softcap * torch.tanh(scores / softcap)
if causal:
sq, skv = scores.shape[-2], scores.shape[-1]
mask = torch.triu(
torch.ones(sq, skv, dtype=torch.bool, device=scores.device),
diagonal=1 + skv - sq,
)
scores = scores.masked_fill(mask, float("-inf"))
attn = torch.softmax(scores, dim=-1)
out = torch.matmul(attn, vt)
# bhsd -> bshd
return out.transpose(1, 2)


@pytest.mark.parametrize("dtype", [torch.float16, torch.bfloat16])
@pytest.mark.parametrize("softcap", [0.0, 50.0])
@pytest.mark.parametrize("num_gqa_groups", [4, 2])
@pytest.mark.parametrize("causal", [False, True])
def test_softcap_numerics(dtype, softcap, num_gqa_groups, causal):
"""FlashAttention softcap forward + grads match a pure-PyTorch reference.

``softcap == 0.0`` additionally proves that softcapping is a no-op relative to
the plain (no-softcap) reference, i.e. today's behavior is reproduced exactly.
"""
reset_rng_states()

batch_size = 2
max_seqlen = 32
num_heads = 4
head_dim = 64
scale = 1.0 / (head_dim**0.5)

q_shape = (batch_size, max_seqlen, num_heads, head_dim)
kv_shape = (batch_size, max_seqlen, num_gqa_groups, head_dim)

q = (0.5 * torch.randn(q_shape, dtype=dtype, device="cuda")).requires_grad_()
k = (0.5 * torch.randn(kv_shape, dtype=dtype, device="cuda")).requires_grad_()
v = (0.5 * torch.randn(kv_shape, dtype=dtype, device="cuda")).requires_grad_()
Comment thread
nvegesna-netizen marked this conversation as resolved.
q_ref, k_ref, v_ref = [x.detach().clone().requires_grad_() for x in (q, k, v)]

grad_output = torch.randn(q_shape, dtype=dtype, device="cuda")

_force_flash_backend()
dpa = DotProductAttention(
num_heads,
head_dim,
num_gqa_groups=num_gqa_groups,
qkv_format="bshd",
attn_mask_type="causal" if causal else "no_mask",
softmax_scale=scale,
softcap=softcap,
layer_number=1,
).to(dtype=dtype, device="cuda")

out = dpa(q, k, v)
out.backward(grad_output)

out_ref = _reference_attention(q_ref, k_ref, v_ref, scale, softcap, causal)
out_ref.backward(grad_output.float())

atol, rtol = (2e-2, 2e-2) if dtype == torch.float16 else (3.5e-2, 3.5e-2)

torch.testing.assert_close(out.float(), out_ref.float(), atol=atol, rtol=rtol)
torch.testing.assert_close(q.grad.float(), q_ref.grad.float(), atol=atol, rtol=rtol)
torch.testing.assert_close(k.grad.float(), k_ref.grad.float(), atol=atol, rtol=rtol)
torch.testing.assert_close(v.grad.float(), v_ref.grad.float(), atol=atol, rtol=rtol)
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 @@ -165,6 +166,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 opt-in (NVTE_FA3_SOFTCAP) + head_dim <= 256 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 @@ -885,6 +899,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 @@ -1100,6 +1115,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 @@ -1130,6 +1150,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 @@ -1215,6 +1236,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 @@ -1235,9 +1258,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) + opt-in (NVTE_FA3_SOFTCAP) + 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 @@ -1387,6 +1387,7 @@ def forward(
deterministic,
use_fused_attention,
return_max_logit,
softcap,
fp8,
fp8_meta,
cp_group,
Expand Down Expand Up @@ -1664,7 +1665,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 @@ -2156,6 +2157,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 @@ -2454,7 +2456,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 @@ -2970,6 +2972,7 @@ def backward(ctx, dout, *_args):
None,
None,
None,
None,
)


Expand Down Expand Up @@ -3047,6 +3050,7 @@ def forward(
deterministic,
use_fused_attention,
return_max_logit,
softcap,
window_size,
cp_group,
cp_stream,
Expand Down Expand Up @@ -3128,7 +3132,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 @@ -3644,6 +3648,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.pad_between_seqs = pad_between_seqs
Expand Down Expand Up @@ -3840,7 +3845,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

local_seq_chunk_ids = [rank, 2 * cp_size - rank - 1]
for i in range(len(local_seq_chunk_ids) + 1):
Expand Down Expand Up @@ -4164,6 +4169,7 @@ def backward(ctx, dout, *_args):
None,
None,
None,
None,
)


Expand Down Expand Up @@ -4195,6 +4201,7 @@ def forward(
deterministic,
use_fused_attention,
return_max_logit,
softcap,
window_size,
fp8,
fp8_meta,
Expand Down Expand Up @@ -4284,7 +4291,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 @@ -4585,6 +4592,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 @@ -4725,7 +4733,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 @@ -4916,6 +4924,7 @@ def backward(ctx, dout, *_args):
None,
None,
None,
None,
d_softmax_offset,
None,
)
Expand Down Expand Up @@ -4945,6 +4954,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 @@ -5091,6 +5101,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