Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions python/paddle/optimizer/muon.py
Original file line number Diff line number Diff line change
Expand Up @@ -606,6 +606,9 @@ def ortho_fn(m):
self._master_weights[param.name] if find_master else None
)

lr_ratio = 1.0 if self._lr_ratio is None else self._lr_ratio(param)

This comment was marked as outdated.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 优先级:P1
处理要求:请针对该评论修复并提交新的 commit。

这里把 lr_ratio 接入了 Muon 主更新路径,并同时影响 decoupled weight decay 和 final_step,属于优化器数值行为变更。但当前提交只修改了 python/paddle/optimizer/muon.py,仓库里 Muon 相关测试没有任何一处传入 lr_ratio=test/ai_edited_test/test_ai_optimizer_muon.py 也仍未覆盖这条新增路径。后续如果只缩放 final_step、漏掉 weight decay,或把 lr_ratio=None 兼容路径改坏,现有测试发现不了。

请补一个直接覆盖 Muon 2D 参数路径的回归测试,至少验证 lr_ratio=0.0 不更新参数、lr_ratio=0.5 的参数更新量是 lr_ratio=1.0 的一半,并开启 weight_decay > 0 覆盖本行下面的衰减逻辑。测试形态可以参考:

def test_muon_lr_ratio_scales_muon_update(self):
    paddle.disable_static()
    weight_np = np.array([[0.2, -0.4], [0.6, 0.8]], dtype="float32")
    grad_np = np.array([[0.1, 0.3], [-0.2, 0.4]], dtype="float32")

    def run(ratio):
        p = paddle.create_parameter(shape=[2, 2], dtype="float32")
        p.set_value(weight_np)
        p.grad = paddle.to_tensor(grad_np)
        opt = Muon(
            parameters=[p],
            learning_rate=0.01,
            weight_decay=0.01,
            ns_steps=1,
            ns_matmul_dtype=paddle.float32,
            muon_param_info_map={p.name: MuonParamInfo(use_muon=True)},
            lr_ratio=lambda _: ratio,
        )
        opt.step()
        return p.numpy()

    full = run(1.0)
    half = run(0.5)
    zero = run(0.0)

    np.testing.assert_allclose(zero, weight_np, rtol=1e-6, atol=1e-6)
    np.testing.assert_allclose(
        weight_np - half,
        0.5 * (weight_np - full),
        rtol=1e-5,
        atol=1e-6,
    )

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 优先级:P1
处理要求:请针对该评论继续修复并提交新的 commit。

这次提交新增的 test_freeze_parameter 已覆盖 lr_ratio=0.0 冻结路径,并且因为启用了 weight_decay=0.01,也能防止 0 比例时参数被 decoupled weight decay 误衰减;这部分可以认为已补上。

剩下还缺少非零比例的数值回归:当前测试只断言另一个参数“发生了变化”,没有验证 lr_ratio=0.5 的更新量确实是 lr_ratio=1.0 的一半。这样如果后续只对 final_step 缩放、漏掉非零 ratio 下的 weight decay 缩放,或者把非零 ratio 当成 1.0 处理,现有测试仍可能通过。

请继续补齐 0.5 vs 1.0 的比例断言,例如把更新逻辑抽成 helper 后增加类似检查:

full = run(ratio=1.0)
half = run(ratio=0.5)

np.testing.assert_allclose(
    weight_np - half,
    0.5 * (weight_np - full),
    rtol=1e-5,
    atol=1e-6,
)

effective_lr = lr * lr_ratio

with_decay = True
if (
self._apply_decay_param_fun is not None
Expand All @@ -614,11 +617,11 @@ def ortho_fn(m):
with_decay = False
if with_decay and weight_decay > 0:
if find_master:
master_weight.scale_(1.0 - lr * weight_decay)
master_weight.scale_(1.0 - effective_lr * weight_decay)
else:
param.scale_(1.0 - lr * weight_decay)
param.scale_(1.0 - effective_lr * weight_decay)

final_step = orthogonal_update * lr
final_step = orthogonal_update * effective_lr

if find_master:
master_weight.subtract_(final_step)
Expand Down
65 changes: 65 additions & 0 deletions test/ai_edited_test/test_ai_optimizer_muon.py
Original file line number Diff line number Diff line change
Expand Up @@ -274,5 +274,70 @@ def test_zero_steps(self):
self.assertEqual(result.shape, [4, 4])


class TestMuonLrRatio(unittest.TestCase):
"""Test lr_ratio in Muon optimizer."""

def test_freeze_parameter(self):
"""Test that lr_ratio=0 freezes the selected Muon parameter."""
paddle.seed(2026)
frozen = paddle.create_parameter(shape=[4, 4], dtype='float32')
trainable = paddle.create_parameter(shape=[4, 4], dtype='float32')

optimizer = Muon(
learning_rate=0.02,
parameters=[frozen, trainable],
lr_ratio=lambda param: 0.0 if param.name == frozen.name else 1.0,
weight_decay=0.01,
muon_param_info_map={
frozen.name: MuonParamInfo(use_muon=True),
trainable.name: MuonParamInfo(use_muon=True),
},
ns_matmul_dtype=paddle.float32,
)

frozen_before = frozen.numpy().copy()
trainable_before = trainable.numpy().copy()
loss = frozen.sum() + trainable.sum()
loss.backward()
optimizer.step()

np.testing.assert_array_equal(frozen.numpy(), frozen_before)
self.assertFalse(np.array_equal(trainable.numpy(), trainable_before))

def test_muon_lr_ratio_scales_muon_update(self):
"""lr_ratio=0.0 freezes the param; lr_ratio=0.5 update is half of lr_ratio=1.0."""
paddle.disable_static()
weight_np = np.array([[0.2, -0.4], [0.6, 0.8]], dtype="float32")
grad_np = np.array([[0.1, 0.3], [-0.2, 0.4]], dtype="float32")

def run(ratio):
p = paddle.create_parameter(shape=[2, 2], dtype="float32")
p.set_value(weight_np)
p.grad = paddle.to_tensor(grad_np)
opt = Muon(
parameters=[p],
learning_rate=0.01,
weight_decay=0.01,
ns_steps=1,
ns_matmul_dtype=paddle.float32,
muon_param_info_map={p.name: MuonParamInfo(use_muon=True)},
lr_ratio=lambda _: ratio,
)
opt.step()
return p.numpy()

full = run(1.0)
half = run(0.5)
zero = run(0.0)

np.testing.assert_allclose(zero, weight_np, rtol=1e-6, atol=1e-6)
np.testing.assert_allclose(
weight_np - half,
0.5 * (weight_np - full),
rtol=1e-5,
atol=1e-6,
)


if __name__ == '__main__':
unittest.main()
Loading