Skip to content

Commit b752897

Browse files
committed
Support learnable softmax (attention sink) with cp_comm_type="p2p"
The p2p fused-attention helpers never passed softmax_type to fused_attn_fwd or fused_attn_bwd, so the kernels fell back to their "vanilla" default and discarded softmax_offset on the floor. Nothing failed loudly: the forward returned the vanilla answer and the sink gradient came back as an exact zero. The a2a path has always passed it; p2p never did. Thread softmax_type through both helpers, add the offset to the auxiliary tensor pack in the order the forward returns it, and accumulate the per-tile sink gradient into the p2p backward's return tuple. The offset is replicated across CP ranks and every rank computes all query rows, so each already holds the full gradient and the outer framework's DDP average reproduces it; no extra reduction is needed here. The aux pack is [stats, rng, (bias), (offset)], so the previously unconditional attn_bias = rest[0] is now guarded -- otherwise it would hand the offset back as the attention bias once a sink is active. Also bind d_sink_ before the section dispatch: it is assigned only in the fused backward branch, so a vanilla run on the flash backend would have raised UnboundLocalError at the accumulation. Verified against an FP64 oracle at cp=2, BF16, 1024 tokens: the p2p forward matches to 9.5e-3 relative error and dSink to 1.7e-3, on par with the already-supported a2a path. The oracle's own autograd and finite-difference answers agree to 2.3e-08. Not covered: qkv_format="thd", cp_size > 2, and the flash backend. Signed-off-by: 0z5a <dezhen.lu@student.uni-tuebingen.de>
1 parent 699ed6e commit b752897

2 files changed

Lines changed: 54 additions & 11 deletions

File tree

‎transformer_engine/pytorch/attention/dot_product_attention/context_parallel.py‎

