Skip to content

[megatron] feat: add Muon optimizer support (expose Megatron-Core TensorParallelMuon) - #7120

Merged
wuxibin89 merged 6 commits into
verl-project:mainfrom
ISEEKYAN:megatron-muon-clean
Jul 30, 2026
Merged

wuxibin89 merged 6 commits into
verl-project:mainfrom
ISEEKYAN:megatron-muon-clean

Conversation

@ISEEKYAN

@ISEEKYAN ISEEKYAN commented Jul 22, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Add Muon optimizer support to verl's native Megatron backend.

verl's native Megatron backend previously supported only AdamW-type optimizers. Megatron-Core ships TensorParallelMuon (via the emerging_optimizers package), but verl did not expose it. This PR wires it through so verl users on the Megatron engine can select Muon.

What it does

  • McoreOptimizerConfig (verl/workers/config/optimizer.py): expose optimizer=muon plus the muon_* hyperparameters, each mirroring the like-named field on Megatron-Core OptimizerConfig.
  • verl/utils/megatron/optimizer.py: forward the muon_* fields to Megatron-Core and route parameters โ€” Muon on 2D weight matrices, AdamW fallback on the rest (embeddings, norms, biases).
  • LayerWise-DDP-aware wiring + master-weight buffer integration: avoids the double-buffer / extra fp32 master clone that would otherwise inflate optimizer memory.
  • Fail-closed: requesting a muon_* field that the installed Megatron-Core build does not declare raises, instead of silently ignoring it.
  • muon_match_adamw_update_rms: opt-in flag deriving muon_extra_scale_factor from sqrt((1 - beta1) / (1 + beta1)), the closed form that analytically matches AdamW's update RMS norm (emerging_optimizers 0.3.0 get_muon_scale_factor docstring; https://kexue.fm/archives/11267; https://arxiv.org/abs/2502.16982). Muon's effective step is lr * muon_extra_scale_factor and the Megatron-Core default 1.0 is not AdamW-comparable, so reusing an AdamW lr unchanged gives a ~4.4x larger effective step. The resolved value is logged on rank 0; the default stays 1.0 so no existing config changes behaviour.

Results

Qwen3-30B-A3B, 2 nodes ร— 8 GPUs (TP=2, PP=1, EP=8, DP=1), matched two-arm comparison (same data / seed / schedule; only the optimizer changes):

Metric Muon AdamW
Parameter update vs reference path bitwise-identical โ€”
Peak optimizer memory (30B SFT, layer-wise) 39.66 GiB 46.63 GiB
Peak optimizer memory (30B GSM8K RL) 30.52 GiB 37.37 GiB
Actor update time (GSM8K RL) 7.50 s 15.30 s
GSM8K RL reward level within run-to-run spread โ€” see below

How these are judged. Two 30-step GSM8K A/Bs were run whose AdamW arms use a
byte-identical configuration, so the pair also measures this workload's run-to-run spread.
Every claim is reported against that spread rather than as a raw difference:

Metric AdamW vs AdamW spread (n = 2) Muon vs AdamW gap ratio verdict
Peak optimizer memory 0.14 GiB (0.4%) 6.85 GiB (18.3%) 49.5x holds
Actor update time 1.89 s (11.6%) 7.80 s (51%) 4.1x holds
Reward, last-3 mean 0.0104 0.0208 (Muon higher) 1.0x within spread
Reward, last-10 mean 0.0234 0.0250 (AdamW higher) 1.1x within spread
End-to-end step time 5.13 s (15.8%) 5.16 s 1.0x within spread
Throughput 35.03 tok/s/GPU (86%) โ€” โ€” not usable

Supported: Muon reduces peak optimizer memory by 18.3%, a gap 49.5x the run-to-run
spread of that metric, while preserving bitwise-correct parameter updates. Its
optimizer-update time is roughly half AdamW's, ~4x the spread โ€” the portion of the step Muon
actually governs.

Not supported: on reward, Muon and AdamW are level within run-to-run spread. The gaps
match the AdamW-vs-AdamW spread in size and the sign flips between metrics (Muon ahead on
the last-3 mean, AdamW ahead on the last-10). An earlier total-gain difference of +0.032 is
likewise inside the 0.023 spread, so the previous "at least matching AdamW" wording is
withdrawn. No hyperparameter was tuned to produce a difference. End-to-end throughput and step
time are dominated by ~2x run-to-run variation in tokens per step and are not usable here.

Scoped to two 30-step short runs; the n = 2 spread estimate is itself crude.

Scale-factor caveat. The RL numbers were obtained with
muon_extra_scale_factor = 0.4227, a constant that came from a mis-calibration and is
not the recommended setting: upstream's often-quoted 0.2 is the value of the
factor, not a target for the measured update RMS. The recommended value is the closed
form sqrt((1 - beta1) / (1 + beta1)), which this PR exposes as the
muon_match_adamw_update_rms switch: it derives the factor from the configured
betas[0] at run time. Configure the switch, not a number -- a hand-entered constant
is the original problem regardless of how it was obtained. (For orientation only, the
expression evaluates to about 0.229 at beta1 = 0.9.) No A/B exists between the old
constant and the derived value
; re-running the comparison is a pre-merge follow-up. The
memory and bitwise results above are independent of this factor.

