Skip to content

Commit 4ebce60

Browse files
Fix Polar Express Coefficient Iterator for >8 step polar express (#303)
Signed-off-by: mkhona <mkhona@nvidia.com>
1 parent a93fc65 commit 4ebce60

3 files changed

Lines changed: 74 additions & 21 deletions

File tree

‎emerging_optimizers/orthogonalized_optimizers/muon_utils.py‎

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323

2424
__all__ = ["newton_schulz", "newton_schulz_tp", "NSCoeffT", "get_coefficient_iterator", "CoeffIterMode"]
2525

26-
CoeffIterMode = Literal["cycle", "repeat_last"]
26+
CoeffIterMode = Literal["cycle", "repeat_last", "repeat_penultimate"]
2727

2828
NSCoeffT = Literal["simple", "quintic", "polar_express", "cans", "aol", "deepseekv4", "custom", "cubic5"]
2929

@@ -53,6 +53,9 @@
5353
(2.2779, -1.6198, 0.3985),
5454
(1.8726, -1.2307, 0.3585),
5555
(1.8564, -1.2132, 0.3568),
56+
# Guarded copy of the final tuple: (a / 1.01, b / 1.01**3, c / 1.01**5), rounded to 4 decimals.
57+
# Used for extra iterations before the unguarded final step.
58+
(1.8564, -1.2132, 0.3568),
5659
(1.8750, -1.2500, 0.3750),
5760
],
5861
"cans": [
@@ -89,6 +92,12 @@
8992
],
9093
}
9194

95+
_COEFFICIENT_ITER_MODES: dict[NSCoeffT, CoeffIterMode] = {
96+
"polar_express": "repeat_penultimate",
97+
"cans": "repeat_last",
98+
"deepseekv4": "repeat_last",
99+
}
100+
92101

