Skip to content

Commit cea0980

Browse files
Fix the batch-invariant RMSNorm forward and its input gradient (#6859)
Signed-off-by: Rui Zhu <rui.zhu.rz399@yale.edu> Co-authored-by: Charlie Truong <chtruong@nvidia.com>
1 parent eb00152 commit cea0980

2 files changed

Lines changed: 57 additions & 5 deletions

File tree

‎megatron/core/transformer/custom_layers/batch_invariant_kernels.py‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1336,13 +1336,14 @@ def forward(ctx, x: torch.Tensor, weight: torch.Tensor, eps: float, zero_centere
13361336
raise RuntimeError("Batch-invariant RMSNorm requires CUDA tensors.")
13371337
if not _is_supported_dtype_for_bik(x.dtype):
13381338
raise RuntimeError(f"Unsupported dtype for batch-invariant RMSNorm: {x.dtype}")
1339-
weight_eff = weight + 1.0 if zero_centered_gamma else weight
13401339

13411340
# We do everything in rmsnorm_batch_invariant manually here so that we can
13421341
# save rsigma in full precision for backward to match the TE behavior.
13431342
x_dtype = x.dtype
13441343
x_fp32 = x.float()
13451344
w_fp32 = weight.to(device=x.device, dtype=torch.float32)
1345+
if zero_centered_gamma:
1346+
w_fp32 = w_fp32 + 1.0
13461347
ms = mean_dim(x_fp32 * x_fp32, dim=-1, keepdim=True)
13471348
rsigma = torch.rsqrt(ms + eps)
13481349
out_fp32 = (x_fp32 * rsigma) * w_fp32
@@ -1364,11 +1365,11 @@ def backward(ctx, grad_output: torch.Tensor):
13641365
accumulation and reduction behavior for numerical stability.
13651366
"""
13661367
x, weight, rsigma = ctx.saved_tensors
1367-
w_eff = (weight + 1.0) if ctx.zero_centered_gamma else weight
1368-
13691368
go_fp32 = grad_output.float()
13701369
x_fp32 = x.float()
1371-
w_fp32 = w_eff.to(device=x.device, dtype=torch.float32)
1370+
w_fp32 = weight.to(device=x.device, dtype=torch.float32)
1371+
if ctx.zero_centered_gamma:
1372+
w_fp32 = w_fp32 + 1.0
13721373
r = rsigma
13731374
r3 = r * r * r
13741375
D = x.shape[-1]
@@ -1377,7 +1378,7 @@ def backward(ctx, grad_output: torch.Tensor):
13771378
g_w = (go_fp32 * x_fp32 * r).sum(dim=red_dims).to(weight.dtype)
13781379

13791380
s = (go_fp32 * x_fp32 * w_fp32).sum(dim=-1, keepdim=True)
1380-
dx = go_fp32 * (w_fp32 * r) - (w_fp32 * r3) * (s * x_fp32) / D
1381+
dx = go_fp32 * (w_fp32 * r) - r3 * (s * x_fp32) / D
13811382
dx = dx.to(x.dtype)
13821383

13831384
return dx, g_w, None, None

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

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -618,6 +618,57 @@ def test_batch_invariant_kernels_replay():
618618
)
619619

620620

621+
@pytest.mark.parametrize("zero_centered_gamma", [False, True], ids=["gamma", "zero_centered_gamma"])
622+
def test_batch_invariant_rmsnorm_replays_and_matches_reference(zero_centered_gamma):
623+
"""Replay the batch-invariant RMSNorm autograd function and check it against fp32 torch.
624+
625+
The weight is random on purpose. At ``weight == 1`` the input gradient with the weight
626+
factor applied twice equals the correct one, and under zero-centred gamma a forward that
627+
forgets the ``+ 1`` returns exactly zero at ``weight == 0`` -- so either default init hides
628+
one of the two defects this function has had. Only a non-unit, non-zero weight sees both.
629+
"""
630+
from megatron.core.transformer.custom_layers.batch_invariant_kernels import (
631+
BatchInvariantRMSNormFn,
632+
)
633+
634+
seeded()
635+
rows, hidden, eps = 4096, 2048, 1e-5
636+
x = torch.randn(rows, hidden, device="cuda", dtype=torch.bfloat16)
637+
weight = torch.randn(hidden, device="cuda", dtype=torch.bfloat16)
638+
grad_output = torch.randn(rows, hidden, device="cuda", dtype=torch.bfloat16)
639+
640+
def run(x, weight):
641+
return BatchInvariantRMSNormFn.apply(x, weight, eps, zero_centered_gamma)
642+
643+
assert_replays_bit_exact(
644+
run,
645+
(x.clone().requires_grad_(True), weight.clone().requires_grad_(True)),
646+
replays=3,
647+
grad_outputs={"out": grad_output},
648+
what=f"bik rmsnorm[zero_centered_gamma={zero_centered_gamma}]",
649+
)
650+
651+
# fp32 reference, the same arithmetic TE's RMSNorm defines. Its gradients come from
652+
# autograd, so the two defects (a dropped ``+ 1`` in forward, a duplicated weight factor in
653+
# the input gradient) each show up as a mismatch here and nowhere in the replay above.
654+
x_bik = x.clone().requires_grad_(True)
655+
w_bik = weight.clone().requires_grad_(True)
656+
out_bik = BatchInvariantRMSNormFn.apply(x_bik, w_bik, eps, zero_centered_gamma)
657+
out_bik.backward(grad_output)
658+
659+
x_ref = x.float().requires_grad_(True)
660+
w_ref = weight.float().requires_grad_(True)
661+
w_eff = w_ref + 1.0 if zero_centered_gamma else w_ref
662+
out_ref = x_ref * torch.rsqrt((x_ref * x_ref).mean(dim=-1, keepdim=True) + eps) * w_eff
663+
out_ref.backward(grad_output.float())
664+
665+
# The function computes in fp32 and rounds once to the input dtype, so compare after the
666+
# same rounding of the reference; the only remaining difference is reduction order.
667+
torch.testing.assert_close(out_bik, out_ref.to(out_bik.dtype))
668+
torch.testing.assert_close(x_bik.grad, x_ref.grad.to(x_bik.dtype))
669+
torch.testing.assert_close(w_bik.grad, w_ref.grad.to(w_bik.dtype))
670+
671+
621672
def test_mask_routing_padding_replays():
622673
from megatron.core.transformer.moe.inference_routing_mask_kernel import mask_routing_padding
623674

0 commit comments

Comments
 (0)