Provenance of that constant: in this A/B the factor was injected as a literal value,
because the frozen experiment snapshot predates the muon_match_adamw_update_rms switch.
The injected value is bit-identical to what the switch computes: verified independently
with no factor present anywhere in the configuration, the switch resolves to the same
value on both the verl and Megatron-Lite sides, and tracks beta1 rather than being
fixed. The recommended usage is the switch, not a constant.

Compatibility

The optimizer wiring uses Megatron-Core's native get_megatron_optimizer and is independent of the model-build path. The LayerWise-DDP path is validated on both the vanilla Megatron-bridge and the full Megatron-Bridge (non-vanilla) model builds. On the full Megatron-Bridge path (Qwen3-30B-A3B, 12-step check): Muon peak 39.92 GiB vs AdamW 45.40 GiB (Muon saves ~12%), consistent with the vanilla path (39.64 GiB), confirming the layer-wise wiring holds under Megatron-Bridge.

Testing

  • CPU: optimizer-config + wiring unit tests (tests/utils/megatron/, tests/workers/config/).
  • GPU: muon_optim_gpu_smoke.py smoke test.

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@CLAassistant

CLAassistant commented Jul 23, 2026 •

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@ISEEKYAN
ISEEKYAN force-pushed the megatron-muon-clean branch from 434dd82 to 3ef58a3 Compare July 24, 2026 08:06
โ€ฆsorParallelMuon)

verl's native Megatron backend previously supported only AdamW-type
optimizers. Megatron-Core ships TensorParallelMuon (via emerging_optimizers),
but verl did not expose it. This PR wires it through so verl users on the
Megatron engine can select Muon.

What it does
- McoreOptimizerConfig: expose optimizer=muon/adaptive_muon plus the muon_*
  hyperparameters, each mirroring the like-named Megatron-Core OptimizerConfig
  field.
- verl/utils/megatron/optimizer.py: forward the muon_* fields to Megatron-Core
  and route matrix vs non-matrix params (Muon on 2D weights, AdamW fallback on
  the rest).
- LayerWise-DDP-aware wiring + master-weight buffer integration to avoid the
  double-buffer / extra fp32 master clone that would otherwise inflate memory.
- Fail-closed: requesting a muon_* field absent from the installed
  Megatron-Core build raises instead of silently ignoring it.
- CPU wiring/real-config tests + a GPU smoke test.

Tested
- CPU: optimizer config + wiring unit tests.
- GPU (Qwen3-30B-A3B, 2 nodes x 8 GPUs): bitwise-identical parameter updates
  vs the reference path; Muon peak memory 39.66 GiB < AdamW 46.63 GiB; a short
  GSM8K RL A/B shows Muon reward increasing and its convergence at least
  matching AdamW.
@ISEEKYAN
ISEEKYAN force-pushed the megatron-muon-clean branch 2 times, most recently from 9aacd13 to 3bb6903 Compare July 24, 2026 18:03
When vanilla_mbridge=False, defer provider DDP wrapping for Muon
layer-wise configs and apply wrap_model_chunks_with_layerwise_aware_ddp
(matching the legacy mbridge path). Fail closed on Megatron FSDP combo.
@ISEEKYAN
ISEEKYAN force-pushed the megatron-muon-clean branch from 3bb6903 to 50a5551 Compare July 24, 2026 18:12
ISEEKYAN added 4 commits July 24, 2026 11:14
Megatron-Core declares muon_scalar_optimizer with the docstring "one of
'adam' or 'lion'", and this docstring mirrored that wording. In practice
no Megatron-Core code path reads the field, so selecting "lion" is a
silent no-op; the default "adam" is both the shipped default and the
only behaviour Muon is documented against upstream (emerging_optimizers
recommends a standard method such as AdamW for the non-matrix params).

Describe the field as it actually behaves instead of advertising a
choice that does not take effect. No functional change.
Muon's effective step size is lr * muon_extra_scale_factor, and the
Megatron-Core default of 1.0 is not AdamW-comparable: reusing an AdamW
learning rate unchanged gives a ~4.4x larger effective step, a common
cause of unstable or non-converging Muon runs. The previous README text
("Muon does not need a separate learning rate") was misleading on
exactly this point and is replaced.

Add opt-in muon_match_adamw_update_rms, which resolves the factor from
the closed form documented by emerging_optimizers 0.3.0:

    muon_extra_scale_factor = sqrt((1 - beta1) / (1 + beta1))

("analytically gives the update RMS norm of AdamW",
get_muon_scale_factor docstring; https://kexue.fm/archives/11267;
https://arxiv.org/abs/2502.16982). At the default beta1=0.9 this is
~0.229416, which is where the commonly quoted ~0.2 comes from -- that is
the value of the factor, not a target for any measured update RMS.

The flag is verl-side only and is not forwarded to Megatron-Core. The
resolved value is logged on rank 0 so it stays reproducible from the
config instead of being pasted in as a constant. Setting the flag
together with an explicit muon_extra_scale_factor raises. The default
stays 1.0, matching Megatron-Core, so no existing config changes
behaviour.
Make explicit that the recommended configuration is
muon_match_adamw_update_rms, not a pasted-in muon_extra_scale_factor
value. A hand-entered constant is the thing to avoid regardless of how it
was derived: a reader can re-derive the switch's value from beta1, but
not a literal. The numeric value is kept only as orientation.
@wuxibin89
wuxibin89 merged commit 8446ee4 into verl-project:main Jul 30, 2026
93 of 102 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants