Skip to content

Commit 9ff49d7

Browse files
fix(gdn): make fused gate backward gradients repeatable
Signed-off-by: Pinoeer-kingxi <13022943007@163.com>
1 parent 1e0bb06 commit 9ff49d7

3 files changed

Lines changed: 114 additions & 28 deletions

File tree

‎megatron/core/fusions/fused_pre_gated_delta_rule.py‎

Lines changed: 21 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -672,14 +672,6 @@ def _z_layout_to_qkvzba_kernel(
672672
tl.store(d_qkvzba_ptrs, dgate_val, mask=s_mask[:, None])
673673

674674

675-
@triton.autotune(
676-
configs=_g_beta_autotune_configs(),
677-
key=["seq_len", "num_v_heads"],
678-
# Each autotune trial atomic-adds partial sums into these accumulators.
679-
# Without reset_to_zero the trials would stack on top of one another and
680-
# produce values that are ``num_trials`` × the correct result.
681-
reset_to_zero=["d_A_log_ptr", "d_dt_bias_ptr"],
682-
)
683675
@triton.jit
684676
def _g_beta_backward_kernel(
685677
qkvzba_ptr,
@@ -721,8 +713,8 @@ def _g_beta_backward_kernel(
721713
d_A_log[h] = Σ_{b,s} d_g[b,s,h] * g[b,s,h]
722714
723715
``d_alpha`` and ``d_beta_raw`` are written into the matching channel slices
724-
of ``d_qkvzba``. ``d_A_log`` and ``d_dt_bias`` are reduced via per-element
725-
atomic_add to fp32 buffers; the caller casts those to the parameter dtype.
716+
of ``d_qkvzba``. Each program writes its parameter-gradient partial sums
717+
to a separate FP32 slot; the caller reduces them in a fixed order.
726718
"""
727719

728720
pid_b = tl.program_id(0)
@@ -802,9 +794,10 @@ def _g_beta_backward_kernel(
802794
tl.store(d_alpha_ptrs, d_alpha.to(d_qkvzba_ptr.dtype.element_ty), mask=mask)
803795
tl.store(d_beta_ptrs, d_beta_raw.to(d_qkvzba_ptr.dtype.element_ty), mask=mask)
804796

805-
# ----- Atomic-add (b, s) partials into per-head accumulators -----
806-
tl.atomic_add(d_A_log_ptr + h_offs, d_A_log_partial, mask=h_mask)
807-
tl.atomic_add(d_dt_bias_ptr + h_offs, d_dt_bias_partial, mask=h_mask)
797+
# ----- Store one partial per (batch, sequence block, head) -----
798+
partial_offset = (pid_b * tl.num_programs(1) + pid_s) * num_v_heads + h_offs
799+
tl.store(d_A_log_ptr + partial_offset, d_A_log_partial, mask=h_mask)
800+
tl.store(d_dt_bias_ptr + partial_offset, d_dt_bias_partial, mask=h_mask)
808801

809802

810803
# ---------------------------------------------------------------------------
@@ -996,25 +989,23 @@ def _triton_g_beta_backward(
996989

997990
device = qkvzba.device
998991

999-
g_beta_grid = lambda meta: (
1000-
batch,
1001-
triton.cdiv(seq_len, meta["BLOCK_S"]),
1002-
triton.cdiv(num_value_heads, meta["BLOCK_H"]),
1003-
)
992+
# Fixed tiles keep both the partial sums and their final reduction repeatable.
993+
block_s, block_h = 128, 16
994+
sequence_blocks = triton.cdiv(seq_len, block_s)
995+
g_beta_grid = (batch, sequence_blocks, triton.cdiv(num_value_heads, block_h))
1004996
with _launch_context(device, stream):
1005-
d_param_grads = torch.empty((2, num_value_heads), dtype=torch.float32, device=device)
1006-
d_param_grads.zero_()
1007-
d_A_log = d_param_grads[0]
1008-
d_dt_bias = d_param_grads[1]
997+
d_param_grads = torch.empty(
998+
(2, batch, sequence_blocks, num_value_heads), dtype=torch.float32, device=device
999+
)
10091000
_g_beta_backward_kernel[g_beta_grid](
10101001
qkvzba,
10111002
A_log,
10121003
dt_bias,
10131004
d_g,
10141005
d_beta_out,
10151006
d_qkvzba_out,
1016-
d_A_log,
1017-
d_dt_bias,
1007+
d_param_grads[0],
1008+
d_param_grads[1],
10181009
seq_len,
10191010
num_value_heads,
10201011
beta_channel_offset,
@@ -1028,8 +1019,13 @@ def _triton_g_beta_backward(
10281019
d_beta_out.stride(0),
10291020
d_beta_out.stride(1),
10301021
d_beta_out.stride(2),
1022+
BLOCK_S=block_s,
1023+
BLOCK_H=block_h,
1024+
num_warps=4,
1025+
num_stages=2,
10311026
)
1032-
return d_qkvzba_out, d_A_log, d_dt_bias
1027+
d_param_grads = d_param_grads.sum(dim=(1, 2))
1028+
return d_qkvzba_out, d_param_grads[0], d_param_grads[1]
10331029

10341030

10351031
def _launch_context(device: torch.device, stream: Optional["torch.cuda.Stream"]):

‎tests/unit_tests/determinism/kernels/manifest.py‎

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -480,9 +480,18 @@ class KernelEntry:
480480
name="gdn_pre_gated_delta_rule_fusion",
481481
sources=("megatron/core/fusions/fused_pre_gated_delta_rule.py",),
482482
kind="triton",
483-
exempt_reason="Pre-GDR fusion is explicitly rejected with deterministic_mode=True. "
484-
"Its backward uses atomic parameter-gradient reductions and timing-based autotuning; "
485-
"numerical parity and the rejection guard are covered by the GDN fusion unit tests.",
483+
exempt_reason="Full pre-GDR fusion still rejects deterministic_mode=True. "
484+
"Other kernels use timing-based autotuning and convolution backward needs its external "
485+
"determinism setting. Gate backward is tested separately; full-path numerical parity "
486+
"and the rejection guard remain covered by the GDN fusion unit tests.",
487+
),
488+
KernelEntry(
489+
name="gdn_pre_gated_delta_rule_gate_backward",
490+
sources=("megatron/core/fusions/fused_pre_gated_delta_rule.py",),
491+
tests=(K + "test_gdn_gate_backward.py",),
492+
kind="triton",
493+
notes="Gate backward uses fixed tiles and ordered FP32 parameter-gradient reduction; "
494+
"replay compares input and FP32 parameter gradients before the caller's BF16 cast.",
486495
),
487496
KernelEntry(
488497
name="gdn_gated_output_norm_fusion",
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
# Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
3+
"""Replay the pre-GDR gate gradients before casting away FP32 reduction differences."""
4+
5+
import pytest
6+
import torch
7+
import torch.nn.functional as F
8+
9+
from megatron.core.fusions.fused_pre_gated_delta_rule import _triton_g_beta_backward
10+
from tests.unit_tests.determinism.kernels.harness import assert_replays_bit_exact, seeded
11+
12+
pytestmark = pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA is unavailable")
13+
14+
15+
@pytest.mark.parametrize(
16+
"batch,length,heads", [(1, 33, 8), (2, 2048, 16), (1, 4096, 16), (3, 4103, 24)]
17+
)
18+
@pytest.mark.parametrize("dtype", [torch.bfloat16, torch.float32])
19+
@pytest.mark.parametrize("strided", [False, True])
20+
def test_gate_backward_replays(
21+
batch: int, length: int, heads: int, dtype: torch.dtype, strided: bool
22+
) -> None:
23+
"""Compare all gradients bit-exactly under contention, and against Torch autograd."""
24+
seeded()
25+
key_heads, dim = 4, 32
26+
beta_offset = 2 * key_heads * dim + 2 * heads * dim
27+
channels = beta_offset + 2 * heads
28+
qkvzba = torch.randn(length, batch, channels, dtype=dtype, device="cuda")
29+
A_log = torch.randn(heads, dtype=dtype, device="cuda") * 0.2
30+
dt_bias = torch.randn(heads, dtype=dtype, device="cuda") * 0.2
31+
d_g = torch.randn(batch, length, heads * (2 if strided else 1), device="cuda")
32+
d_beta = torch.randn_like(d_g)
33+
d_g, d_beta = d_g[..., :heads], d_beta[..., :heads]
34+
stream = torch.cuda.Stream() if strided else None
35+
36+
def backward(
37+
qkvzba: torch.Tensor,
38+
A_log: torch.Tensor,
39+
dt_bias: torch.Tensor,
40+
d_g: torch.Tensor,
41+
d_beta: torch.Tensor,
42+
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
43+
result = _triton_g_beta_backward(
44+
qkvzba,
45+
A_log,
46+
dt_bias,
47+
d_g,
48+
d_beta,
49+
num_value_heads=heads,
50+
key_head_dim=dim,
51+
value_head_dim=dim,
52+
num_key_heads=key_heads,
53+
d_qkvzba_out=torch.zeros_like(qkvzba) if strided else None,
54+
stream=stream,
55+
)
56+
if stream is not None:
57+
torch.cuda.current_stream().wait_stream(stream)
58+
return result
59+
60+
outputs, _ = assert_replays_bit_exact(
61+
backward,
62+
(qkvzba, A_log, dt_bias, d_g, d_beta),
63+
backward=False,
64+
replays=5,
65+
contention=True,
66+
what="pre-GDR gate backward",
67+
)
68+
69+
projection = qkvzba.float().detach().requires_grad_()
70+
a = A_log.float().detach().requires_grad_()
71+
bias = dt_bias.float().detach().requires_grad_()
72+
beta = projection[..., beta_offset : beta_offset + heads].permute(1, 0, 2).sigmoid()
73+
alpha = projection[..., beta_offset + heads :].permute(1, 0, 2)
74+
g = -a.exp() * F.softplus(alpha + bias)
75+
reference = torch.autograd.grad((g, beta), (projection, a, bias), (d_g, d_beta))
76+
reference = (reference[0].to(dtype), reference[1], reference[2])
77+
torch.testing.assert_close(outputs["out[0]"], reference[0])
78+
for index in (1, 2):
79+
# Torch's exp/sigmoid approximations and reduction tree differ from
80+
# Triton's. This numerical reference is separate from the byte replay.
81+
torch.testing.assert_close(outputs[f"out[{index}]"], reference[index], atol=1e-4, rtol=2e-5)

0 commit comments

Comments
 (0)