93102
def get_coefficient_iterator(
94103
steps: int,
@@ -103,12 +112,15 @@ def get_coefficient_iterator(
103112
mode: Iteration mode:
104113
- "cycle": After the last element, restart from the beginning.
105114
- "repeat_last": After the last element, keep yielding the last tuple.
115+
- "repeat_penultimate": Consume the prefix, repeat the penultimate tuple for
116+
extra steps, and finish with the last tuple. Short schedules truncate the prefix.
106117
107118
Yields:
108119
Tuples (a, b, c) from coefficient_sets according to the specified mode.
109120
110121
Raises:
111122
ValueError: If coefficient_sets is empty.
123+
ValueError: If "repeat_penultimate" is used with fewer than two coefficient tuples.
112124
ValueError: If an invalid mode is provided.
113125
"""
114126
logging.debug("Iterating through %s steps with %s mode.", steps, mode)
@@ -123,8 +135,16 @@ def get_coefficient_iterator(
123135
elif mode == "repeat_last":
124136
# Chain the original list with an infinite repeat of the last item
125137
base = chain(coefficient_sets, repeat(coefficient_sets[-1]))
138+
elif mode == "repeat_penultimate":
139+
if len(coefficient_sets) < 2:
140+
raise ValueError("repeat_penultimate requires at least two coefficient tuples.")
141+
base = chain(
142+
coefficient_sets[:-2],
143+
repeat(coefficient_sets[-2], max(0, steps - len(coefficient_sets) + 1)),
144+
coefficient_sets[-1:],
145+
)
126146
else:
127-
raise ValueError(f"Invalid mode: {mode}. Expected 'cycle' or 'repeat_last'.")
147+
raise ValueError(f"Invalid mode: {mode}. Expected 'cycle', 'repeat_last', or 'repeat_penultimate'.")
128148

129149
return islice(base, steps)
130150

@@ -236,8 +256,7 @@ def newton_schulz(
236256
"Use steps=5, or pass explicit custom coefficients."
237257
)
238258

239-
repeat_last_types = ("polar_express", "cans", "deepseekv4")
240-
iter_mode: CoeffIterMode = "repeat_last" if coefficient_type in repeat_last_types else "cycle"
259+
iter_mode = _COEFFICIENT_ITER_MODES.get(coefficient_type, "cycle")
241260
coeff_iter = get_coefficient_iterator(steps, coefficient_sets, mode=iter_mode)
242261

243262
ns_step_fn = newton_schulz_step if X.ndim == 2 else batched_newton_schulz_step

‎tests/test_distributed_muon_utils_cpu.py‎

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -102,20 +102,26 @@ def test_with_partial_tp(self, shape, tp_size):
102102

103103

104104
class DistributedNewtonSchulzCpuTest(parameterized.TestCase):
105-
@parameterized.parameters(
106-
{"shape": (21, 16)},
107-
{"shape": (16, 32)},
105+
@parameterized.product(
106+
shape=[(21, 16), (16, 32)],
107+
normalize_in_double=[False, True],
108+
scale=[1.0, 1e-30],
108109
)
109-
def test_distributed_normalize_close_to_non_distributed(self, shape):
110-
x = torch.randint(-5, 5, shape, device="cpu", dtype=torch.float32)
110+
def test_distributed_normalize_close_to_non_distributed(self, shape, normalize_in_double, scale):
111+
x = torch.randint(-5, 5, shape, device="cpu", dtype=torch.float32) * scale
111112
torch.distributed.all_reduce(x, op=torch.distributed.ReduceOp.SUM)
112113

113114
world_size = torch.distributed.get_world_size()
114115
rank = torch.distributed.get_rank()
115116
local_x = x.chunk(world_size, dim=1)[rank]
116117

117-
dist_out = muon_utils.distributed_normalize_p2(local_x, eps=1e-7, group=torch.distributed.group.WORLD)
118-
ref_out = torch.nn.functional.normalize(x, dim=(-2, -1), eps=1e-7)
118+
dist_out = muon_utils.distributed_normalize_p2(
119+
local_x, eps=1e-7, group=torch.distributed.group.WORLD, normalize_in_double=normalize_in_double
120+
)
121+
if normalize_in_double:
122+
ref_out = x / torch.linalg.vector_norm(x, dim=(-2, -1), keepdim=True, dtype=torch.float64).to(x.dtype)
123+
else:
124+
ref_out = torch.nn.functional.normalize(x, dim=(-2, -1), eps=1e-7)
119125

120126
torch.testing.assert_close(ref_out.chunk(world_size, dim=1)[rank], dist_out)
121127

‎tests/test_muon_utils.py‎

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -261,19 +261,30 @@ def test_polar_express_16steps_almost_orthogonal(self, size):
261261
out = muon_utils.newton_schulz(x, steps=16, coefficient_type="polar_express")
262262
assert_close_to_orthogonal(out, diag_atol=1e-5, off_diag_atol=1e-5)
263263

264-
@parameterized.parameters(
265-
(511, 513),
266-
(511, 257),
267-
(257, 513),
264+
@parameterized.product(
265+
shape=[(511, 513), (511, 257), (257, 513), (2, 31, 17)],
266+
steps=[0, 1, 7, 8, 9, 10, 16],
268267
)
269-
def test_polar_express_9steps_close_to_reference(self, dim1, dim2):
270-
x = torch.randn(dim1, dim2, device=self.device, dtype=torch.float32)
271-
out_pe9 = muon_utils.newton_schulz(x, steps=9, coefficient_type="polar_express")
268+
def test_polar_express_close_to_reference(self, shape, steps):
269+
x = torch.randn(*shape, device=self.device, dtype=torch.float32)
270+
out_pe = muon_utils.newton_schulz(x, steps=steps, coefficient_type="polar_express")
272271

273-
coeff = deepcopy(muon_utils._COEFFICIENT_SETS["polar_express"])
274-
coeff.append(coeff[-1])
272+
coeff = list(
273+
muon_utils.get_coefficient_iterator(
274+
steps, muon_utils._COEFFICIENT_SETS["polar_express"], mode="repeat_penultimate"
275+
)
276+
)
275277
out_ref = newton_schulz_ref(x, coefficient_sets=coeff)
276-
torch.testing.assert_close(out_pe9, out_ref, atol=2e-6, rtol=1e-7)
278+
torch.testing.assert_close(out_pe, out_ref, atol=2e-6, rtol=1e-7)
279+
280+
def test_polar_express_guarded_coefficient_matches_rounded_safety_factor(self):
281+
coeff = muon_utils._COEFFICIENT_SETS["polar_express"]
282+
a, b, c = coeff[-1]
283+
expected = torch.tensor(
284+
(round(a / 1.01, 4), round(b / 1.01**3, 4), round(c / 1.01**5, 4)), dtype=torch.float32
285+
)
286+
actual = torch.tensor(coeff[-2], dtype=torch.float32)
287+
torch.testing.assert_close(actual, expected, atol=0, rtol=0)
277288

278289
@parameterized.parameters(
279290
(511, 513),
@@ -428,6 +439,23 @@ def test_get_coefficient_iterator_empty_raises_value_error(self) -> None:
428439
with self.assertRaisesRegex(ValueError, "must be non-empty"):
429440
list(muon_utils.get_coefficient_iterator(5, []))
430441

442+
@parameterized.named_parameters(
443+
("zero_steps", 0, []),
444+
("truncated_prefix", 1, [0]),
445+
("no_repetition", 2, [0, 2]),
446+
("full_schedule", 3, [0, 1, 2]),
447+
("one_extra_step", 4, [0, 1, 1, 2]),
448+
("several_extra_steps", 7, [0, 1, 1, 1, 1, 1, 2]),
449+
)
450+
def test_get_coefficient_iterator_repeat_penultimate(self, steps, expected_indices):
451+
coeff = [(1.0, 2.0, 3.0), (4.0, 5.0, 6.0), (7.0, 8.0, 9.0)]
452+
actual = list(muon_utils.get_coefficient_iterator(steps, coeff, mode="repeat_penultimate"))
453+
self.assertEqual(actual, [coeff[index] for index in expected_indices])
454+
455+
def test_get_coefficient_iterator_repeat_penultimate_requires_two_coefficients(self):
456+
with self.assertRaisesRegex(ValueError, "requires at least two"):
457+
muon_utils.get_coefficient_iterator(5, [(1.0, 2.0, 3.0)], mode="repeat_penultimate")
458+
431459
def test_get_coefficient_iterator_invalid_mode_raises_value_error(self) -> None:
432460
"""Test that get_coefficient_iterator raises ValueError for invalid mode."""
433461
with self.assertRaisesRegex(ValueError, "Invalid mode.*invalid"):

0 commit comments

Comments
 (0)