use alloc::{format, string::String, vec::Vec};
use core::marker::PhantomData;
use hashbrown::{HashMap, HashSet};
use ruda_model::{
config::Config,
module::{AutodiffModule, ModuleVisitor, Param, ParamId},
record::{PrecisionSettings, Record},
tensor::{Tensor, backend::AutodiffBackend},
};
use crate::{
AdamW, AdamWConfig, GradientsParams, LearningRate, MultiGradientsParams,
Optimizer, adaptor::OptimizerAdaptor, record::{AdaptorRecord, AdaptorRecordV1},
};
use super::{Muon, MuonConfig, MuonError};
type Manifest = Vec<(u64, Vec<usize>, bool, String)>;
type MuonRecords<B: AutodiffBackend> = HashMap<ParamId, AdaptorRecord<Muon<<B as AutodiffBackend>::InnerBackend>, B>>;
type AdamRecords<B: AutodiffBackend> = HashMap<ParamId, AdaptorRecord<AdamW, B>>;
#[derive(Config, Debug)]
pub struct MuonAdamWConfig {
#[config(default = "MuonConfig::new()")]
muon: MuonConfig,
#[config(default = "AdamWConfig::new()")]
adamw: AdamWConfig,
#[config(default = 0.015)]
adamw_lr_ratio: f64,
}
impl MuonAdamWConfig {
pub fn init<B: AutodiffBackend, M: AutodiffModule<B>>(
&self, module: &M, muon_parameters: &[ParamId],
) -> Result<MuonAdamW<M, B>, MuonError> {
self.muon.validate()?;
self.adamw.validate_hyperparameters().map_err(MuonError::InvalidConfig)?;
valid_lr(self.adamw_lr_ratio)?;
if muon_parameters.is_empty() { return Err(MuonError::EmptyMuonGroup); }
let mut selected = HashSet::new();
for id in muon_parameters {
if !selected.insert(*id) { return Err(MuonError::DuplicateParameter(id.val())); }
}
let muon: OptimizerAdaptor<Muon<B::InnerBackend>, M, B> = self.muon.try_init()?;
let manifest = inspect_module(module, &selected, muon.optim(), None, 0.0)?;
Ok(MuonAdamW {
muon, adamw: self.adamw.init(), selected, manifest,
config_key: format!("muon-adamw-v1:{self:?}"),
adamw_lr_ratio: self.adamw_lr_ratio,
})
}
}
#[derive(Clone)]
pub struct MuonAdamW<M: AutodiffModule<B>, B: AutodiffBackend> {
muon: OptimizerAdaptor<Muon<B::InnerBackend>, M, B>,
adamw: OptimizerAdaptor<AdamW, M, B>,
selected: HashSet<ParamId>,
manifest: Manifest,
config_key: String,
adamw_lr_ratio: f64,
}
#[derive(Clone)]
pub struct MuonAdamWRecord<B: AutodiffBackend> {
version: u32,
config_key: String,
manifest: Manifest,
muon: MuonRecords<B>,
adamw: AdamRecords<B>,
}
impl<B: AutodiffBackend> MuonAdamWRecord<B> {
pub fn muon_state_count(&self) -> usize { self.muon.len() }
pub fn adamw_state_count(&self) -> usize { self.adamw.len() }
}
impl<B: AutodiffBackend> Record<B> for MuonAdamWRecord<B> {
type Item<S: PrecisionSettings> = (
u32, String, Manifest,
<MuonRecords<B> as Record<B>>::Item<S>,
<AdamRecords<B> as Record<B>>::Item<S>,
);
fn into_item<S: PrecisionSettings>(self) -> Self::Item<S> {
(self.version, self.config_key, self.manifest,
<MuonRecords<B> as Record<B>>::into_item::<S>(self.muon),
<AdamRecords<B> as Record<B>>::into_item::<S>(self.adamw))
}
fn from_item<S: PrecisionSettings>(item: Self::Item<S>, device: &B::Device) -> Self {
Self {
version: item.0, config_key: item.1, manifest: item.2,
muon: <MuonRecords<B> as Record<B>>::from_item::<S>(item.3, device),
adamw: <AdamRecords<B> as Record<B>>::from_item::<S>(item.4, device),
}
}
}
impl<M: AutodiffModule<B>, B: AutodiffBackend> MuonAdamW<M, B> {
pub fn muon_parameter_count(&self) -> usize { self.selected.len() }
pub fn try_step_with_lrs(
&mut self, muon_lr: LearningRate, adamw_lr: LearningRate,
module: M, grads: GradientsParams,
) -> Result<M, MuonError> {
valid_lr(muon_lr)?;
valid_lr(adamw_lr)?;
let manifest = inspect_module(&module, &self.selected, self.muon.optim(), Some(&grads), muon_lr)?;
if manifest != self.manifest { return Err(MuonError::ModelChanged); }
let mut split = SplitGradients::<B> {
selected: &self.selected, source: grads,
muon: GradientsParams::new(), adamw: GradientsParams::new(),
seen: HashSet::new(), backend: PhantomData,
};
module.visit(&mut split);
if !split.source.is_empty() { return Err(MuonError::UnusedGradients); }
let module = self.muon.step(muon_lr, module, split.muon);
Ok(self.adamw.step(adamw_lr, module, split.adamw))
}
pub fn try_step_or_skip(
&mut self, muon_lr: LearningRate, adamw_lr: LearningRate,
module: M, grads: GradientsParams, skip_update: bool,
) -> Result<M, MuonError> {
if skip_update { return Ok(module); }
self.try_step_with_lrs(muon_lr, adamw_lr, module, grads)
}
pub fn try_load_record(mut self, record: MuonAdamWRecord<B>) -> Result<Self, MuonError> {
if record.version != 1 || record.config_key != self.config_key || record.manifest != self.manifest {
return Err(MuonError::IncompatibleRecord);
}
let known: HashSet<_> = self.manifest.iter().map(|(id, _, _, _)| ParamId::from(*id)).collect();
if record.muon.keys().any(|id| !self.selected.contains(id))
|| record.adamw.keys().any(|id| !known.contains(id) || self.selected.contains(id)) {
return Err(MuonError::IncompatibleRecord);
}
for (id, state) in &record.muon {
let expected = self.manifest.iter().find(|entry| entry.0 == id.val())
.ok_or(MuonError::IncompatibleRecord)?;
match state {
AdaptorRecord::V1(AdaptorRecordV1::Rank2(state)) => {
let v = state.momentum.velocity();
if v.shape().to_vec() != expected.1 || format!("{:?}", v.dtype()) != expected.3 {
return Err(MuonError::IncompatibleRecord);
}
}
_ => return Err(MuonError::IncompatibleRecord),
}
}
for (id, state) in &record.adamw {
let expected = self.manifest.iter().find(|entry| entry.0 == id.val())
.ok_or(MuonError::IncompatibleRecord)?;
macro_rules! check_adam {
($state:expr) => {{
let m = &$state.momentum;
if m.time == 0 || m.moment_1.shape().to_vec() != expected.1
|| m.moment_2.shape().to_vec() != expected.1
|| format!("{:?}", m.moment_1.dtype()) != expected.3
|| format!("{:?}", m.moment_2.dtype()) != expected.3
|| m.max_moment_2.as_ref().is_some_and(|v|
v.shape().to_vec() != expected.1 || format!("{:?}", v.dtype()) != expected.3) {
return Err(MuonError::IncompatibleRecord);
}
}};
}
match state {
AdaptorRecord::V1(state) => match state {
AdaptorRecordV1::Rank0(v) => check_adam!(v),
AdaptorRecordV1::Rank1(v) => check_adam!(v),
AdaptorRecordV1::Rank2(v) => check_adam!(v),
AdaptorRecordV1::Rank3(v) => check_adam!(v),
AdaptorRecordV1::Rank4(v) => check_adam!(v),
AdaptorRecordV1::Rank5(v) => check_adam!(v),
AdaptorRecordV1::Rank6(v) => check_adam!(v),
AdaptorRecordV1::Rank7(v) => check_adam!(v),
AdaptorRecordV1::Rank8(v) => check_adam!(v),
},
}
}
self.muon = self.muon.load_record(record.muon);
self.adamw = self.adamw.load_record(record.adamw);
Ok(self)
}
}
impl<M: AutodiffModule<B>, B: AutodiffBackend> Optimizer<M, B> for MuonAdamW<M, B> {
type Record = MuonAdamWRecord<B>;
fn step(&mut self, lr: LearningRate, module: M, grads: GradientsParams) -> M {
self.try_step_with_lrs(lr, lr * self.adamw_lr_ratio, module, grads)
.unwrap_or_else(|error| panic!("{error}"))
}
fn step_multi(&mut self, _lr: LearningRate, _module: M, _grads: MultiGradientsParams) -> M {
panic!("{}", MuonError::UnsupportedDistributed)
}
fn to_record(&self) -> Self::Record {
MuonAdamWRecord {
version: 1, config_key: self.config_key.clone(), manifest: self.manifest.clone(),
muon: self.muon.to_record(), adamw: self.adamw.to_record(),
}
}
fn load_record(self, record: Self::Record) -> Self {
self.try_load_record(record).unwrap_or_else(|error| panic!("{error}"))
}
}
fn valid_lr(lr: f64) -> Result<(), MuonError> {
if !lr.is_finite() || lr < 0.0 || !(lr as f32).is_finite() {
Err(MuonError::InvalidConfig("learning rates/ratios must be finite, nonnegative and FP32-representable"))
} else { Ok(()) }
}
struct Inspect<'a, B: AutodiffBackend> {
selected: &'a HashSet<ParamId>,
muon: &'a Muon<B::InnerBackend>,
grads: Option<&'a GradientsParams>,
lr: f64,
entries: HashMap<ParamId, (Vec<usize>, bool, String)>,
seen_grads: usize,
error: Option<MuonError>,
}
fn inspect_module<B: AutodiffBackend, M: AutodiffModule<B>>(
module: &M, selected: &HashSet<ParamId>, muon: &Muon<B::InnerBackend>,
grads: Option<&GradientsParams>, lr: f64,
) -> Result<Manifest, MuonError> {
let mut inspect = Inspect { selected, muon, grads, lr, entries: HashMap::new(), seen_grads: 0, error: None };
module.visit(&mut inspect);
if let Some(error) = inspect.error { return Err(error); }
for id in selected {
if !inspect.entries.contains_key(id) { return Err(MuonError::UnknownParameter(id.val())); }
}
if grads.is_some_and(|g| g.len() != inspect.seen_grads) { return Err(MuonError::UnusedGradients); }
let mut manifest: Manifest = inspect.entries.into_iter()
.map(|(id, (shape, _, dtype))| (id.val(), shape, selected.contains(&id), dtype)).collect();
manifest.sort_by_key(|entry| entry.0);
Ok(manifest)
}
impl<B: AutodiffBackend> ModuleVisitor<B> for Inspect<'_, B> {
fn visit_float<const D: usize>(&mut self, param: &Param<Tensor<B, D>>) {
if self.error.is_some() { return; }
let tensor = param.val();
let shape = tensor.shape().to_vec();
let trainable = param.is_require_grad();
let dtype = format!("{:?}", tensor.dtype());
if let Some(prior) = self.entries.get(¶m.id) {
if prior != &(shape, trainable, dtype) { self.error = Some(MuonError::ModelChanged); }
return;
}
self.entries.insert(param.id, (shape, trainable, dtype));
if D > 8 { self.error = Some(MuonError::InvalidConfig("optimizer records support at most rank 8")); return; }
#[cfg(feature = "distributed")]
if tensor.is_distributed() { self.error = Some(MuonError::UnsupportedDistributed); return; }
let selected = self.selected.contains(¶m.id);
if selected && !trainable { self.error = Some(MuonError::FrozenParameter(param.id.val())); return; }
let inner = tensor.inner();
if selected {
if let Err(error) = self.muon.validate_step(self.lr, &inner, &inner, None) {
self.error = Some(error); return;
}
}
if !trainable { return; }
if let Some(grad) = self.grads.and_then(|g| g.get::<B::InnerBackend, D>(param.id)) {
self.seen_grads += 1;
if inner.shape() != grad.shape() { self.error = Some(MuonError::ShapeMismatch("gradient")); return; }
if inner.dtype() != grad.dtype() { self.error = Some(MuonError::DTypeMismatch("gradient")); return; }
if inner.device() != grad.device() { self.error = Some(MuonError::DeviceMismatch("gradient")); }
}
}
}
struct SplitGradients<'a, B: AutodiffBackend> {
selected: &'a HashSet<ParamId>,
source: GradientsParams,
muon: GradientsParams,
adamw: GradientsParams,
seen: HashSet<ParamId>,
backend: PhantomData<B>,
}
impl<B: AutodiffBackend> ModuleVisitor<B> for SplitGradients<'_, B> {
fn visit_float<const D: usize>(&mut self, param: &Param<Tensor<B, D>>) {
if !param.is_require_grad() || !self.seen.insert(param.id) { return; }
if let Some(grad) = self.source.remove::<B::InnerBackend, D>(param.id) {
if self.selected.contains(¶m.id) { self.muon.register(param.id, grad); }
else { self.adamw.register(param.id, grad); }
}
}
}
#[cfg(test)]
mod tests;