Skip to content

Commit 6534c36

Browse files
ds-hwangchanglan
authored andcommitted
Add zero-centered RMSNorm support
GitOrigin-RevId: dd52991
1 parent b5d0fff commit 6534c36

2 files changed

Lines changed: 48 additions & 1 deletion

File tree

axlearn/common/layers.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -352,10 +352,15 @@ def forward(self, x: Tensor, *, paddings: Optional[Tensor] = None) -> Tensor:
352352

353353

354354
class RMSNormStateless(BaseNormalizationLayer):
355-
"""Stateless version of https://github.com/bzhangGo/rmsnorm."""
355+
"""Stateless version of https://github.com/bzhangGo/rmsnorm.
356+
357+
Paper: https://arxiv.org/abs/1910.07467
358+
"""
356359

357360
@config_class
358361
class Config(BaseNormalizationLayer.Config):
362+
"""RMSNormStateless.Config."""
363+
359364
# The epsilon.
360365
eps: float = 1e-8
361366
# Cast input to this dtype for the 'forward' call. If None, do not cast.
@@ -364,14 +369,27 @@ class Config(BaseNormalizationLayer.Config):
364369
input_partition_spec: Optional[tuple[Optional[str]]] = None
365370
# If not None, how to partition output activation values.
366371
output_partition_spec: Optional[tuple[Optional[str]]] = None
372+
# If True, center the input by subtracting the mean before RMS normalization.
373+
# This variant is used in Gemma and Qwen3-Next and provides better numerical stability.
374+
zero_centered: Optional[bool] = None
367375

368376
def _forward(self, x: Tensor) -> Tensor:
377+
"""Applies RMS normalization to the input.
378+
379+
Args:
380+
x: Input tensor to normalize.
381+
382+
Returns:
383+
Normalized tensor with the same shape as input.
384+
"""
369385
cfg = self.config
370386
x = maybe_shard(x, cfg.input_partition_spec)
371387
x_dtype = x.dtype
372388
if cfg.forward_dtype is not None:
373389
x = x.astype(cfg.forward_dtype)
374390
moment2 = (x * x).mean(axis=-1, keepdims=True)
391+
if cfg.zero_centered:
392+
x = x - x.mean(axis=-1, keepdims=True)
375393
x = x * jax.lax.rsqrt(moment2 + cfg.eps)
376394
x = x.astype(x_dtype)
377395
return x

axlearn/common/layers_test.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -561,6 +561,35 @@ def test_rms_norm_partition_specs_constraint(self, mock_with_sharding_constraint
561561
self.assertEqual(calls[1].args[0].dtype, jnp.float32)
562562
np.testing.assert_array_equal(calls[1].args[0], outputs)
563563

564+
def test_rms_norm_zero_centered(self):
565+
dim = 6
566+
cfg = RMSNorm.default_config().set(name="norm", input_dim=dim, zero_centered=True)
567+
layer: RMSNorm = cfg.instantiate(parent=None)
568+
prng_key = jax.random.PRNGKey(123)
569+
prng_key, init_key = jax.random.split(prng_key)
570+
layer_params = layer.initialize_parameters_recursively(init_key)
571+
572+
# Random inputs with non-zero mean to test zero-centering.
573+
prng_key, input_key = jax.random.split(prng_key)
574+
inputs = jax.random.normal(input_key, [2, 3, dim]) + 5.0 # Add offset
575+
outputs, _ = F(
576+
layer,
577+
inputs=(inputs,),
578+
is_training=True,
579+
state=layer_params,
580+
prng_key=prng_key,
581+
)
582+
583+
# Manually compute expected output based on zero_centered setting.
584+
moment2 = (inputs * inputs).mean(axis=-1, keepdims=True)
585+
inputs = inputs - inputs.mean(axis=-1, keepdims=True)
586+
expected_normalized = inputs * jax.lax.rsqrt(moment2 + cfg.eps)
587+
588+
expected_outputs = expected_normalized * layer_params["scale"]
589+
self.assertNestedAllClose(outputs, expected_outputs)
590+
output_mean = outputs.mean(axis=-1, keepdims=True)
591+
self.assertNestedAllClose(output_mean, np.zeros_like(output_mean))
592+
564593
def test_l2_norm(self):
565594
cfg = L2Norm.default_config().set(name="norm")
566595
layer: L2Norm = cfg.instantiate(parent=None)

0 commit comments

Comments
 (0)