Andrej Karpathy commited on
Commit ·
41bb2ea
1
Parent(s): 64a651a
Combine AdamW and Muon into single MuonAdamW optimizer, cleaner, ty @chrisjmccormick for idea/help
Browse files- README.md +1 -2
- nanochat/adamw.py +0 -143
- nanochat/gpt.py +27 -24
- nanochat/muon.py +0 -352
- nanochat/optim.py +528 -0
- scripts/base_train.py +12 -16
- scripts/chat_rl.py +7 -10
- scripts/chat_sft.py +8 -11
- scripts/mid_train.py +12 -16
README.md
CHANGED
|
@@ -135,7 +135,6 @@ python -m pytest tests/test_engine.py -v -s
|
|
| 135 |
│ └── repackage_data_reference.py # Pretraining data shard generation
|
| 136 |
├── nanochat
|
| 137 |
│ ├── __init__.py # empty
|
| 138 |
-
│ ├── adamw.py # Distributed AdamW optimizer
|
| 139 |
│ ├── checkpoint_manager.py # Save/Load model checkpoints
|
| 140 |
│ ├── common.py # Misc small utilities, quality of life
|
| 141 |
│ ├── core_eval.py # Evaluates base model CORE score (DCLM paper)
|
|
@@ -146,7 +145,7 @@ python -m pytest tests/test_engine.py -v -s
|
|
| 146 |
│ ├── gpt.py # The GPT nn.Module Transformer
|
| 147 |
│ ├── logo.svg
|
| 148 |
│ ├── loss_eval.py # Evaluate bits per byte (instead of loss)
|
| 149 |
-
│ ├──
|
| 150 |
│ ├── report.py # Utilities for writing the nanochat Report
|
| 151 |
│ ├── tokenizer.py # BPE Tokenizer wrapper in style of GPT-4
|
| 152 |
│ └── ui.html # HTML/CSS/JS for nanochat frontend
|
|
|
|
| 135 |
│ └── repackage_data_reference.py # Pretraining data shard generation
|
| 136 |
├── nanochat
|
| 137 |
│ ├── __init__.py # empty
|
|
|
|
| 138 |
│ ├── checkpoint_manager.py # Save/Load model checkpoints
|
| 139 |
│ ├── common.py # Misc small utilities, quality of life
|
| 140 |
│ ├── core_eval.py # Evaluates base model CORE score (DCLM paper)
|
|
|
|
| 145 |
│ ├── gpt.py # The GPT nn.Module Transformer
|
| 146 |
│ ├── logo.svg
|
| 147 |
│ ├── loss_eval.py # Evaluate bits per byte (instead of loss)
|
| 148 |
+
│ ├── optim.py # AdamW + Muon optimizer, 1GPU and distributed
|
| 149 |
│ ├── report.py # Utilities for writing the nanochat Report
|
| 150 |
│ ├── tokenizer.py # BPE Tokenizer wrapper in style of GPT-4
|
| 151 |
│ └── ui.html # HTML/CSS/JS for nanochat frontend
|
nanochat/adamw.py
DELETED
|
@@ -1,143 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Distributed AdamW optimizer with a fused step function.
|
| 3 |
-
A bunch of ideas (e.g. dist comms in slices) are borrowed from modded-nanogpt.
|
| 4 |
-
"""
|
| 5 |
-
import torch
|
| 6 |
-
import torch.distributed as dist
|
| 7 |
-
from torch import Tensor
|
| 8 |
-
|
| 9 |
-
@torch.compile(dynamic=False, fullgraph=True)
|
| 10 |
-
def adamw_step_fused(
|
| 11 |
-
p: Tensor,
|
| 12 |
-
grad: Tensor,
|
| 13 |
-
exp_avg: Tensor,
|
| 14 |
-
exp_avg_sq: Tensor,
|
| 15 |
-
step_t: Tensor,
|
| 16 |
-
lr_t: Tensor,
|
| 17 |
-
beta1_t: Tensor,
|
| 18 |
-
beta2_t: Tensor,
|
| 19 |
-
eps_t: Tensor,
|
| 20 |
-
wd_t: Tensor,
|
| 21 |
-
) -> None:
|
| 22 |
-
"""
|
| 23 |
-
Fused AdamW step: weight_decay -> momentum_update -> bias_correction -> param_update
|
| 24 |
-
All in one compiled graph to eliminate Python overhead between ops.
|
| 25 |
-
The 0-D CPU tensors avoid recompilation when hyperparameter values change.
|
| 26 |
-
"""
|
| 27 |
-
# Weight decay (decoupled, applied before the update)
|
| 28 |
-
p.mul_(1 - lr_t * wd_t)
|
| 29 |
-
# Update running averages (lerp_ is cleaner and fuses well)
|
| 30 |
-
exp_avg.lerp_(grad, 1 - beta1_t)
|
| 31 |
-
exp_avg_sq.lerp_(grad.square(), 1 - beta2_t)
|
| 32 |
-
# Bias corrections
|
| 33 |
-
bias1 = 1 - beta1_t ** step_t
|
| 34 |
-
bias2 = 1 - beta2_t ** step_t
|
| 35 |
-
# Compute update and apply
|
| 36 |
-
denom = (exp_avg_sq / bias2).sqrt() + eps_t
|
| 37 |
-
step_size = lr_t / bias1
|
| 38 |
-
p.add_(exp_avg / denom, alpha=-step_size)
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
class DistAdamW(torch.optim.Optimizer):
|
| 42 |
-
"""
|
| 43 |
-
Distributed AdamW optimizer.
|
| 44 |
-
In the style of ZeRO-2, i.e. sharded optimizer states and gradient reduction
|
| 45 |
-
"""
|
| 46 |
-
def __init__(self, param_groups, lr: float = 1e-3, betas: tuple[float, float] = (0.9, 0.999), eps: float = 1e-8, weight_decay: float = 0.01):
|
| 47 |
-
defaults = dict(lr=lr, betas=betas, eps=eps, weight_decay=weight_decay)
|
| 48 |
-
rank = dist.get_rank()
|
| 49 |
-
world_size = dist.get_world_size()
|
| 50 |
-
# Validate
|
| 51 |
-
if rank == 0:
|
| 52 |
-
for group in param_groups:
|
| 53 |
-
assert isinstance(group, dict), "expecting param_groups to be a list of dicts"
|
| 54 |
-
assert isinstance(group['params'], list), "expecting group['params'] to be a list of tensors"
|
| 55 |
-
for p in group['params']:
|
| 56 |
-
sliced = p.numel() >= 1024
|
| 57 |
-
print(f"AdamW: 1 param of shape {p.shape}, sliced={sliced}")
|
| 58 |
-
if sliced: # large parameter tensors will be operated on in slices
|
| 59 |
-
assert p.shape[0] % world_size == 0, f"First dim of parameter shape {p.shape} must be divisible by world size {world_size}"
|
| 60 |
-
super().__init__(param_groups, defaults)
|
| 61 |
-
# 0-D CPU tensors to avoid torch.compile recompilation when values change
|
| 62 |
-
self._step_t = torch.tensor(0.0, dtype=torch.float32, device="cpu")
|
| 63 |
-
self._lr_t = torch.tensor(0.0, dtype=torch.float32, device="cpu")
|
| 64 |
-
self._beta1_t = torch.tensor(0.0, dtype=torch.float32, device="cpu")
|
| 65 |
-
self._beta2_t = torch.tensor(0.0, dtype=torch.float32, device="cpu")
|
| 66 |
-
self._eps_t = torch.tensor(0.0, dtype=torch.float32, device="cpu")
|
| 67 |
-
self._wd_t = torch.tensor(0.0, dtype=torch.float32, device="cpu")
|
| 68 |
-
|
| 69 |
-
@torch.no_grad()
|
| 70 |
-
def step(self):
|
| 71 |
-
rank = dist.get_rank()
|
| 72 |
-
world_size = dist.get_world_size()
|
| 73 |
-
reduce_futures: list[torch.Future] = []
|
| 74 |
-
gather_futures: list[torch.Future] = []
|
| 75 |
-
grad_slices = []
|
| 76 |
-
is_small = [] # track which params are small (use all_reduce) vs large (use reduce_scatter)
|
| 77 |
-
|
| 78 |
-
for group in self.param_groups:
|
| 79 |
-
params: list[Tensor] = group["params"]
|
| 80 |
-
for p in params:
|
| 81 |
-
grad = p.grad
|
| 82 |
-
# Small params: use all_reduce (no scatter/gather needed)
|
| 83 |
-
if p.numel() < 1024:
|
| 84 |
-
is_small.append(True)
|
| 85 |
-
reduce_futures.append(dist.all_reduce(grad, op=dist.ReduceOp.AVG, async_op=True).get_future())
|
| 86 |
-
grad_slices.append(grad)
|
| 87 |
-
else:
|
| 88 |
-
is_small.append(False)
|
| 89 |
-
rank_size = grad.shape[0] // world_size # p.shape[0] % world_size == 0 is checked in __init__
|
| 90 |
-
grad_slice = torch.empty_like(grad[:rank_size])
|
| 91 |
-
reduce_futures.append(dist.reduce_scatter_tensor(grad_slice, grad, op=dist.ReduceOp.AVG, async_op=True).get_future())
|
| 92 |
-
grad_slices.append(grad_slice)
|
| 93 |
-
|
| 94 |
-
idx = 0
|
| 95 |
-
for group in self.param_groups:
|
| 96 |
-
beta1, beta2 = group['betas']
|
| 97 |
-
eps = group['eps']
|
| 98 |
-
wd = group['weight_decay']
|
| 99 |
-
params = group['params']
|
| 100 |
-
for p in params:
|
| 101 |
-
reduce_futures[idx].wait()
|
| 102 |
-
g_slice = grad_slices[idx]
|
| 103 |
-
lr = group['lr'] * getattr(p, "lr_mul", 1.0)
|
| 104 |
-
state = self.state[p]
|
| 105 |
-
|
| 106 |
-
# For small params, operate on full param; for large, operate on slice
|
| 107 |
-
if is_small[idx]:
|
| 108 |
-
p_slice = p
|
| 109 |
-
else:
|
| 110 |
-
rank_size = p.shape[0] // world_size
|
| 111 |
-
p_slice = p[rank * rank_size:(rank + 1) * rank_size]
|
| 112 |
-
|
| 113 |
-
# State init
|
| 114 |
-
if not state:
|
| 115 |
-
state['step'] = 0
|
| 116 |
-
state['exp_avg'] = torch.zeros_like(p_slice)
|
| 117 |
-
state['exp_avg_sq'] = torch.zeros_like(p_slice)
|
| 118 |
-
exp_avg = state['exp_avg']
|
| 119 |
-
exp_avg_sq = state['exp_avg_sq']
|
| 120 |
-
state['step'] += 1
|
| 121 |
-
|
| 122 |
-
# Fill 0-D tensors with current values
|
| 123 |
-
eff_wd = wd * getattr(p, "wd_mul", 1.0)
|
| 124 |
-
self._step_t.fill_(state['step'])
|
| 125 |
-
self._lr_t.fill_(lr)
|
| 126 |
-
self._beta1_t.fill_(beta1)
|
| 127 |
-
self._beta2_t.fill_(beta2)
|
| 128 |
-
self._eps_t.fill_(eps)
|
| 129 |
-
self._wd_t.fill_(eff_wd)
|
| 130 |
-
|
| 131 |
-
# Fused update: weight_decay -> momentum -> bias_correction -> param_update
|
| 132 |
-
adamw_step_fused(
|
| 133 |
-
p_slice, g_slice, exp_avg, exp_avg_sq,
|
| 134 |
-
self._step_t, self._lr_t, self._beta1_t, self._beta2_t, self._eps_t, self._wd_t,
|
| 135 |
-
)
|
| 136 |
-
|
| 137 |
-
# Only large params need all_gather
|
| 138 |
-
if not is_small[idx]:
|
| 139 |
-
gather_futures.append(dist.all_gather_into_tensor(p, p_slice, async_op=True).get_future())
|
| 140 |
-
idx += 1
|
| 141 |
-
|
| 142 |
-
if gather_futures:
|
| 143 |
-
torch.futures.collect_all(gather_futures).wait()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
nanochat/gpt.py
CHANGED
|
@@ -20,8 +20,7 @@ import torch.nn as nn
|
|
| 20 |
import torch.nn.functional as F
|
| 21 |
|
| 22 |
from nanochat.common import get_dist_info, print0
|
| 23 |
-
from nanochat.
|
| 24 |
-
from nanochat.adamw import DistAdamW
|
| 25 |
|
| 26 |
# Our custom Flash Attention module that automatically uses FA3 on Hopper+ and SDPA fallback elsewhere
|
| 27 |
from nanochat.flash_attention import flash_attn
|
|
@@ -346,9 +345,10 @@ class GPT(nn.Module):
|
|
| 346 |
'total': total,
|
| 347 |
}
|
| 348 |
|
| 349 |
-
def
|
| 350 |
model_dim = self.config.n_embd
|
| 351 |
ddp, rank, local_rank, world_size = get_dist_info()
|
|
|
|
| 352 |
# Separate out all parameters into groups
|
| 353 |
matrix_params = list(self.transformer.h.parameters())
|
| 354 |
value_embeds_params = list(self.value_embeds.parameters())
|
|
@@ -357,30 +357,33 @@ class GPT(nn.Module):
|
|
| 357 |
resid_params = [self.resid_lambdas]
|
| 358 |
x0_params = [self.x0_lambdas]
|
| 359 |
assert len(list(self.parameters())) == len(matrix_params) + len(embedding_params) + len(lm_head_params) + len(value_embeds_params) + len(resid_params) + len(x0_params)
|
| 360 |
-
|
| 361 |
-
# Scale the LR for the AdamW parameters by ∝1/√dmodel (
|
| 362 |
dmodel_lr_scale = (model_dim / 768) ** -0.5
|
| 363 |
print0(f"Scaling the LR for the AdamW parameters ∝1/√({model_dim}/768) = {dmodel_lr_scale:.6f}")
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
dict(params=
|
| 369 |
-
dict(params=
|
|
|
|
|
|
|
|
|
|
| 370 |
]
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
return
|
| 384 |
|
| 385 |
def forward(self, idx, targets=None, kv_cache=None, loss_reduction='mean'):
|
| 386 |
B, T = idx.size()
|
|
|
|
| 20 |
import torch.nn.functional as F
|
| 21 |
|
| 22 |
from nanochat.common import get_dist_info, print0
|
| 23 |
+
from nanochat.optim import MuonAdamW, DistMuonAdamW
|
|
|
|
| 24 |
|
| 25 |
# Our custom Flash Attention module that automatically uses FA3 on Hopper+ and SDPA fallback elsewhere
|
| 26 |
from nanochat.flash_attention import flash_attn
|
|
|
|
| 345 |
'total': total,
|
| 346 |
}
|
| 347 |
|
| 348 |
+
def setup_optimizer(self, unembedding_lr=0.004, embedding_lr=0.2, matrix_lr=0.02, weight_decay=0.0, adam_betas=(0.8, 0.95), scalar_lr=0.5):
|
| 349 |
model_dim = self.config.n_embd
|
| 350 |
ddp, rank, local_rank, world_size = get_dist_info()
|
| 351 |
+
|
| 352 |
# Separate out all parameters into groups
|
| 353 |
matrix_params = list(self.transformer.h.parameters())
|
| 354 |
value_embeds_params = list(self.value_embeds.parameters())
|
|
|
|
| 357 |
resid_params = [self.resid_lambdas]
|
| 358 |
x0_params = [self.x0_lambdas]
|
| 359 |
assert len(list(self.parameters())) == len(matrix_params) + len(embedding_params) + len(lm_head_params) + len(value_embeds_params) + len(resid_params) + len(x0_params)
|
| 360 |
+
|
| 361 |
+
# Scale the LR for the AdamW parameters by ∝1/√dmodel (tuned for 768 dim model)
|
| 362 |
dmodel_lr_scale = (model_dim / 768) ** -0.5
|
| 363 |
print0(f"Scaling the LR for the AdamW parameters ∝1/√({model_dim}/768) = {dmodel_lr_scale:.6f}")
|
| 364 |
+
|
| 365 |
+
# Build param_groups with all required fields explicit
|
| 366 |
+
param_groups = [
|
| 367 |
+
# AdamW groups (embeddings, lm_head, scalars)
|
| 368 |
+
dict(kind='adamw', params=lm_head_params, lr=unembedding_lr * dmodel_lr_scale, betas=adam_betas, eps=1e-10, weight_decay=0.0),
|
| 369 |
+
dict(kind='adamw', params=embedding_params, lr=embedding_lr * dmodel_lr_scale, betas=adam_betas, eps=1e-10, weight_decay=0.0),
|
| 370 |
+
dict(kind='adamw', params=value_embeds_params, lr=embedding_lr * dmodel_lr_scale, betas=adam_betas, eps=1e-10, weight_decay=0.0),
|
| 371 |
+
dict(kind='adamw', params=resid_params, lr=scalar_lr * 0.01, betas=adam_betas, eps=1e-10, weight_decay=0.0),
|
| 372 |
+
dict(kind='adamw', params=x0_params, lr=scalar_lr, betas=(0.96, 0.95), eps=1e-10, weight_decay=0.0), # higher beta1 for x0
|
| 373 |
]
|
| 374 |
+
# Muon groups (matrix params, grouped by shape for stacking)
|
| 375 |
+
for shape in sorted({p.shape for p in matrix_params}):
|
| 376 |
+
group_params = [p for p in matrix_params if p.shape == shape]
|
| 377 |
+
param_groups.append(dict(
|
| 378 |
+
kind='muon', params=group_params, lr=matrix_lr,
|
| 379 |
+
momentum=0.95, ns_steps=5, beta2=0.95, weight_decay=weight_decay,
|
| 380 |
+
))
|
| 381 |
+
|
| 382 |
+
Factory = DistMuonAdamW if ddp else MuonAdamW
|
| 383 |
+
optimizer = Factory(param_groups)
|
| 384 |
+
for group in optimizer.param_groups:
|
| 385 |
+
group["initial_lr"] = group["lr"]
|
| 386 |
+
return optimizer
|
| 387 |
|
| 388 |
def forward(self, idx, targets=None, kv_cache=None, loss_reduction='mean'):
|
| 389 |
B, T = idx.size()
|
nanochat/muon.py
DELETED
|
@@ -1,352 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Muon optimizer adapted and simplified from modded-nanogpt.
|
| 3 |
-
https://github.com/KellerJordan/modded-nanogpt
|
| 4 |
-
|
| 5 |
-
Background:
|
| 6 |
-
Newton-Schulz iteration to compute the zeroth power / orthogonalization of G. We opt to use a
|
| 7 |
-
quintic iteration whose coefficients are selected to maximize the slope at zero. For the purpose
|
| 8 |
-
of minimizing steps, it turns out to be empirically effective to keep increasing the slope at
|
| 9 |
-
zero even beyond the point where the iteration no longer converges all the way to one everywhere
|
| 10 |
-
on the interval. This iteration therefore does not produce UV^T but rather something like US'V^T
|
| 11 |
-
where S' is diagonal with S_{ii}' ~ Uniform(0.5, 1.5), which turns out not to hurt model
|
| 12 |
-
performance at all relative to UV^T, where USV^T = G is the SVD.
|
| 13 |
-
|
| 14 |
-
Here, an alternative to Newton-Schulz iteration with potentially better convergence properties:
|
| 15 |
-
Polar Express Sign Method for orthogonalization.
|
| 16 |
-
https://arxiv.org/pdf/2505.16932
|
| 17 |
-
by Noah Amsel, David Persson, Christopher Musco, Robert M. Gower.
|
| 18 |
-
|
| 19 |
-
Some of the changes in nanochat implementation:
|
| 20 |
-
- Uses a simpler, more general approach to parameter grouping and stacking
|
| 21 |
-
- Uses a single fused kernel for the momentum -> polar_express -> variance_reduction -> update step
|
| 22 |
-
- Makes no assumptions about model architecture (e.g. that attention weights are fused into QKVO format)
|
| 23 |
-
"""
|
| 24 |
-
|
| 25 |
-
import torch
|
| 26 |
-
from torch import Tensor
|
| 27 |
-
import torch.distributed as dist
|
| 28 |
-
|
| 29 |
-
# Coefficients for Polar Express (computed for num_iters=5, safety_factor=2e-2, cushion=2)
|
| 30 |
-
# From https://arxiv.org/pdf/2505.16932
|
| 31 |
-
polar_express_coeffs = [
|
| 32 |
-
(8.156554524902461, -22.48329292557795, 15.878769915207462),
|
| 33 |
-
(4.042929935166739, -2.808917465908714, 0.5000178451051316),
|
| 34 |
-
(3.8916678022926607, -2.772484153217685, 0.5060648178503393),
|
| 35 |
-
(3.285753657755655, -2.3681294933425376, 0.46449024233003106),
|
| 36 |
-
(2.3465413258596377, -1.7097828382687081, 0.42323551169305323),
|
| 37 |
-
]
|
| 38 |
-
|
| 39 |
-
@torch.compile(dynamic=False, fullgraph=True)
|
| 40 |
-
def muon_step_fused(
|
| 41 |
-
stacked_grads: Tensor,
|
| 42 |
-
stacked_params: Tensor,
|
| 43 |
-
momentum_buffer: Tensor,
|
| 44 |
-
second_momentum_buffer: Tensor,
|
| 45 |
-
momentum_t: Tensor,
|
| 46 |
-
lr_t: Tensor,
|
| 47 |
-
wd_t: Tensor,
|
| 48 |
-
beta2_t: Tensor,
|
| 49 |
-
ns_steps: int,
|
| 50 |
-
red_dim: int,
|
| 51 |
-
) -> None:
|
| 52 |
-
"""
|
| 53 |
-
Fused Muon step: momentum -> polar_express -> variance_reduction -> cautious_update
|
| 54 |
-
All in one compiled graph to eliminate Python overhead between ops.
|
| 55 |
-
Some of the constants are 0-D CPU tensors to avoid recompilation when values change.
|
| 56 |
-
"""
|
| 57 |
-
|
| 58 |
-
# Nesterov momentum
|
| 59 |
-
momentum = momentum_t.to(stacked_grads.dtype)
|
| 60 |
-
momentum_buffer.lerp_(stacked_grads, 1 - momentum)
|
| 61 |
-
g = stacked_grads.lerp_(momentum_buffer, momentum)
|
| 62 |
-
|
| 63 |
-
# Polar express
|
| 64 |
-
X = g.bfloat16()
|
| 65 |
-
if g.size(-2) > g.size(-1):
|
| 66 |
-
X = X.mT
|
| 67 |
-
X = X / (X.norm(dim=(-2, -1), keepdim=True) * 1.02 + 1e-6)
|
| 68 |
-
for a, b, c in polar_express_coeffs[:ns_steps]:
|
| 69 |
-
A = X @ X.mT
|
| 70 |
-
B = b * A + c * (A @ A)
|
| 71 |
-
X = a * X + B @ X
|
| 72 |
-
if g.size(-2) > g.size(-1):
|
| 73 |
-
X = X.mT
|
| 74 |
-
g = X
|
| 75 |
-
|
| 76 |
-
# Variance reduction
|
| 77 |
-
beta2 = beta2_t.to(g.dtype)
|
| 78 |
-
v_mean = g.float().square().mean(dim=red_dim, keepdim=True)
|
| 79 |
-
red_dim_size = g.size(red_dim)
|
| 80 |
-
v_norm_sq = v_mean.sum(dim=(-2, -1), keepdim=True) * red_dim_size
|
| 81 |
-
v_norm = v_norm_sq.sqrt()
|
| 82 |
-
second_momentum_buffer.lerp_(v_mean.to(dtype=second_momentum_buffer.dtype), 1 - beta2)
|
| 83 |
-
step_size = second_momentum_buffer.clamp_min(1e-10).rsqrt()
|
| 84 |
-
scaled_sq_sum = (v_mean * red_dim_size) * step_size.float().square()
|
| 85 |
-
v_norm_new = scaled_sq_sum.sum(dim=(-2, -1), keepdim=True).sqrt()
|
| 86 |
-
final_scale = step_size * (v_norm / v_norm_new.clamp_min(1e-10))
|
| 87 |
-
g = g * final_scale.to(g.dtype)
|
| 88 |
-
|
| 89 |
-
# Cautious weight decay + parameter update
|
| 90 |
-
lr = lr_t.to(g.dtype)
|
| 91 |
-
wd = wd_t.to(g.dtype)
|
| 92 |
-
mask = (g * stacked_params) >= 0
|
| 93 |
-
stacked_params.sub_(lr * g + lr * wd * stacked_params * mask)
|
| 94 |
-
|
| 95 |
-
class Muon(torch.optim.Optimizer):
|
| 96 |
-
"""
|
| 97 |
-
Muon - MomentUm Orthogonalized by Newton-schulz
|
| 98 |
-
|
| 99 |
-
https://kellerjordan.github.io/posts/muon/
|
| 100 |
-
|
| 101 |
-
Muon internally runs standard SGD-momentum, and then performs an orthogonalization post-
|
| 102 |
-
processing step, in which each 2D parameter's update is replaced with the nearest orthogonal
|
| 103 |
-
matrix. To efficiently orthogonalize each update, we use a Newton-Schulz iteration, which has
|
| 104 |
-
the advantage that it can be stably run in bfloat16 on the GPU.
|
| 105 |
-
|
| 106 |
-
Some warnings:
|
| 107 |
-
- This optimizer should not be used for the embedding layer, the final fully connected layer,
|
| 108 |
-
or any {0,1}-D parameters; those should all be optimized by a standard method (e.g., AdamW).
|
| 109 |
-
- To use it with 4D convolutional filters, it works well to just flatten their last 3 dimensions.
|
| 110 |
-
|
| 111 |
-
Arguments:
|
| 112 |
-
lr: The learning rate used by the internal SGD.
|
| 113 |
-
momentum: The momentum used by the internal SGD.
|
| 114 |
-
ns_steps: The number of Newton-Schulz iteration steps to use.
|
| 115 |
-
beta2: The decay rate for the second moment (variance) estimate. Set to None to disable.
|
| 116 |
-
weight_decay: Cautious weight decay coefficient. Only decays where update and weight agree.
|
| 117 |
-
"""
|
| 118 |
-
def __init__(self, params, lr=0.02, momentum=0.95, ns_steps=5, beta2=0.95, weight_decay=0.0):
|
| 119 |
-
defaults = dict(lr=lr, momentum=momentum, ns_steps=ns_steps, beta2=beta2, weight_decay=weight_decay)
|
| 120 |
-
assert all(p.ndim == 2 for p in params), "Muon expects 2D parameters only"
|
| 121 |
-
params = list(params) # ensure we have a list, not an e.g. (exhaustible) iterator
|
| 122 |
-
# Group by shape so we can stack tensors
|
| 123 |
-
shapes = sorted({p.shape for p in params})
|
| 124 |
-
param_groups = []
|
| 125 |
-
for shape in shapes:
|
| 126 |
-
group_params = [p for p in params if p.shape == shape]
|
| 127 |
-
param_groups.append(dict(params=group_params))
|
| 128 |
-
super().__init__(param_groups, defaults)
|
| 129 |
-
# 0-D CPU tensors to avoid torch.compile recompilation when values change
|
| 130 |
-
self._momentum_t = torch.tensor(0.0, dtype=torch.float32, device="cpu")
|
| 131 |
-
self._lr_t = torch.tensor(0.0, dtype=torch.float32, device="cpu")
|
| 132 |
-
self._wd_t = torch.tensor(0.0, dtype=torch.float32, device="cpu")
|
| 133 |
-
self._beta2_t = torch.tensor(0.0, dtype=torch.float32, device="cpu")
|
| 134 |
-
|
| 135 |
-
@torch.no_grad()
|
| 136 |
-
def step(self):
|
| 137 |
-
for group in self.param_groups:
|
| 138 |
-
params: list[Tensor] = group["params"]
|
| 139 |
-
if not params:
|
| 140 |
-
continue
|
| 141 |
-
|
| 142 |
-
# Get or create group-level buffers (stored in first param's state for convenience)
|
| 143 |
-
state = self.state[params[0]]
|
| 144 |
-
num_params = len(params) # e.g.: 12 (for a d12 model)
|
| 145 |
-
# e.g.: shape = (768, 3072), device = cuda:0, dtype = torch.float32, for one of the MLP projections
|
| 146 |
-
shape, device, dtype = params[0].shape, params[0].device, params[0].dtype
|
| 147 |
-
|
| 148 |
-
# Momentum for every individual parameter
|
| 149 |
-
if "momentum_buffer" not in state:
|
| 150 |
-
state["momentum_buffer"] = torch.zeros(num_params, *shape, dtype=dtype, device=device)
|
| 151 |
-
momentum_buffer = state["momentum_buffer"] # e.g.: (12, 768, 3072)
|
| 152 |
-
|
| 153 |
-
# Second momentum buffer is factored, either per-row or per-column
|
| 154 |
-
if "second_momentum_buffer" not in state:
|
| 155 |
-
if shape[-2] >= shape[-1]:
|
| 156 |
-
state["second_momentum_buffer"] = torch.zeros(num_params, shape[-2], 1, dtype=dtype, device=device)
|
| 157 |
-
else:
|
| 158 |
-
state["second_momentum_buffer"] = torch.zeros(num_params, 1, shape[-1], dtype=dtype, device=device)
|
| 159 |
-
second_momentum_buffer = state["second_momentum_buffer"] # (12, 1, 3072)
|
| 160 |
-
red_dim = -1 if shape[-2] >= shape[-1] else -2 # e.g.: -2
|
| 161 |
-
|
| 162 |
-
# Stack grads and params
|
| 163 |
-
stacked_grads = torch.stack([p.grad for p in params]) # (12, 768, 3072)
|
| 164 |
-
stacked_params = torch.stack(params) # (12, 768, 3072)
|
| 165 |
-
|
| 166 |
-
# Fill all the 0-D tensors with current values
|
| 167 |
-
self._momentum_t.fill_(group["momentum"])
|
| 168 |
-
self._beta2_t.fill_(group["beta2"] if group["beta2"] is not None else 0.0)
|
| 169 |
-
self._lr_t.fill_(group["lr"] * max(1.0, shape[-2] / shape[-1])**0.5)
|
| 170 |
-
self._wd_t.fill_(group["weight_decay"])
|
| 171 |
-
|
| 172 |
-
# Single fused kernel: momentum -> polar_express -> variance_reduction -> update
|
| 173 |
-
muon_step_fused(
|
| 174 |
-
stacked_grads,
|
| 175 |
-
stacked_params,
|
| 176 |
-
momentum_buffer,
|
| 177 |
-
second_momentum_buffer,
|
| 178 |
-
self._momentum_t,
|
| 179 |
-
self._lr_t,
|
| 180 |
-
self._wd_t,
|
| 181 |
-
self._beta2_t,
|
| 182 |
-
group["ns_steps"],
|
| 183 |
-
red_dim,
|
| 184 |
-
)
|
| 185 |
-
|
| 186 |
-
# Copy back to original params: [(768, 3072), (768, 3072), ...] <- (12, 768, 3072)
|
| 187 |
-
torch._foreach_copy_(params, list(stacked_params.unbind(0)))
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
class DistMuon(torch.optim.Optimizer):
|
| 191 |
-
"""
|
| 192 |
-
Distributed version of the Muon optimizer.
|
| 193 |
-
"""
|
| 194 |
-
def __init__(self, params, lr: float = 0.02, momentum: float = 0.95,
|
| 195 |
-
ns_steps: int = 5, beta2: float = 0.95, weight_decay: float = 0.0):
|
| 196 |
-
defaults = dict(lr=lr, momentum=momentum, ns_steps=ns_steps, beta2=beta2, weight_decay=weight_decay)
|
| 197 |
-
assert all(p.ndim == 2 for p in params), "Muon expects 2D parameters only"
|
| 198 |
-
params = list(params)
|
| 199 |
-
world_size = dist.get_world_size()
|
| 200 |
-
rank = dist.get_rank()
|
| 201 |
-
# Group all parameters by their shape
|
| 202 |
-
shapes = sorted({p.shape for p in params}) # sort for deterministic ordering across ranks
|
| 203 |
-
param_groups = []
|
| 204 |
-
for shape in shapes:
|
| 205 |
-
group_params = [p for p in params if p.shape == shape]
|
| 206 |
-
device, dtype = group_params[0].device, group_params[0].dtype
|
| 207 |
-
assert all(p.device == device for p in group_params)
|
| 208 |
-
assert all(p.dtype == dtype for p in group_params)
|
| 209 |
-
# Compute chunk size for this group (how many params each rank owns)
|
| 210 |
-
chunk_size = (len(group_params) + world_size - 1) // world_size
|
| 211 |
-
if rank == 0:
|
| 212 |
-
print(f"Muon: {len(group_params)} params of shape {shape}, chunk_size={chunk_size}")
|
| 213 |
-
param_groups.append(dict(params=group_params, chunk_size=chunk_size))
|
| 214 |
-
super().__init__(param_groups, defaults)
|
| 215 |
-
# 0-D CPU tensors to avoid torch.compile recompilation when values change
|
| 216 |
-
self._momentum_t = torch.tensor(0.0, dtype=torch.float32, device="cpu")
|
| 217 |
-
self._lr_t = torch.tensor(0.0, dtype=torch.float32, device="cpu")
|
| 218 |
-
self._wd_t = torch.tensor(0.0, dtype=torch.float32, device="cpu")
|
| 219 |
-
self._beta2_t = torch.tensor(0.0, dtype=torch.float32, device="cpu")
|
| 220 |
-
|
| 221 |
-
@torch.no_grad()
|
| 222 |
-
def step(self):
|
| 223 |
-
rank = dist.get_rank()
|
| 224 |
-
world_size = dist.get_world_size()
|
| 225 |
-
|
| 226 |
-
# Ensure all grads exist
|
| 227 |
-
assert all(p.grad is not None for group in self.param_groups for p in group["params"]), "All params must have grads"
|
| 228 |
-
|
| 229 |
-
# First pass: stack grads and kick off reduce_scatter for each group
|
| 230 |
-
group_infos = []
|
| 231 |
-
for group in self.param_groups:
|
| 232 |
-
params: list[Tensor] = group["params"]
|
| 233 |
-
chunk_size = group["chunk_size"]
|
| 234 |
-
padded_num_params = chunk_size * world_size
|
| 235 |
-
shape = params[0].shape
|
| 236 |
-
device, dtype = params[0].device, params[0].dtype
|
| 237 |
-
|
| 238 |
-
# Stack all gradients into a single tensor (single kernel via torch.stack)
|
| 239 |
-
grad_stack = torch.stack([p.grad for p in params])
|
| 240 |
-
stacked_grads = torch.empty(padded_num_params, *shape, dtype=dtype, device=device)
|
| 241 |
-
stacked_grads[:len(params)].copy_(grad_stack)
|
| 242 |
-
# Zero-pad if we have fewer params than padded size
|
| 243 |
-
if len(params) < padded_num_params:
|
| 244 |
-
stacked_grads[len(params):].zero_()
|
| 245 |
-
|
| 246 |
-
# Output buffer for this rank's chunk
|
| 247 |
-
grad_chunk = torch.empty(chunk_size, *shape, dtype=dtype, device=device)
|
| 248 |
-
|
| 249 |
-
# Async reduce_scatter on the stacked tensor
|
| 250 |
-
reduce_future = dist.reduce_scatter_tensor(
|
| 251 |
-
grad_chunk, stacked_grads, op=dist.ReduceOp.AVG, async_op=True
|
| 252 |
-
).get_future()
|
| 253 |
-
|
| 254 |
-
group_infos.append(dict(
|
| 255 |
-
grad_chunk=grad_chunk,
|
| 256 |
-
reduce_future=reduce_future,
|
| 257 |
-
stacked_grads=stacked_grads, # reuse for all_gather output
|
| 258 |
-
))
|
| 259 |
-
|
| 260 |
-
# Second pass: wait for reduce, compute batched updates, kick off all_gather
|
| 261 |
-
all_gather_futures = []
|
| 262 |
-
for group, info in zip(self.param_groups, group_infos):
|
| 263 |
-
info["reduce_future"].wait()
|
| 264 |
-
|
| 265 |
-
params = group["params"]
|
| 266 |
-
chunk_size = group["chunk_size"]
|
| 267 |
-
shape = params[0].shape
|
| 268 |
-
device, dtype = params[0].device, params[0].dtype
|
| 269 |
-
grad_chunk = info["grad_chunk"]
|
| 270 |
-
|
| 271 |
-
# How many params does this rank actually own?
|
| 272 |
-
start_idx = rank * chunk_size
|
| 273 |
-
num_owned = min(chunk_size, max(0, len(params) - start_idx))
|
| 274 |
-
|
| 275 |
-
# Get or create group-level state (stored keyed by first param)
|
| 276 |
-
state = self.state[params[0]]
|
| 277 |
-
|
| 278 |
-
# Momentum buffer
|
| 279 |
-
if "momentum_buffer" not in state:
|
| 280 |
-
state["momentum_buffer"] = torch.zeros(chunk_size, *shape, dtype=dtype, device=device)
|
| 281 |
-
momentum_buffer = state["momentum_buffer"]
|
| 282 |
-
|
| 283 |
-
# Second momentum buffer is factored, either per-row or per-column
|
| 284 |
-
if "second_momentum_buffer" not in state:
|
| 285 |
-
if shape[-2] >= shape[-1]:
|
| 286 |
-
state["second_momentum_buffer"] = torch.zeros(chunk_size, shape[-2], 1, dtype=dtype, device=device)
|
| 287 |
-
else:
|
| 288 |
-
state["second_momentum_buffer"] = torch.zeros(chunk_size, 1, shape[-1], dtype=dtype, device=device)
|
| 289 |
-
second_momentum_buffer = state["second_momentum_buffer"]
|
| 290 |
-
red_dim = -1 if shape[-2] >= shape[-1] else -2
|
| 291 |
-
|
| 292 |
-
# Build updated_params tensor for all_gather
|
| 293 |
-
updated_params = torch.empty(chunk_size, *shape, dtype=dtype, device=device)
|
| 294 |
-
|
| 295 |
-
if num_owned > 0:
|
| 296 |
-
# Stack owned params (single kernel via torch.stack)
|
| 297 |
-
owned_params = [params[start_idx + i] for i in range(num_owned)]
|
| 298 |
-
stacked_owned_params = torch.stack(owned_params)
|
| 299 |
-
|
| 300 |
-
# Get owned slices of buffers and grads
|
| 301 |
-
owned_grads = grad_chunk[:num_owned]
|
| 302 |
-
owned_momentum = momentum_buffer[:num_owned]
|
| 303 |
-
owned_second_momentum = second_momentum_buffer[:num_owned]
|
| 304 |
-
|
| 305 |
-
# Fill 0-D tensors with current values
|
| 306 |
-
self._momentum_t.fill_(group["momentum"])
|
| 307 |
-
self._beta2_t.fill_(group["beta2"] if group["beta2"] is not None else 0.0)
|
| 308 |
-
self._lr_t.fill_(group["lr"] * max(1.0, shape[-2] / shape[-1])**0.5)
|
| 309 |
-
self._wd_t.fill_(group["weight_decay"])
|
| 310 |
-
|
| 311 |
-
# Single fused kernel: momentum -> polar_express -> variance_reduction -> update
|
| 312 |
-
muon_step_fused(
|
| 313 |
-
owned_grads,
|
| 314 |
-
stacked_owned_params,
|
| 315 |
-
owned_momentum,
|
| 316 |
-
owned_second_momentum,
|
| 317 |
-
self._momentum_t,
|
| 318 |
-
self._lr_t,
|
| 319 |
-
self._wd_t,
|
| 320 |
-
self._beta2_t,
|
| 321 |
-
group["ns_steps"],
|
| 322 |
-
red_dim,
|
| 323 |
-
)
|
| 324 |
-
|
| 325 |
-
# Copy updated params to output buffer
|
| 326 |
-
updated_params[:num_owned].copy_(stacked_owned_params)
|
| 327 |
-
|
| 328 |
-
# Zero-pad the rest (for ranks that own fewer params)
|
| 329 |
-
if num_owned < chunk_size:
|
| 330 |
-
updated_params[num_owned:].zero_()
|
| 331 |
-
|
| 332 |
-
# Reuse stacked_grads buffer for all_gather output
|
| 333 |
-
stacked_params = info["stacked_grads"]
|
| 334 |
-
|
| 335 |
-
# Async all_gather to replicate updated params to all ranks
|
| 336 |
-
gather_future = dist.all_gather_into_tensor(
|
| 337 |
-
stacked_params, updated_params, async_op=True
|
| 338 |
-
).get_future()
|
| 339 |
-
|
| 340 |
-
all_gather_futures.append(dict(
|
| 341 |
-
gather_future=gather_future,
|
| 342 |
-
stacked_params=stacked_params,
|
| 343 |
-
params=params,
|
| 344 |
-
))
|
| 345 |
-
|
| 346 |
-
# Final pass: wait for all_gather and copy back to params
|
| 347 |
-
for info in all_gather_futures:
|
| 348 |
-
info["gather_future"].wait()
|
| 349 |
-
stacked_params = info["stacked_params"]
|
| 350 |
-
params = info["params"]
|
| 351 |
-
# Batched copy back (single kernel instead of N individual copies)
|
| 352 |
-
torch._foreach_copy_(params, list(stacked_params[:len(params)].unbind(0)))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
nanochat/optim.py
ADDED
|
@@ -0,0 +1,528 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
A nice and efficient mixed AdamW/Muon Combined Optimizer.
|
| 3 |
+
Usually the embeddings and scalars go into AdamW, and the matrix parameters go into Muon.
|
| 4 |
+
Two versions are provided (MuonAdamW, DistMuonAdamW), for single GPU and distributed.
|
| 5 |
+
|
| 6 |
+
Addapted from: https://github.com/KellerJordan/modded-nanogpt
|
| 7 |
+
Further contributions from @karpathy and @chrisjmccormick.
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
import torch
|
| 11 |
+
import torch.distributed as dist
|
| 12 |
+
from torch import Tensor
|
| 13 |
+
|
| 14 |
+
# -----------------------------------------------------------------------------
|
| 15 |
+
"""
|
| 16 |
+
Good old AdamW optimizer, fused kernel.
|
| 17 |
+
https://arxiv.org/abs/1711.05101
|
| 18 |
+
"""
|
| 19 |
+
|
| 20 |
+
@torch.compile(dynamic=False, fullgraph=True)
|
| 21 |
+
def adamw_step_fused(
|
| 22 |
+
p: Tensor, # (32768, 768) - parameter tensor
|
| 23 |
+
grad: Tensor, # (32768, 768) - gradient, same shape as p
|
| 24 |
+
exp_avg: Tensor, # (32768, 768) - first moment, same shape as p
|
| 25 |
+
exp_avg_sq: Tensor, # (32768, 768) - second moment, same shape as p
|
| 26 |
+
step_t: Tensor, # () - 0-D CPU tensor, step count
|
| 27 |
+
lr_t: Tensor, # () - 0-D CPU tensor, learning rate
|
| 28 |
+
beta1_t: Tensor, # () - 0-D CPU tensor, beta1
|
| 29 |
+
beta2_t: Tensor, # () - 0-D CPU tensor, beta2
|
| 30 |
+
eps_t: Tensor, # () - 0-D CPU tensor, epsilon
|
| 31 |
+
wd_t: Tensor, # () - 0-D CPU tensor, weight decay
|
| 32 |
+
) -> None:
|
| 33 |
+
"""
|
| 34 |
+
Fused AdamW step: weight_decay -> momentum_update -> bias_correction -> param_update
|
| 35 |
+
All in one compiled graph to eliminate Python overhead between ops.
|
| 36 |
+
The 0-D CPU tensors avoid recompilation when hyperparameter values change.
|
| 37 |
+
"""
|
| 38 |
+
# Weight decay (decoupled, applied before the update)
|
| 39 |
+
p.mul_(1 - lr_t * wd_t)
|
| 40 |
+
# Update running averages (lerp_ is cleaner and fuses well)
|
| 41 |
+
exp_avg.lerp_(grad, 1 - beta1_t)
|
| 42 |
+
exp_avg_sq.lerp_(grad.square(), 1 - beta2_t)
|
| 43 |
+
# Bias corrections
|
| 44 |
+
bias1 = 1 - beta1_t ** step_t
|
| 45 |
+
bias2 = 1 - beta2_t ** step_t
|
| 46 |
+
# Compute update and apply
|
| 47 |
+
denom = (exp_avg_sq / bias2).sqrt() + eps_t
|
| 48 |
+
step_size = lr_t / bias1
|
| 49 |
+
p.add_(exp_avg / denom, alpha=-step_size)
|
| 50 |
+
|
| 51 |
+
# -----------------------------------------------------------------------------
|
| 52 |
+
"""
|
| 53 |
+
Muon optimizer adapted and simplified from modded-nanogpt.
|
| 54 |
+
https://github.com/KellerJordan/modded-nanogpt
|
| 55 |
+
|
| 56 |
+
Background:
|
| 57 |
+
Newton-Schulz iteration to compute the zeroth power / orthogonalization of G. We opt to use a
|
| 58 |
+
quintic iteration whose coefficients are selected to maximize the slope at zero. For the purpose
|
| 59 |
+
of minimizing steps, it turns out to be empirically effective to keep increasing the slope at
|
| 60 |
+
zero even beyond the point where the iteration no longer converges all the way to one everywhere
|
| 61 |
+
on the interval. This iteration therefore does not produce UV^T but rather something like US'V^T
|
| 62 |
+
where S' is diagonal with S_{ii}' ~ Uniform(0.5, 1.5), which turns out not to hurt model
|
| 63 |
+
performance at all relative to UV^T, where USV^T = G is the SVD.
|
| 64 |
+
|
| 65 |
+
Here, an alternative to Newton-Schulz iteration with potentially better convergence properties:
|
| 66 |
+
Polar Express Sign Method for orthogonalization.
|
| 67 |
+
https://arxiv.org/pdf/2505.16932
|
| 68 |
+
by Noah Amsel, David Persson, Christopher Musco, Robert M. Gower.
|
| 69 |
+
|
| 70 |
+
Some of the changes in nanochat implementation:
|
| 71 |
+
- Uses a simpler, more general approach to parameter grouping and stacking
|
| 72 |
+
- Uses a single fused kernel for the momentum -> polar_express -> variance_reduction -> update step
|
| 73 |
+
- Makes no assumptions about model architecture (e.g. that attention weights are fused into QKVO format)
|
| 74 |
+
"""
|
| 75 |
+
|
| 76 |
+
# Coefficients for Polar Express (computed for num_iters=5, safety_factor=2e-2, cushion=2)
|
| 77 |
+
# From https://arxiv.org/pdf/2505.16932
|
| 78 |
+
polar_express_coeffs = [
|
| 79 |
+
(8.156554524902461, -22.48329292557795, 15.878769915207462),
|
| 80 |
+
(4.042929935166739, -2.808917465908714, 0.5000178451051316),
|
| 81 |
+
(3.8916678022926607, -2.772484153217685, 0.5060648178503393),
|
| 82 |
+
(3.285753657755655, -2.3681294933425376, 0.46449024233003106),
|
| 83 |
+
(2.3465413258596377, -1.7097828382687081, 0.42323551169305323),
|
| 84 |
+
]
|
| 85 |
+
|
| 86 |
+
@torch.compile(dynamic=False, fullgraph=True)
|
| 87 |
+
def muon_step_fused(
|
| 88 |
+
stacked_grads: Tensor, # (12, 768, 3072) - stacked gradients
|
| 89 |
+
stacked_params: Tensor, # (12, 768, 3072) - stacked parameters
|
| 90 |
+
momentum_buffer: Tensor, # (12, 768, 3072) - first moment buffer
|
| 91 |
+
second_momentum_buffer: Tensor, # (12, 768, 1) or (12, 1, 3072) - factored second moment
|
| 92 |
+
momentum_t: Tensor, # () - 0-D CPU tensor, momentum coefficient
|
| 93 |
+
lr_t: Tensor, # () - 0-D CPU tensor, learning rate
|
| 94 |
+
wd_t: Tensor, # () - 0-D CPU tensor, weight decay
|
| 95 |
+
beta2_t: Tensor, # () - 0-D CPU tensor, beta2 for second moment
|
| 96 |
+
ns_steps: int, # 5 - number of Newton-Schulz/Polar Express iterations
|
| 97 |
+
red_dim: int, # -1 or -2 - reduction dimension for variance
|
| 98 |
+
) -> None:
|
| 99 |
+
"""
|
| 100 |
+
Fused Muon step: momentum -> polar_express -> variance_reduction -> cautious_update
|
| 101 |
+
All in one compiled graph to eliminate Python overhead between ops.
|
| 102 |
+
Some of the constants are 0-D CPU tensors to avoid recompilation when values change.
|
| 103 |
+
"""
|
| 104 |
+
|
| 105 |
+
# Nesterov momentum
|
| 106 |
+
momentum = momentum_t.to(stacked_grads.dtype)
|
| 107 |
+
momentum_buffer.lerp_(stacked_grads, 1 - momentum)
|
| 108 |
+
g = stacked_grads.lerp_(momentum_buffer, momentum)
|
| 109 |
+
|
| 110 |
+
# Polar express
|
| 111 |
+
X = g.bfloat16()
|
| 112 |
+
X = X / (X.norm(dim=(-2, -1), keepdim=True) * 1.02 + 1e-6)
|
| 113 |
+
if g.size(-2) > g.size(-1): # Tall matrix
|
| 114 |
+
for a, b, c in polar_express_coeffs[:ns_steps]:
|
| 115 |
+
A = X.mT @ X
|
| 116 |
+
B = b * A + c * (A @ A)
|
| 117 |
+
X = a * X + X @ B
|
| 118 |
+
else: # Wide matrix (original math)
|
| 119 |
+
for a, b, c in polar_express_coeffs[:ns_steps]:
|
| 120 |
+
A = X @ X.mT
|
| 121 |
+
B = b * A + c * (A @ A)
|
| 122 |
+
X = a * X + B @ X
|
| 123 |
+
g = X
|
| 124 |
+
|
| 125 |
+
# Variance reduction
|
| 126 |
+
beta2 = beta2_t.to(g.dtype)
|
| 127 |
+
v_mean = g.float().square().mean(dim=red_dim, keepdim=True)
|
| 128 |
+
red_dim_size = g.size(red_dim)
|
| 129 |
+
v_norm_sq = v_mean.sum(dim=(-2, -1), keepdim=True) * red_dim_size
|
| 130 |
+
v_norm = v_norm_sq.sqrt()
|
| 131 |
+
second_momentum_buffer.lerp_(v_mean.to(dtype=second_momentum_buffer.dtype), 1 - beta2)
|
| 132 |
+
step_size = second_momentum_buffer.clamp_min(1e-10).rsqrt()
|
| 133 |
+
scaled_sq_sum = (v_mean * red_dim_size) * step_size.float().square()
|
| 134 |
+
v_norm_new = scaled_sq_sum.sum(dim=(-2, -1), keepdim=True).sqrt()
|
| 135 |
+
final_scale = step_size * (v_norm / v_norm_new.clamp_min(1e-10))
|
| 136 |
+
g = g * final_scale.to(g.dtype)
|
| 137 |
+
|
| 138 |
+
# Cautious weight decay + parameter update
|
| 139 |
+
lr = lr_t.to(g.dtype)
|
| 140 |
+
wd = wd_t.to(g.dtype)
|
| 141 |
+
mask = (g * stacked_params) >= 0
|
| 142 |
+
stacked_params.sub_(lr * g + lr * wd * stacked_params * mask)
|
| 143 |
+
|
| 144 |
+
# -----------------------------------------------------------------------------
|
| 145 |
+
# Single GPU version of the MuonAdamW optimizer.
|
| 146 |
+
# Used mostly for reference, debugging and testing.
|
| 147 |
+
|
| 148 |
+
class MuonAdamW(torch.optim.Optimizer):
|
| 149 |
+
"""
|
| 150 |
+
Combined optimizer: Muon for 2D matrix params, AdamW for others, single GPU version.
|
| 151 |
+
|
| 152 |
+
AdamW - Fused AdamW optimizer step.
|
| 153 |
+
|
| 154 |
+
Muon - MomentUm Orthogonalized by Newton-schulz
|
| 155 |
+
https://kellerjordan.github.io/posts/muon/
|
| 156 |
+
|
| 157 |
+
Muon internally runs standard SGD-momentum, and then performs an orthogonalization post-
|
| 158 |
+
processing step, in which each 2D parameter's update is replaced with the nearest orthogonal
|
| 159 |
+
matrix. To efficiently orthogonalize each update, we use a Newton-Schulz iteration, which has
|
| 160 |
+
the advantage that it can be stably run in bfloat16 on the GPU.
|
| 161 |
+
|
| 162 |
+
Some warnings:
|
| 163 |
+
- The Muon optimizer should not be used for the embedding layer, the final fully connected layer,
|
| 164 |
+
or any {0,1}-D parameters; those should all be optimized by a standard method (e.g., AdamW).
|
| 165 |
+
- To use it with 4D convolutional filters, it works well to just flatten their last 3 dimensions.
|
| 166 |
+
|
| 167 |
+
Arguments:
|
| 168 |
+
param_groups: List of dicts, each containing:
|
| 169 |
+
- 'params': List of parameters
|
| 170 |
+
- 'kind': 'adamw' or 'muon'
|
| 171 |
+
- For AdamW groups: 'lr', 'betas', 'eps', 'weight_decay'
|
| 172 |
+
- For Muon groups: 'lr', 'momentum', 'ns_steps', 'beta2', 'weight_decay'
|
| 173 |
+
"""
|
| 174 |
+
def __init__(self, param_groups: list[dict]):
|
| 175 |
+
super().__init__(param_groups, defaults={})
|
| 176 |
+
# 0-D CPU tensors to avoid torch.compile recompilation when values change
|
| 177 |
+
# AdamW tensors
|
| 178 |
+
self._adamw_step_t = torch.tensor(0.0, dtype=torch.float32, device="cpu")
|
| 179 |
+
self._adamw_lr_t = torch.tensor(0.0, dtype=torch.float32, device="cpu")
|
| 180 |
+
self._adamw_beta1_t = torch.tensor(0.0, dtype=torch.float32, device="cpu")
|
| 181 |
+
self._adamw_beta2_t = torch.tensor(0.0, dtype=torch.float32, device="cpu")
|
| 182 |
+
self._adamw_eps_t = torch.tensor(0.0, dtype=torch.float32, device="cpu")
|
| 183 |
+
self._adamw_wd_t = torch.tensor(0.0, dtype=torch.float32, device="cpu")
|
| 184 |
+
# Muon tensors
|
| 185 |
+
self._muon_momentum_t = torch.tensor(0.0, dtype=torch.float32, device="cpu")
|
| 186 |
+
self._muon_lr_t = torch.tensor(0.0, dtype=torch.float32, device="cpu")
|
| 187 |
+
self._muon_wd_t = torch.tensor(0.0, dtype=torch.float32, device="cpu")
|
| 188 |
+
self._muon_beta2_t = torch.tensor(0.0, dtype=torch.float32, device="cpu")
|
| 189 |
+
|
| 190 |
+
def _step_adamw(self, group: dict) -> None:
|
| 191 |
+
"""
|
| 192 |
+
AdamW update for each param in the group individually.
|
| 193 |
+
Lazy init the state, fill in all 0-D tensors, call the fused kernel.
|
| 194 |
+
"""
|
| 195 |
+
for p in group['params']:
|
| 196 |
+
if p.grad is None:
|
| 197 |
+
continue
|
| 198 |
+
grad = p.grad
|
| 199 |
+
state = self.state[p]
|
| 200 |
+
|
| 201 |
+
# State init
|
| 202 |
+
if not state:
|
| 203 |
+
state['step'] = 0
|
| 204 |
+
state['exp_avg'] = torch.zeros_like(p)
|
| 205 |
+
state['exp_avg_sq'] = torch.zeros_like(p)
|
| 206 |
+
exp_avg = state['exp_avg']
|
| 207 |
+
exp_avg_sq = state['exp_avg_sq']
|
| 208 |
+
state['step'] += 1
|
| 209 |
+
|
| 210 |
+
# Fill 0-D tensors with current values
|
| 211 |
+
self._adamw_step_t.fill_(state['step'])
|
| 212 |
+
self._adamw_lr_t.fill_(group['lr'])
|
| 213 |
+
self._adamw_beta1_t.fill_(group['betas'][0])
|
| 214 |
+
self._adamw_beta2_t.fill_(group['betas'][1])
|
| 215 |
+
self._adamw_eps_t.fill_(group['eps'])
|
| 216 |
+
self._adamw_wd_t.fill_(group['weight_decay'])
|
| 217 |
+
|
| 218 |
+
# Fused update: weight_decay -> momentum -> bias_correction -> param_update
|
| 219 |
+
adamw_step_fused(
|
| 220 |
+
p, grad, exp_avg, exp_avg_sq,
|
| 221 |
+
self._adamw_step_t, self._adamw_lr_t, self._adamw_beta1_t,
|
| 222 |
+
self._adamw_beta2_t, self._adamw_eps_t, self._adamw_wd_t,
|
| 223 |
+
)
|
| 224 |
+
|
| 225 |
+
def _step_muon(self, group: dict) -> None:
|
| 226 |
+
"""
|
| 227 |
+
Muon update for all params in the group (stacked for efficiency).
|
| 228 |
+
Lazy init the state, fill in all 0-D tensors, call the fused kernel.
|
| 229 |
+
"""
|
| 230 |
+
params: list[Tensor] = group['params']
|
| 231 |
+
if not params:
|
| 232 |
+
return
|
| 233 |
+
|
| 234 |
+
# Get or create group-level buffers (stored in first param's state for convenience)
|
| 235 |
+
p = params[0]
|
| 236 |
+
state = self.state[p]
|
| 237 |
+
num_params = len(params)
|
| 238 |
+
shape, device, dtype = p.shape, p.device, p.dtype
|
| 239 |
+
|
| 240 |
+
# Momentum for every individual parameter
|
| 241 |
+
if "momentum_buffer" not in state:
|
| 242 |
+
state["momentum_buffer"] = torch.zeros(num_params, *shape, dtype=dtype, device=device)
|
| 243 |
+
momentum_buffer = state["momentum_buffer"]
|
| 244 |
+
|
| 245 |
+
# Second momentum buffer is factored, either per-row or per-column
|
| 246 |
+
if "second_momentum_buffer" not in state:
|
| 247 |
+
state_shape = (num_params, shape[-2], 1) if shape[-2] >= shape[-1] else (num_params, 1, shape[-1])
|
| 248 |
+
state["second_momentum_buffer"] = torch.zeros(state_shape, dtype=dtype, device=device)
|
| 249 |
+
second_momentum_buffer = state["second_momentum_buffer"]
|
| 250 |
+
red_dim = -1 if shape[-2] >= shape[-1] else -2
|
| 251 |
+
|
| 252 |
+
# Stack grads and params (NOTE: this assumes all params have the same shape)
|
| 253 |
+
stacked_grads = torch.stack([p.grad for p in params])
|
| 254 |
+
stacked_params = torch.stack(params)
|
| 255 |
+
|
| 256 |
+
# Fill all the 0-D tensors with current values
|
| 257 |
+
self._muon_momentum_t.fill_(group["momentum"])
|
| 258 |
+
self._muon_beta2_t.fill_(group["beta2"] if group["beta2"] is not None else 0.0)
|
| 259 |
+
self._muon_lr_t.fill_(group["lr"] * max(1.0, shape[-2] / shape[-1])**0.5)
|
| 260 |
+
self._muon_wd_t.fill_(group["weight_decay"])
|
| 261 |
+
|
| 262 |
+
# Single fused kernel: momentum -> polar_express -> variance_reduction -> update
|
| 263 |
+
muon_step_fused(
|
| 264 |
+
stacked_grads,
|
| 265 |
+
stacked_params,
|
| 266 |
+
momentum_buffer,
|
| 267 |
+
second_momentum_buffer,
|
| 268 |
+
self._muon_momentum_t,
|
| 269 |
+
self._muon_lr_t,
|
| 270 |
+
self._muon_wd_t,
|
| 271 |
+
self._muon_beta2_t,
|
| 272 |
+
group["ns_steps"],
|
| 273 |
+
red_dim,
|
| 274 |
+
)
|
| 275 |
+
|
| 276 |
+
# Copy back to original params
|
| 277 |
+
torch._foreach_copy_(params, list(stacked_params.unbind(0)))
|
| 278 |
+
|
| 279 |
+
@torch.no_grad()
|
| 280 |
+
def step(self):
|
| 281 |
+
for group in self.param_groups:
|
| 282 |
+
if group['kind'] == 'adamw':
|
| 283 |
+
self._step_adamw(group)
|
| 284 |
+
elif group['kind'] == 'muon':
|
| 285 |
+
self._step_muon(group)
|
| 286 |
+
else:
|
| 287 |
+
raise ValueError(f"Unknown optimizer kind: {group['kind']}")
|
| 288 |
+
|
| 289 |
+
# -----------------------------------------------------------------------------
|
| 290 |
+
# Distributed version of the MuonAdamW optimizer.
|
| 291 |
+
# Used for training on multiple GPUs.
|
| 292 |
+
|
| 293 |
+
class DistMuonAdamW(torch.optim.Optimizer):
|
| 294 |
+
"""
|
| 295 |
+
Combined distributed optimizer: Muon for 2D matrix params, AdamW for others.
|
| 296 |
+
|
| 297 |
+
See MuonAdamW for the algorithmic details of each optimizer. This class adds
|
| 298 |
+
distributed communication to enable multi-GPU training without PyTorch DDP.
|
| 299 |
+
|
| 300 |
+
Design Goals:
|
| 301 |
+
- Overlap communication with computation (async ops)
|
| 302 |
+
- Minimize memory by sharding optimizer states across ranks (ZeRO-2 style)
|
| 303 |
+
- Batch small tensors into single comm ops where possible
|
| 304 |
+
|
| 305 |
+
Communication Pattern (3-phase async):
|
| 306 |
+
We use a 3-phase structure to maximize overlap between communication and compute:
|
| 307 |
+
|
| 308 |
+
Phase 1: Launch all async reduce ops
|
| 309 |
+
- Kick off all reduce_scatter/all_reduce operations
|
| 310 |
+
- Don't wait - let them run in background while we continue
|
| 311 |
+
|
| 312 |
+
Phase 2: Wait for reduces, compute updates, launch gathers
|
| 313 |
+
- For each group: wait for its reduce, compute the update, launch gather
|
| 314 |
+
- By processing groups in order, earlier gathers run while later computes happen
|
| 315 |
+
|
| 316 |
+
Phase 3: Wait for gathers, copy back
|
| 317 |
+
- Wait for all gathers to complete
|
| 318 |
+
- Copy updated params back to original tensors (Muon only)
|
| 319 |
+
|
| 320 |
+
AdamW Communication (ZeRO-2 style):
|
| 321 |
+
- Small params (<1024 elements): all_reduce gradients, update full param on each rank.
|
| 322 |
+
Optimizer state is replicated but these params are tiny (scalars, biases).
|
| 323 |
+
- Large params: reduce_scatter gradients so each rank gets 1/N of the grad, update
|
| 324 |
+
only that slice, then all_gather the updated slices. Optimizer state (exp_avg,
|
| 325 |
+
exp_avg_sq) is sharded - each rank only stores state for its slice.
|
| 326 |
+
Requires param.shape[0] divisible by world_size.
|
| 327 |
+
|
| 328 |
+
Muon Communication (stacked + chunked):
|
| 329 |
+
- All params in a Muon group must have the same shape (caller's responsibility).
|
| 330 |
+
- Stack all K params into a single (K, *shape) tensor for efficient comm.
|
| 331 |
+
- Divide K params across N ranks: each rank "owns" ceil(K/N) params.
|
| 332 |
+
- reduce_scatter the stacked grads so each rank gets its chunk.
|
| 333 |
+
- Each rank computes Muon update only for params it owns.
|
| 334 |
+
- all_gather the updated params back to all ranks.
|
| 335 |
+
- Optimizer state (momentum_buffer, second_momentum_buffer) is sharded by chunk.
|
| 336 |
+
- Padding: if K doesn't divide evenly, we zero-pad to (ceil(K/N) * N) for comm,
|
| 337 |
+
then ignore the padding when copying back.
|
| 338 |
+
|
| 339 |
+
Buffer Reuse:
|
| 340 |
+
- For Muon, we allocate stacked_grads for reduce_scatter input, then reuse the
|
| 341 |
+
same buffer as the output for all_gather (stacked_params). This saves memory
|
| 342 |
+
since we don't need both buffers simultaneously.
|
| 343 |
+
|
| 344 |
+
Arguments:
|
| 345 |
+
param_groups: List of dicts, each containing:
|
| 346 |
+
- 'params': List of parameters
|
| 347 |
+
- 'kind': 'adamw' or 'muon'
|
| 348 |
+
- For AdamW groups: 'lr', 'betas', 'eps', 'weight_decay'
|
| 349 |
+
- For Muon groups: 'lr', 'momentum', 'ns_steps', 'beta2', 'weight_decay'
|
| 350 |
+
"""
|
| 351 |
+
def __init__(self, param_groups: list[dict]):
|
| 352 |
+
super().__init__(param_groups, defaults={})
|
| 353 |
+
# 0-D CPU tensors to avoid torch.compile recompilation when values change
|
| 354 |
+
self._adamw_step_t = torch.tensor(0.0, dtype=torch.float32, device="cpu")
|
| 355 |
+
self._adamw_lr_t = torch.tensor(0.0, dtype=torch.float32, device="cpu")
|
| 356 |
+
self._adamw_beta1_t = torch.tensor(0.0, dtype=torch.float32, device="cpu")
|
| 357 |
+
self._adamw_beta2_t = torch.tensor(0.0, dtype=torch.float32, device="cpu")
|
| 358 |
+
self._adamw_eps_t = torch.tensor(0.0, dtype=torch.float32, device="cpu")
|
| 359 |
+
self._adamw_wd_t = torch.tensor(0.0, dtype=torch.float32, device="cpu")
|
| 360 |
+
self._muon_momentum_t = torch.tensor(0.0, dtype=torch.float32, device="cpu")
|
| 361 |
+
self._muon_lr_t = torch.tensor(0.0, dtype=torch.float32, device="cpu")
|
| 362 |
+
self._muon_wd_t = torch.tensor(0.0, dtype=torch.float32, device="cpu")
|
| 363 |
+
self._muon_beta2_t = torch.tensor(0.0, dtype=torch.float32, device="cpu")
|
| 364 |
+
|
| 365 |
+
def _reduce_adamw(self, group: dict, world_size: int) -> dict:
|
| 366 |
+
"""Launch async reduce ops for AdamW group. Returns info dict with per-param infos."""
|
| 367 |
+
param_infos = {}
|
| 368 |
+
for p in group['params']:
|
| 369 |
+
grad = p.grad
|
| 370 |
+
if p.numel() < 1024:
|
| 371 |
+
# Small params: all_reduce (no scatter/gather needed)
|
| 372 |
+
future = dist.all_reduce(grad, op=dist.ReduceOp.AVG, async_op=True).get_future()
|
| 373 |
+
param_infos[p] = dict(future=future, grad_slice=grad, is_small=True)
|
| 374 |
+
else:
|
| 375 |
+
# Large params: reduce_scatter
|
| 376 |
+
rank_size = grad.shape[0] // world_size
|
| 377 |
+
grad_slice = torch.empty_like(grad[:rank_size])
|
| 378 |
+
future = dist.reduce_scatter_tensor(grad_slice, grad, op=dist.ReduceOp.AVG, async_op=True).get_future()
|
| 379 |
+
param_infos[p] = dict(future=future, grad_slice=grad_slice, is_small=False)
|
| 380 |
+
return dict(param_infos=param_infos)
|
| 381 |
+
|
| 382 |
+
def _reduce_muon(self, group: dict, world_size: int) -> dict:
|
| 383 |
+
"""Launch async reduce op for Muon group. Returns info dict."""
|
| 384 |
+
params = group['params']
|
| 385 |
+
chunk_size = (len(params) + world_size - 1) // world_size
|
| 386 |
+
padded_num_params = chunk_size * world_size
|
| 387 |
+
p = params[0]
|
| 388 |
+
shape, device, dtype = p.shape, p.device, p.dtype
|
| 389 |
+
|
| 390 |
+
# Stack grads and zero-pad to padded_num_params
|
| 391 |
+
grad_stack = torch.stack([p.grad for p in params])
|
| 392 |
+
stacked_grads = torch.empty(padded_num_params, *shape, dtype=dtype, device=device)
|
| 393 |
+
stacked_grads[:len(params)].copy_(grad_stack)
|
| 394 |
+
if len(params) < padded_num_params:
|
| 395 |
+
stacked_grads[len(params):].zero_()
|
| 396 |
+
|
| 397 |
+
# Reduce_scatter to get this rank's chunk
|
| 398 |
+
grad_chunk = torch.empty(chunk_size, *shape, dtype=dtype, device=device)
|
| 399 |
+
future = dist.reduce_scatter_tensor(grad_chunk, stacked_grads, op=dist.ReduceOp.AVG, async_op=True).get_future()
|
| 400 |
+
|
| 401 |
+
return dict(future=future, grad_chunk=grad_chunk, stacked_grads=stacked_grads, chunk_size=chunk_size)
|
| 402 |
+
|
| 403 |
+
def _compute_adamw(self, group: dict, info: dict, gather_list: list, rank: int, world_size: int) -> None:
|
| 404 |
+
"""Wait for reduce, compute AdamW updates, launch gathers for large params."""
|
| 405 |
+
param_infos = info['param_infos']
|
| 406 |
+
for p in group['params']:
|
| 407 |
+
pinfo = param_infos[p]
|
| 408 |
+
pinfo['future'].wait()
|
| 409 |
+
grad_slice = pinfo['grad_slice']
|
| 410 |
+
state = self.state[p]
|
| 411 |
+
|
| 412 |
+
# For small params, operate on full param; for large, operate on slice
|
| 413 |
+
if pinfo['is_small']:
|
| 414 |
+
p_slice = p
|
| 415 |
+
else:
|
| 416 |
+
rank_size = p.shape[0] // world_size
|
| 417 |
+
p_slice = p[rank * rank_size:(rank + 1) * rank_size]
|
| 418 |
+
|
| 419 |
+
# State init
|
| 420 |
+
if not state:
|
| 421 |
+
state['step'] = 0
|
| 422 |
+
state['exp_avg'] = torch.zeros_like(p_slice)
|
| 423 |
+
state['exp_avg_sq'] = torch.zeros_like(p_slice)
|
| 424 |
+
state['step'] += 1
|
| 425 |
+
|
| 426 |
+
# Fill 0-D tensors and run fused kernel
|
| 427 |
+
self._adamw_step_t.fill_(state['step'])
|
| 428 |
+
self._adamw_lr_t.fill_(group['lr'])
|
| 429 |
+
self._adamw_beta1_t.fill_(group['betas'][0])
|
| 430 |
+
self._adamw_beta2_t.fill_(group['betas'][1])
|
| 431 |
+
self._adamw_eps_t.fill_(group['eps'])
|
| 432 |
+
self._adamw_wd_t.fill_(group['weight_decay'])
|
| 433 |
+
adamw_step_fused(
|
| 434 |
+
p_slice, grad_slice, state['exp_avg'], state['exp_avg_sq'],
|
| 435 |
+
self._adamw_step_t, self._adamw_lr_t, self._adamw_beta1_t,
|
| 436 |
+
self._adamw_beta2_t, self._adamw_eps_t, self._adamw_wd_t,
|
| 437 |
+
)
|
| 438 |
+
|
| 439 |
+
# Large params need all_gather
|
| 440 |
+
if not pinfo['is_small']:
|
| 441 |
+
future = dist.all_gather_into_tensor(p, p_slice, async_op=True).get_future()
|
| 442 |
+
gather_list.append(dict(future=future, params=None))
|
| 443 |
+
|
| 444 |
+
def _compute_muon(self, group: dict, info: dict, gather_list: list, rank: int) -> None:
|
| 445 |
+
"""Wait for reduce, compute Muon updates, launch gather."""
|
| 446 |
+
info['future'].wait()
|
| 447 |
+
params = group['params']
|
| 448 |
+
chunk_size = info['chunk_size']
|
| 449 |
+
grad_chunk = info['grad_chunk']
|
| 450 |
+
p = params[0]
|
| 451 |
+
shape, device, dtype = p.shape, p.device, p.dtype
|
| 452 |
+
|
| 453 |
+
# How many params does this rank own?
|
| 454 |
+
start_idx = rank * chunk_size
|
| 455 |
+
num_owned = min(chunk_size, max(0, len(params) - start_idx))
|
| 456 |
+
|
| 457 |
+
# Get or create group-level state
|
| 458 |
+
state = self.state[p]
|
| 459 |
+
if "momentum_buffer" not in state:
|
| 460 |
+
state["momentum_buffer"] = torch.zeros(chunk_size, *shape, dtype=dtype, device=device)
|
| 461 |
+
if "second_momentum_buffer" not in state:
|
| 462 |
+
state_shape = (chunk_size, shape[-2], 1) if shape[-2] >= shape[-1] else (chunk_size, 1, shape[-1])
|
| 463 |
+
state["second_momentum_buffer"] = torch.zeros(state_shape, dtype=dtype, device=device)
|
| 464 |
+
red_dim = -1 if shape[-2] >= shape[-1] else -2
|
| 465 |
+
|
| 466 |
+
# Build output buffer for all_gather
|
| 467 |
+
updated_params = torch.empty(chunk_size, *shape, dtype=dtype, device=device)
|
| 468 |
+
|
| 469 |
+
if num_owned > 0:
|
| 470 |
+
owned_params = [params[start_idx + i] for i in range(num_owned)]
|
| 471 |
+
stacked_owned = torch.stack(owned_params)
|
| 472 |
+
|
| 473 |
+
# Fill 0-D tensors and run fused kernel
|
| 474 |
+
self._muon_momentum_t.fill_(group["momentum"])
|
| 475 |
+
self._muon_beta2_t.fill_(group["beta2"])
|
| 476 |
+
self._muon_lr_t.fill_(group["lr"] * max(1.0, shape[-2] / shape[-1])**0.5)
|
| 477 |
+
self._muon_wd_t.fill_(group["weight_decay"])
|
| 478 |
+
muon_step_fused(
|
| 479 |
+
grad_chunk[:num_owned], stacked_owned,
|
| 480 |
+
state["momentum_buffer"][:num_owned], state["second_momentum_buffer"][:num_owned],
|
| 481 |
+
self._muon_momentum_t, self._muon_lr_t, self._muon_wd_t, self._muon_beta2_t,
|
| 482 |
+
group["ns_steps"], red_dim,
|
| 483 |
+
)
|
| 484 |
+
updated_params[:num_owned].copy_(stacked_owned)
|
| 485 |
+
|
| 486 |
+
if num_owned < chunk_size:
|
| 487 |
+
updated_params[num_owned:].zero_()
|
| 488 |
+
|
| 489 |
+
# Reuse stacked_grads buffer for all_gather output
|
| 490 |
+
stacked_params = info["stacked_grads"]
|
| 491 |
+
future = dist.all_gather_into_tensor(stacked_params, updated_params, async_op=True).get_future()
|
| 492 |
+
gather_list.append(dict(future=future, stacked_params=stacked_params, params=params))
|
| 493 |
+
|
| 494 |
+
def _finish_gathers(self, gather_list: list) -> None:
|
| 495 |
+
"""Wait for all gathers and copy Muon params back."""
|
| 496 |
+
for info in gather_list:
|
| 497 |
+
info["future"].wait()
|
| 498 |
+
if info["params"] is not None:
|
| 499 |
+
# Muon: copy from stacked buffer back to individual params
|
| 500 |
+
torch._foreach_copy_(info["params"], list(info["stacked_params"][:len(info["params"])].unbind(0)))
|
| 501 |
+
|
| 502 |
+
@torch.no_grad()
|
| 503 |
+
def step(self):
|
| 504 |
+
rank = dist.get_rank()
|
| 505 |
+
world_size = dist.get_world_size()
|
| 506 |
+
|
| 507 |
+
# Phase 1: launch all async reduce ops
|
| 508 |
+
reduce_infos: list[dict] = []
|
| 509 |
+
for group in self.param_groups:
|
| 510 |
+
if group['kind'] == 'adamw':
|
| 511 |
+
reduce_infos.append(self._reduce_adamw(group, world_size))
|
| 512 |
+
elif group['kind'] == 'muon':
|
| 513 |
+
reduce_infos.append(self._reduce_muon(group, world_size))
|
| 514 |
+
else:
|
| 515 |
+
raise ValueError(f"Unknown optimizer kind: {group['kind']}")
|
| 516 |
+
|
| 517 |
+
# Phase 2: wait for reduces, compute updates, launch gathers
|
| 518 |
+
gather_list: list[dict] = []
|
| 519 |
+
for group, info in zip(self.param_groups, reduce_infos):
|
| 520 |
+
if group['kind'] == 'adamw':
|
| 521 |
+
self._compute_adamw(group, info, gather_list, rank, world_size)
|
| 522 |
+
elif group['kind'] == 'muon':
|
| 523 |
+
self._compute_muon(group, info, gather_list, rank)
|
| 524 |
+
else:
|
| 525 |
+
raise ValueError(f"Unknown optimizer kind: {group['kind']}")
|
| 526 |
+
|
| 527 |
+
# Phase 3: wait for gathers, copy back
|
| 528 |
+
self._finish_gathers(gather_list)
|
scripts/base_train.py
CHANGED
|
@@ -211,9 +211,9 @@ print0(f"Tokens : Scaling params ratio: {args.total_batch_size * num_iterations
|
|
| 211 |
print0(f"Total training FLOPs estimate: {num_flops_per_token * total_tokens:e}")
|
| 212 |
|
| 213 |
# -----------------------------------------------------------------------------
|
| 214 |
-
# Initialize the Optimizer (Muon for
|
| 215 |
adam_betas = (args.adam_beta1, args.adam_beta2)
|
| 216 |
-
|
| 217 |
unembedding_lr=args.unembedding_lr * batch_lr_scale,
|
| 218 |
embedding_lr=args.embedding_lr * batch_lr_scale,
|
| 219 |
matrix_lr=args.matrix_lr * batch_lr_scale,
|
|
@@ -221,12 +221,10 @@ optimizers = model.setup_optimizers(
|
|
| 221 |
adam_betas=adam_betas,
|
| 222 |
scalar_lr=args.scalar_lr * batch_lr_scale,
|
| 223 |
)
|
| 224 |
-
adamw_optimizer, muon_optimizer = optimizers
|
| 225 |
|
| 226 |
if resuming:
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
del optimizer_data # free up the memory
|
| 230 |
|
| 231 |
# -----------------------------------------------------------------------------
|
| 232 |
# Initialize the DataLoaders for train/val
|
|
@@ -344,7 +342,7 @@ while True:
|
|
| 344 |
checkpoint_dir,
|
| 345 |
step,
|
| 346 |
orig_model.state_dict(), # model parameters
|
| 347 |
-
|
| 348 |
{ # metadata saved as json
|
| 349 |
"step": step,
|
| 350 |
"val_bpb": val_bpb, # loss at last step
|
|
@@ -378,18 +376,16 @@ while True:
|
|
| 378 |
loss = loss / grad_accum_steps # each .backward() is a grad sum => normalize loss here
|
| 379 |
loss.backward()
|
| 380 |
x, y, dataloader_state_dict = next(train_loader) # prefetch the next batch while the GPU is busy with forward/backward
|
| 381 |
-
# step the
|
| 382 |
lrm = get_lr_multiplier(step)
|
| 383 |
-
for opt in optimizers:
|
| 384 |
-
for group in opt.param_groups:
|
| 385 |
-
group["lr"] = group["initial_lr"] * lrm
|
| 386 |
muon_momentum = get_muon_momentum(step)
|
| 387 |
muon_weight_decay = get_weight_decay(step)
|
| 388 |
-
for group in
|
| 389 |
-
group["
|
| 390 |
-
group[
|
| 391 |
-
|
| 392 |
-
|
|
|
|
| 393 |
model.zero_grad(set_to_none=True)
|
| 394 |
train_loss_f = train_loss.item() # .item() is a CPU-GPU sync point
|
| 395 |
synchronize()
|
|
|
|
| 211 |
print0(f"Total training FLOPs estimate: {num_flops_per_token * total_tokens:e}")
|
| 212 |
|
| 213 |
# -----------------------------------------------------------------------------
|
| 214 |
+
# Initialize the Optimizer (combined MuonAdamW: Muon for matrix params, AdamW for rest)
|
| 215 |
adam_betas = (args.adam_beta1, args.adam_beta2)
|
| 216 |
+
optimizer = model.setup_optimizer(
|
| 217 |
unembedding_lr=args.unembedding_lr * batch_lr_scale,
|
| 218 |
embedding_lr=args.embedding_lr * batch_lr_scale,
|
| 219 |
matrix_lr=args.matrix_lr * batch_lr_scale,
|
|
|
|
| 221 |
adam_betas=adam_betas,
|
| 222 |
scalar_lr=args.scalar_lr * batch_lr_scale,
|
| 223 |
)
|
|
|
|
| 224 |
|
| 225 |
if resuming:
|
| 226 |
+
optimizer.load_state_dict(optimizer_data)
|
| 227 |
+
del optimizer_data
|
|
|
|
| 228 |
|
| 229 |
# -----------------------------------------------------------------------------
|
| 230 |
# Initialize the DataLoaders for train/val
|
|
|
|
| 342 |
checkpoint_dir,
|
| 343 |
step,
|
| 344 |
orig_model.state_dict(), # model parameters
|
| 345 |
+
optimizer.state_dict(), # optimizer state
|
| 346 |
{ # metadata saved as json
|
| 347 |
"step": step,
|
| 348 |
"val_bpb": val_bpb, # loss at last step
|
|
|
|
| 376 |
loss = loss / grad_accum_steps # each .backward() is a grad sum => normalize loss here
|
| 377 |
loss.backward()
|
| 378 |
x, y, dataloader_state_dict = next(train_loader) # prefetch the next batch while the GPU is busy with forward/backward
|
| 379 |
+
# step the optimizer
|
| 380 |
lrm = get_lr_multiplier(step)
|
|
|
|
|
|
|
|
|
|
| 381 |
muon_momentum = get_muon_momentum(step)
|
| 382 |
muon_weight_decay = get_weight_decay(step)
|
| 383 |
+
for group in optimizer.param_groups:
|
| 384 |
+
group["lr"] = group["initial_lr"] * lrm
|
| 385 |
+
if group['kind'] == 'muon':
|
| 386 |
+
group["momentum"] = muon_momentum
|
| 387 |
+
group["weight_decay"] = muon_weight_decay
|
| 388 |
+
optimizer.step()
|
| 389 |
model.zero_grad(set_to_none=True)
|
| 390 |
train_loss_f = train_loss.item() # .item() is a CPU-GPU sync point
|
| 391 |
synchronize()
|
scripts/chat_rl.py
CHANGED
|
@@ -201,7 +201,7 @@ def run_gsm8k_eval(task, tokenizer, engine,
|
|
| 201 |
# Training loop
|
| 202 |
|
| 203 |
# Init the optimizer
|
| 204 |
-
|
| 205 |
unembedding_lr=args.unembedding_lr,
|
| 206 |
embedding_lr=args.embedding_lr,
|
| 207 |
matrix_lr=args.matrix_lr,
|
|
@@ -209,10 +209,9 @@ optimizers = model.setup_optimizers(
|
|
| 209 |
)
|
| 210 |
|
| 211 |
# Set the initial learning rate as a fraction of the base learning rate
|
| 212 |
-
for
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
group["initial_lr"] = group["lr"] # save the initial learning so we can decay easily later
|
| 216 |
|
| 217 |
# Learning rate scheduler: simple rampdown to zero over num_steps
|
| 218 |
def get_lr_multiplier(it):
|
|
@@ -305,11 +304,9 @@ for step in range(num_steps):
|
|
| 305 |
|
| 306 |
# Update the model parameters
|
| 307 |
lrm = get_lr_multiplier(step)
|
| 308 |
-
for
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
for opt in optimizers: # then step the optimizers
|
| 312 |
-
opt.step()
|
| 313 |
model.zero_grad(set_to_none=True)
|
| 314 |
wandb_run.log({
|
| 315 |
"step": step,
|
|
|
|
| 201 |
# Training loop
|
| 202 |
|
| 203 |
# Init the optimizer
|
| 204 |
+
optimizer = model.setup_optimizer(
|
| 205 |
unembedding_lr=args.unembedding_lr,
|
| 206 |
embedding_lr=args.embedding_lr,
|
| 207 |
matrix_lr=args.matrix_lr,
|
|
|
|
| 209 |
)
|
| 210 |
|
| 211 |
# Set the initial learning rate as a fraction of the base learning rate
|
| 212 |
+
for group in optimizer.param_groups:
|
| 213 |
+
group["lr"] = group["lr"] * args.init_lr_frac
|
| 214 |
+
group["initial_lr"] = group["lr"]
|
|
|
|
| 215 |
|
| 216 |
# Learning rate scheduler: simple rampdown to zero over num_steps
|
| 217 |
def get_lr_multiplier(it):
|
|
|
|
| 304 |
|
| 305 |
# Update the model parameters
|
| 306 |
lrm = get_lr_multiplier(step)
|
| 307 |
+
for group in optimizer.param_groups:
|
| 308 |
+
group["lr"] = group["initial_lr"] * lrm
|
| 309 |
+
optimizer.step()
|
|
|
|
|
|
|
| 310 |
model.zero_grad(set_to_none=True)
|
| 311 |
wandb_run.log({
|
| 312 |
"step": step,
|
scripts/chat_sft.py
CHANGED
|
@@ -150,17 +150,16 @@ build_val_loader = lambda: sft_data_generator(val_ds, batch_size=args.device_bat
|
|
| 150 |
# -----------------------------------------------------------------------------
|
| 151 |
# Initialize the Optimizer
|
| 152 |
|
| 153 |
-
|
| 154 |
unembedding_lr=args.unembedding_lr,
|
| 155 |
embedding_lr=args.embedding_lr,
|
| 156 |
matrix_lr=args.matrix_lr,
|
| 157 |
weight_decay=args.weight_decay,
|
| 158 |
)
|
| 159 |
# Set the initial learning rate as a fraction of the base learning rate
|
| 160 |
-
for
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
group["initial_lr"] = group["lr"] # save the initial learning so we can decay easily later
|
| 164 |
|
| 165 |
# -----------------------------------------------------------------------------
|
| 166 |
# Training loop
|
|
@@ -230,13 +229,11 @@ for step in range(num_iterations):
|
|
| 230 |
|
| 231 |
# learning rate scheduler
|
| 232 |
lrm = get_lr_multiplier(step)
|
| 233 |
-
for
|
| 234 |
-
|
| 235 |
-
group["lr"] = group["initial_lr"] * lrm
|
| 236 |
|
| 237 |
-
# step the
|
| 238 |
-
|
| 239 |
-
opt.step()
|
| 240 |
model.zero_grad(set_to_none=True)
|
| 241 |
|
| 242 |
# logging
|
|
|
|
| 150 |
# -----------------------------------------------------------------------------
|
| 151 |
# Initialize the Optimizer
|
| 152 |
|
| 153 |
+
optimizer = model.setup_optimizer(
|
| 154 |
unembedding_lr=args.unembedding_lr,
|
| 155 |
embedding_lr=args.embedding_lr,
|
| 156 |
matrix_lr=args.matrix_lr,
|
| 157 |
weight_decay=args.weight_decay,
|
| 158 |
)
|
| 159 |
# Set the initial learning rate as a fraction of the base learning rate
|
| 160 |
+
for group in optimizer.param_groups:
|
| 161 |
+
group["lr"] = group["lr"] * args.init_lr_frac
|
| 162 |
+
group["initial_lr"] = group["lr"]
|
|
|
|
| 163 |
|
| 164 |
# -----------------------------------------------------------------------------
|
| 165 |
# Training loop
|
|
|
|
| 229 |
|
| 230 |
# learning rate scheduler
|
| 231 |
lrm = get_lr_multiplier(step)
|
| 232 |
+
for group in optimizer.param_groups:
|
| 233 |
+
group["lr"] = group["initial_lr"] * lrm
|
|
|
|
| 234 |
|
| 235 |
+
# step the optimizer
|
| 236 |
+
optimizer.step()
|
|
|
|
| 237 |
model.zero_grad(set_to_none=True)
|
| 238 |
|
| 239 |
# logging
|
scripts/mid_train.py
CHANGED
|
@@ -93,14 +93,12 @@ print0(f"Tokens / micro-batch: {world_tokens_per_fwdbwd:,}")
|
|
| 93 |
print0(f"Total batch size {args.total_batch_size:,} => gradient accumulation steps: {grad_accum_steps}")
|
| 94 |
token_bytes = get_token_bytes(device=device)
|
| 95 |
|
| 96 |
-
# Initialize the Optimizer (Muon for
|
| 97 |
-
|
| 98 |
-
adamw_optimizer, muon_optimizer = optimizers
|
| 99 |
# Override the initial learning rate as a fraction of the base learning rate
|
| 100 |
-
for
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
group["initial_lr"] = group["lr"] # save the initial learning so we can decay easily later
|
| 104 |
|
| 105 |
# Midtraining data mixture and DataLoader
|
| 106 |
base_dir = get_base_dir()
|
|
@@ -274,7 +272,7 @@ while True:
|
|
| 274 |
checkpoint_dir,
|
| 275 |
step,
|
| 276 |
orig_model.state_dict(),
|
| 277 |
-
|
| 278 |
{
|
| 279 |
"step": step,
|
| 280 |
"val_bpb": val_bpb, # loss at last step
|
|
@@ -306,16 +304,14 @@ while True:
|
|
| 306 |
loss.backward()
|
| 307 |
x, y = next(train_loader) # prefetch the next batch while the GPU is busy with forward/backward
|
| 308 |
progress = max(progress, approx_progress) # only increase progress monotonically
|
| 309 |
-
# step the
|
| 310 |
lrm = get_lr_multiplier(progress)
|
| 311 |
-
for opt in optimizers:
|
| 312 |
-
for group in opt.param_groups:
|
| 313 |
-
group["lr"] = group["initial_lr"] * lrm
|
| 314 |
muon_momentum = get_muon_momentum(step)
|
| 315 |
-
for group in
|
| 316 |
-
group["
|
| 317 |
-
|
| 318 |
-
|
|
|
|
| 319 |
model.zero_grad(set_to_none=True)
|
| 320 |
synchronize()
|
| 321 |
t1 = time.time()
|
|
|
|
| 93 |
print0(f"Total batch size {args.total_batch_size:,} => gradient accumulation steps: {grad_accum_steps}")
|
| 94 |
token_bytes = get_token_bytes(device=device)
|
| 95 |
|
| 96 |
+
# Initialize the Optimizer (combined MuonAdamW: Muon for matrix params, AdamW for rest)
|
| 97 |
+
optimizer = model.setup_optimizer(unembedding_lr=args.unembedding_lr, embedding_lr=args.embedding_lr, matrix_lr=args.matrix_lr, weight_decay=args.weight_decay)
|
|
|
|
| 98 |
# Override the initial learning rate as a fraction of the base learning rate
|
| 99 |
+
for group in optimizer.param_groups:
|
| 100 |
+
group["lr"] = group["lr"] * args.init_lr_frac
|
| 101 |
+
group["initial_lr"] = group["lr"]
|
|
|
|
| 102 |
|
| 103 |
# Midtraining data mixture and DataLoader
|
| 104 |
base_dir = get_base_dir()
|
|
|
|
| 272 |
checkpoint_dir,
|
| 273 |
step,
|
| 274 |
orig_model.state_dict(),
|
| 275 |
+
optimizer.state_dict(),
|
| 276 |
{
|
| 277 |
"step": step,
|
| 278 |
"val_bpb": val_bpb, # loss at last step
|
|
|
|
| 304 |
loss.backward()
|
| 305 |
x, y = next(train_loader) # prefetch the next batch while the GPU is busy with forward/backward
|
| 306 |
progress = max(progress, approx_progress) # only increase progress monotonically
|
| 307 |
+
# step the optimizer
|
| 308 |
lrm = get_lr_multiplier(progress)
|
|
|
|
|
|
|
|
|
|
| 309 |
muon_momentum = get_muon_momentum(step)
|
| 310 |
+
for group in optimizer.param_groups:
|
| 311 |
+
group["lr"] = group["initial_lr"] * lrm
|
| 312 |
+
if group['kind'] == 'muon':
|
| 313 |
+
group["momentum"] = muon_momentum
|
| 314 |
+
optimizer.step()
|
| 315 |
model.zero_grad(set_to_none=True)
|
| 316 |
synchronize()
|
| 317 |
t1 = time.time()
|