Lines changed: 53 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -994,6 +994,8 @@ def cp_p2p_fwd_fused_attn(
994994
step,
995995
cp_size,
996996
return_max_logit,
997+
softmax_type,
998+
softmax_offset,
997999
q_part,
9981000
k_part,
9991001
v_part,
@@ -1110,6 +1112,8 @@ def cp_p2p_fwd_fused_attn(
11101112
cu_seqlens_kv_padded=cu_seqlens_kv_padded_,
11111113
**fp8_meta_kwargs,
11121114
return_max_logit=return_max_logit,
1115+
softmax_type=softmax_type,
1116+
softmax_offset=softmax_offset,
11131117
cuda_graph=is_graph_capturing(),
11141118
qkv_scale_inv_format=qkv_scale_inv_format,
11151119
)
@@ -1118,7 +1122,10 @@ def cp_p2p_fwd_fused_attn(
11181122
softmax_lse_per_step, rng_states = aux_ctx_tensors
11191123
else:
11201124
softmax_lse_per_step, rng_states, *rest = aux_ctx_tensors
1121-
attn_bias = rest[0] if len(rest) > 0 else None
1125+
# The aux pack is [stats, rng, (bias), (offset)], so an unconditional
1126+
# rest[0] would return the offset as the bias once a sink is active.
1127+
has_attn_bias = attn_bias_type not in ["no_bias", "alibi"] and attn_bias_inputs is not None
1128+
attn_bias = rest[0] if has_attn_bias else None
11221129

11231130
if return_max_logit:
11241131
return out_per_step, softmax_lse_per_step, rng_states, attn_bias, *max_logit
@@ -1314,6 +1321,8 @@ def cp_p2p_bwd_fused_attn(
13141321
softmax_lse,
13151322
softmax_lse_,
13161323
rng_states,
1324+
softmax_type,
1325+
softmax_offset,
13171326
attn_dbias,
13181327
attn_biases,
13191328
max_seqlen_q,
@@ -1374,6 +1383,11 @@ def cp_p2p_bwd_fused_attn(
13741383
if attn_dbias is not None:
13751384
aux_tensors += [attn_biases[cp_size - step - 1]]
13761385

1386+
# Aux pack order is [softmax_stats, rng_state, (bias), (softmax_offset)],
1387+
# matching what the forward returned.
1388+
if softmax_offset is not None:
1389+
aux_tensors += [softmax_offset]
1390+
13771391
fp8_meta_kwargs = {}
13781392
qkv_scale_inv_format = None
13791393
do_scale_inv_format = None
@@ -1410,7 +1424,7 @@ def cp_p2p_bwd_fused_attn(
14101424
fp8_meta_kwargs["dp_quantizer"] = dP_quantizer_per_step
14111425
fp8_meta_kwargs["dqkv_quantizer"] = dQKV_quantizer_per_step
14121426

1413-
dq, dk, dv, dbias, *_ = fused_attn_bwd(
1427+
fused_bwd_out = fused_attn_bwd(
14141428
max_seqlen_q_,
14151429
max_seqlen_kv_,
14161430
cu_seqlens_q_per_step[cp_size - step - 1],
@@ -1433,14 +1447,16 @@ def cp_p2p_bwd_fused_attn(
14331447
dqkv_layout=dqkv_layout,
14341448
attn_mask_type=attn_mask_type_,
14351449
attn_bias_type=attn_bias_type,
1450+
softmax_type=softmax_type,
14361451
deterministic=deterministic,
14371452
cuda_graph=is_graph_capturing(),
14381453
qkv_scale_inv_format=qkv_scale_inv_format,
14391454
do_scale_inv_format=do_scale_inv_format,
14401455
**fp8_meta_kwargs,
14411456
)
14421457

1443-
return dq, dk, dv, dbias
1458+
dq, dk, dv, dbias, d_softmax_offset = fused_bwd_out
1459+
return dq, dk, dv, dbias, d_softmax_offset
14441460

14451461

14461462
def cp_p2p_bwd_flash_attn(
@@ -1618,6 +1634,7 @@ def forward(
16181634
use_fused_attention,
16191635
return_max_logit,
16201636
softcap,
1637+
softmax_offset,
16211638
fp8,
16221639
fp8_meta,
16231640
cp_group,
@@ -1629,6 +1646,7 @@ def forward(
16291646
use_flash_attn_4,
16301647
fp8_output,
16311648
layer_number,
1649+
softmax_type,
16321650
):
16331651
# pylint: disable=missing-function-docstring
16341652

@@ -2007,6 +2025,8 @@ def forward(
20072025
i,
20082026
cp_size,
20092027
return_max_logit,
2028+
softmax_type,
2029+
softmax_offset,
20102030
]
20112031
else:
20122032
flash_attn_inputs = [
@@ -2396,6 +2416,8 @@ def forward(
23962416
ctx.max_seqlen_q = max_seqlen_q
23972417
ctx.max_seqlen_kv = max_seqlen_kv
23982418
ctx.softmax_scale = softmax_scale
2419+
ctx.softmax_offset = softmax_offset
2420+
ctx.softmax_type = softmax_type
23992421
ctx.attn_mask_type = attn_mask_type
24002422
ctx.attn_bias_type = attn_bias_type
24012423
ctx.attn_bias_shape = None if attn_bias is None else attn_bias.shape
@@ -2520,6 +2542,12 @@ def backward(ctx, dout, *_args):
25202542
attn_dbias = None
25212543
attn_dbias_ = None
25222544

2545+
# Sink gradient accumulator. The offset is replicated on every rank and every
2546+
# per-tile fused call sees the same tensor, so tile contributions sum.
2547+
d_softmax_offset = (
2548+
torch.zeros_like(ctx.softmax_offset) if ctx.softmax_offset is not None else None
2549+
)
2550+
25232551
# set up softmax_lse
25242552
softmax_lse_ = None
25252553
if causal and ctx.second_half_lse_seqlen is not None:
@@ -2779,6 +2807,8 @@ def backward(ctx, dout, *_args):
27792807
softmax_lse,
27802808
softmax_lse_,
27812809
rng_states,
2810+
ctx.softmax_type,
2811+
ctx.softmax_offset,
27822812
attn_dbias,
27832813
attn_biases,
27842814
ctx.max_seqlen_q,
@@ -2831,12 +2861,13 @@ def backward(ctx, dout, *_args):
28312861
# Reverse the steps in forward. In the cp_size x cp_size (i.e. GPU x step) matrix,
28322862
# there are still three sections in these tiles based on their attention pattern
28332863
# for attn_mask_type = causal, and one for attn_mask_type != causal.
2864+
d_sink_ = None
28342865
if causal:
28352866
if i == (cp_size - 1):
28362867
section = "diagonal"
28372868
prepare_outputs = cp_p2p_bwd_prepare_qkv(*prepare_inputs, section)
28382869
if ctx.use_fused_attention:
2839-
dq_, dk_, dv_, dbias_ = cp_p2p_bwd_fused_attn(
2870+
dq_, dk_, dv_, dbias_, d_sink_ = cp_p2p_bwd_fused_attn(
28402871
*fused_attn_inputs, *prepare_outputs, section
28412872
)
28422873
else:
@@ -2849,7 +2880,7 @@ def backward(ctx, dout, *_args):
28492880
section = "lower-triangle"
28502881
prepare_outputs = cp_p2p_bwd_prepare_qkv(*prepare_inputs, section)
28512882
if ctx.use_fused_attention:
2852-
dq_, dk_, dv_, dbias_ = cp_p2p_bwd_fused_attn(
2883+
dq_, dk_, dv_, dbias_, d_sink_ = cp_p2p_bwd_fused_attn(
28532884
*fused_attn_inputs, *prepare_outputs, section
28542885
)
28552886
else:
@@ -2862,7 +2893,7 @@ def backward(ctx, dout, *_args):
28622893
section = "upper-triangle"
28632894
prepare_outputs = cp_p2p_bwd_prepare_qkv(*prepare_inputs, section)
28642895
if ctx.use_fused_attention:
2865-
dq_, dk_, dv_, dbias_ = cp_p2p_bwd_fused_attn(
2896+
dq_, dk_, dv_, dbias_, d_sink_ = cp_p2p_bwd_fused_attn(
28662897
*fused_attn_inputs, *prepare_outputs, section
28672898
)
28682899
else:
@@ -2875,7 +2906,7 @@ def backward(ctx, dout, *_args):
28752906
section = "all"
28762907
prepare_outputs = cp_p2p_bwd_prepare_qkv(*prepare_inputs, section)
28772908
if ctx.use_fused_attention:
2878-
dq_, dk_, dv_, dbias_ = cp_p2p_bwd_fused_attn(
2909+
dq_, dk_, dv_, dbias_, d_sink_ = cp_p2p_bwd_fused_attn(
28792910
*fused_attn_inputs, *prepare_outputs, section
28802911
)
28812912
else:
@@ -2885,6 +2916,13 @@ def backward(ctx, dout, *_args):
28852916
section,
28862917
)
28872918

2919+
# Accumulate this tile's sink gradient. Every rank computes all query rows,
2920+
# so each already holds the full gradient for this replicated parameter and
2921+
# the outer framework's DDP average reproduces it; reducing here as well
2922+
# would double-count.
2923+
if d_sink_ is not None:
2924+
d_softmax_offset += d_sink_
2925+
28882926
# dq, dk, dv are reduced across steps in higher precision
28892927
# DelayedScaling: collect all results in uint8 to one tensor, dequantize to float32, then reduce
28902928
# CurrentScaling: dequantize partial results from each step to float32, then reduce
@@ -3204,6 +3242,8 @@ def backward(ctx, dout, *_args):
32043242
None,
32053243
None,
32063244
None,
3245+
d_softmax_offset,
3246+
None,
32073247
None,
32083248
None,
32093249
None,
@@ -5715,9 +5755,10 @@ def attn_forward_func_with_cp(
57155755
assert (
57165756
softmax_type == "vanilla" or use_fused_attention
57175757
), f"Context parallelism only supports {softmax_type=} with FusedAttention backend!"
5718-
assert (
5719-
softmax_type == "vanilla" or cp_comm_type == "a2a"
5720-
), f"Context parallelism only supports {softmax_type=} with cp_comm_type = 'a2a'!"
5758+
assert softmax_type == "vanilla" or cp_comm_type in (
5759+
"a2a",
5760+
"p2p",
5761+
), f"Context parallelism supports {softmax_type=} with cp_comm_type in ('a2a', 'p2p')!"
57215762
if get_cudnn_version() < (9, 18, 0):
57225763
assert softmax_type == "vanilla" or qkv_format != "thd", (
57235764
f"Before cuDNN 9.18.0, context parallelism does not support {softmax_type=} with"
@@ -5749,6 +5790,7 @@ def attn_forward_func_with_cp(
57495790

57505791
if cp_comm_type in ["p2p", "a2a+p2p"]:
57515792
args += [
5793+
softmax_offset,
57525794
fp8,
57535795
fp8_meta,
57545796
cp_group,
@@ -5760,6 +5802,7 @@ def attn_forward_func_with_cp(
57605802
use_flash_attn_4,
57615803
fp8_output,
57625804
layer_number,
5805+
softmax_type,
57635806
]
57645807
out = AttnFuncWithCPAndKVP2P.apply(*args)
57655808
elif cp_comm_type == "all_gather":

‎transformer_engine/pytorch/attention/dot_product_attention/utils.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1245,7 +1245,7 @@ def _is_fa3_supported(num_heads, num_gqa_groups, head_dim_qk, head_dim_v, qkv_dt
12451245
softmax_type,
12461246
)
12471247
use_fused_attention = False
1248-
if context_parallel and cp_comm_type != "a2a":
1248+
if context_parallel and cp_comm_type not in ("a2a", "p2p"):
12491249
logger.debug(
12501250
"Disabling FusedAttention for context parallelism with softmax_type = %s and"
12511251
" cp_comm_type = %s",

0 commit comments

Comments
 (0)