SPB Git

spb/forge Public MIT

Forge — LLM training from scratch in pure C++20 + Metal on Apple Silicon.

C++ 61.2% C 23% Python 7.6% TeX 7.2% CMake 1.1%
2.9 KB · 79 lines c
Raw Blame History
1// Author: Simon-Pierre Boucher — contact@spboucher.ai2#pragma once34#include "core/autograd.h"56#include <string>7#include <utility>8#include <vector>910namespace forge::train {1112// Two optimizer kinds behind one interface:13//  - "adamw": AdamW with decoupled weight decay on every parameter14//    (llm.c/PyTorch convention: eps outside sqrt, wd only on dim>=2 params).15//  - "muon": Keller Jordan's Muon on 2-D hidden matrices — momentum16//    orthogonalized by 5 Newton-Schulz iterations — with AdamW kept for17//    embeddings / lm_head / 1-D params (the standard Muon split). The Muon18//    group's LR is lr * muon_lr_ratio so both groups follow one schedule.19// Deduplicates tied parameters by Var::id(). m_ doubles as Muon's momentum20// buffer; v_ stays zero for Muon params so the checkpoint format is21// identical across kinds.22class Optimizer {23public:24    struct Options {25        std::string kind = "adamw";  // "adamw" | "muon"26        float beta1 = 0.9f;27        float beta2 = 0.95f;28        float eps = 1e-8f;29        float weight_decay = 0.1f;30        float muon_momentum = 0.95f;31        float muon_lr_ratio = 1.0f;  // muon lr = lr * ratio32    };3334    Optimizer(const std::vector<std::pair<std::string, Var>>& named_params, Options opts);3536    // Global-norm gradient clip: returns the pre-clip norm and scales all37    // grads by min(1, max_norm/norm). No-op when max_norm <= 0. (CPU path.)38    float clip_global_norm(float max_norm);3940    void step(float lr);41    void zero_grad();4243    // Backend-routed step: clip + update in one call, returns the pre-clip44    // grad norm. On Metal it expects backward() already encoded: it encodes45    // per-tensor sumsq, syncs (this is the step's loss-readback boundary),46    // folds the clip factor into the update, and syncs again at the end.47    // Muon adds one extra sync: the Newton-Schulz input must be normalized48    // by its Frobenius norm, which the CPU reads between the two phases.49    float step_with_clip(float lr, float max_norm);5051    // Allocate m/v now (checkpoint loading needs the buffers to exist).52    void ensure_state();5354    int64_t t() const { return t_; }55    // Checkpoint access (M4): moments in parameter order.56    std::vector<Tensor>& m() { return m_; }57    std::vector<Tensor>& v() { return v_; }58    void set_t(int64_t t) { t_ = t; }59    const std::vector<Var>& params() const { return params_; }6061private:62    // AdamW update for one param (CPU path).63    void adamw_cpu(size_t pi, float lr);64    // Muon update for one param (CPU path); grads already clipped.65    void muon_cpu(size_t pi, float lr);6667    Options opts_;68    std::vector<Var> params_;69    std::vector<bool> decay_;   // dim >= 270    std::vector<bool> muon_;    // kind=="muon" && 2-D && not embedding/head71    std::vector<Tensor> m_, v_; // f32, allocated lazily at first step72    int64_t t_ = 0;73};7475// Historical name; checkpoints and tests predate the Muon mode.76using AdamW = Optimizer;7778} // namespace forge::train79