Background
Muon is a momentum-based optimizer that applies Newton-Schulz orthogonalization to the gradient before the update step. It has been validated at scale:
- Kimi Moonlight: 3B/16B-parameter MoE model trained on 5.7T tokens — validated Muon with weight decay, distributed Muon, and hyperparameter matching with AdamW
- Kimi K2: 1040B-parameter MoE model (32B active) trained on 15.5T tokens — added QK clipping for attention stability
The core idea: for matrix-shaped parameters (attention projections, MLP weights), replace the Adam update with a Nesterov-SGD step where the gradient is first passed through Newton-Schulz iteration to approximate msign(G) = U @ diag(sign(σᵢ)) @ Vᵀ. Non-matrix parameters (embeddings, output logits, norms, biases) continue to use AdamW.
What Needs to Change
1. Config Schema (torchtitan/components/optimizer.py)
The current OptimizersContainer.Config has a single lr and AdamW-specific fields. Muon requires a mixed-optimizer config with separate hyperparameters for each group:
@dataclass(kw_only=True, slots=True)
class Config(Configurable.Config):
name: str = "AdamW" # or "MuonWithAdamW"
lr: float = 8e-4 # AdamW lr (for embeddings, norms, etc.)
lr_muon: float = 0.02 # Muon lr (for matrix params)
beta1: float = 0.9
beta2: float = 0.95
eps: float = 1e-8
weight_decay: float = 0.1
muon_weight_decay: float = 0.0
muon_momentum: float = 0.95
ns_steps: int = 5 # Newton-Schulz iteration count
ns_a: float = 3.4445
ns_b: float = -4.7750
ns_c: float = 2.0315
2. Parameter Group Splitting
Currently the optimizer receives a flat parameter list (optimizer.py:119). Muon requires splitting parameters into two groups:
def _split_param_groups(model: nn.Module) -> tuple[list, list]:
"""Returns (muon_params, adam_params)."""
muon_params, adam_params = [], []
for name, param in model.named_parameters():
if not param.requires_grad:
continue
if (
param.ndim == 2
and not any(skip in name for skip in ["tok_embeddings", "output", "norm"])
):
muon_params.append(param)
else:
adam_params.append(param)
return muon_params, adam_params
Per Keller Jordan's empirical findings, applying Muon to wq, wk, wv separately (rather than as a fused QKV matrix) yields better results. torchtitan's Llama model already stores them as separate nn.Linear modules (attention.py:428-482), so this is naturally satisfied.
3. Newton-Schulz Orthogonalization
def newtonschulz5(G: Tensor, steps: int = 5, eps: float = 1e-7,
a: float = 3.4445, b: float = -4.7750, c: float = 2.0315) -> Tensor:
"""Approximately compute msign(G) = U @ diag(sign(σ)) @ Vᵀ via quintic iteration."""
assert G.ndim == 2
X = G.bfloat16()
X = X / (X.norm() + eps)
if G.size(0) > G.size(1):
X = X.T
for _ in range(steps):
A = X @ X.T
B = b * A + c * A @ A
X = a * X + B @ X
if G.size(0) > G.size(1):
X = X.T
return X.to(G.dtype)
The polynomial (a, b, c) = (3.4445, -4.7750, 2.0315) was chosen by Keller Jordan for fast convergence; Kimi Moonlight uses these same values.
4. Distributed Muon (FSDP2 Compatibility)
This is the critical engineering challenge. Under FSDP2, parameters are DTensor objects sharded across the data-parallel mesh. The Newton-Schulz step operates on the full gradient matrix, so it must run after gradient all-reduce.
Two approaches to evaluate:
Option A: All-gather before NS
- All-gather sharded gradient into the full matrix on each rank
- Apply Newton-Schulz locally on the full matrix
- Scatter the update back
- Similar to
torch.optim.Muon in PyTorch 2.9 (pytorch#148819)
Option B: Per-rank NS + all-reduce
- Apply NS to the local gradient shard
- All-reduce the NS-transformed gradient
- Kimi's Moonlight paper approach — avoids the all-gather overhead
Note: As of PyTorch 2.9, torch.optim.Muon does not yet support distributed training (confirmed in pytorch#148819). A custom distributed implementation is required.
5. Checkpoint Compatibility
torchtitan uses get_optimizer_state_dict / set_optimizer_state_dict with flatten_optimizer_state_dict=True (optimizer.py:140-157). For a mixed-optimizer setup:
- Muon state per matrix param:
momentum_buffer (1st-order only, no 2nd moment)
- AdamW state per non-matrix param:
exp_avg, exp_avg_sq, step
The flattened format should still work since state is keyed by parameter FQN, but this requires testing.
6. FP8 Interaction
Do not apply FP8 GEMM to the Newton-Schulz computation. Kimi K2 explicitly disables FP8 for Muon steps (confirmed officially). The post_optimizer_hook registered for Float8 should be excluded from Muon parameter groups.
Recommended Hyperparameters
Based on Kimi Moonlight and Keller Jordan's experiments:
| Hyperparameter |
Recommended Value |
Notes |
| Muon LR |
0.02 |
~25× larger than AdamW LR of 8e-4 |
| Muon momentum |
0.95 |
Nesterov-style |
| Muon weight decay |
0.0 |
Kimi Moonlight; AdamW handles it for non-Muon params |
| NS steps |
5 |
Good accuracy/cost trade-off |
(a, b, c) |
(3.4445, -4.7750, 2.0315) |
Keller Jordan values; Kimi Moonlight uses same |
Proposed Work Items
References
Background
Muon is a momentum-based optimizer that applies Newton-Schulz orthogonalization to the gradient before the update step. It has been validated at scale:
The core idea: for matrix-shaped parameters (attention projections, MLP weights), replace the Adam update with a Nesterov-SGD step where the gradient is first passed through Newton-Schulz iteration to approximate
msign(G) = U @ diag(sign(σᵢ)) @ Vᵀ. Non-matrix parameters (embeddings, output logits, norms, biases) continue to use AdamW.What Needs to Change
1. Config Schema (
torchtitan/components/optimizer.py)The current
OptimizersContainer.Confighas a singlelrand AdamW-specific fields. Muon requires a mixed-optimizer config with separate hyperparameters for each group:2. Parameter Group Splitting
Currently the optimizer receives a flat parameter list (
optimizer.py:119). Muon requires splitting parameters into two groups:Per Keller Jordan's empirical findings, applying Muon to
wq,wk,wvseparately (rather than as a fused QKV matrix) yields better results. torchtitan's Llama model already stores them as separatenn.Linearmodules (attention.py:428-482), so this is naturally satisfied.3. Newton-Schulz Orthogonalization
The polynomial
(a, b, c) = (3.4445, -4.7750, 2.0315)was chosen by Keller Jordan for fast convergence; Kimi Moonlight uses these same values.4. Distributed Muon (FSDP2 Compatibility)
This is the critical engineering challenge. Under FSDP2, parameters are
DTensorobjects sharded across the data-parallel mesh. The Newton-Schulz step operates on the full gradient matrix, so it must run after gradient all-reduce.Two approaches to evaluate:
Option A: All-gather before NS
torch.optim.Muonin PyTorch 2.9 (pytorch#148819)Option B: Per-rank NS + all-reduce
5. Checkpoint Compatibility
torchtitan uses
get_optimizer_state_dict/set_optimizer_state_dictwithflatten_optimizer_state_dict=True(optimizer.py:140-157). For a mixed-optimizer setup:momentum_buffer(1st-order only, no 2nd moment)exp_avg,exp_avg_sq,stepThe flattened format should still work since state is keyed by parameter FQN, but this requires testing.
6. FP8 Interaction
Do not apply FP8 GEMM to the Newton-Schulz computation. Kimi K2 explicitly disables FP8 for Muon steps (confirmed officially). The
post_optimizer_hookregistered for Float8 should be excluded from Muon parameter groups.Recommended Hyperparameters
Based on Kimi Moonlight and Keller Jordan's experiments:
0.028e-40.950.05(a, b, c)(3.4445, -4.7750, 2.0315)Proposed Work Items
MuonWithAdamWoptimizer class that wraps two internal optimizers with parameter group routingMuonWithAdamWto_resolve_optimizer_cls()and extend config schemaReferences