#![cfg(feature = "cuda")]
use mamba_rs::mamba_ssm::gpu::dtype::WeightDtype;
fn det_scaled(n: usize, seed: u32, scale: f32) -> Vec<f32> {
let mut s = seed;
(0..n)
.map(|_| {
s ^= s << 13;
s ^= s >> 17;
s ^= s << 5;
((s & 0xFFFF) as f32 / 65536.0 - 0.5) * scale
})
.collect()
}
fn run_parallel_scan_trainer(dtype: WeightDtype) {
use mamba_rs::config::{MambaConfig, ScanMode};
use mamba_rs::mamba_ssm::gpu::trainer::{MambaTrainer, TrainSessionCfg};
use mamba_rs::weights::MambaWeights;
let cfg = MambaConfig {
d_model: 32,
n_layers: 2,
d_state: 16,
d_conv: 4,
expand: 2,
scan_mode: ScanMode::Parallel,
rms_norm_eps: 1e-5,
};
let input_dim = cfg.d_model;
let batch = 1;
let seq_len = 256;
let n = batch * seq_len * input_dim;
let mut cpu = MambaWeights::init(&cfg, input_dim, 0xBA4A_15CA);
if !matches!(dtype, WeightDtype::F32) {
cpu.input_proj_w.clear();
cpu.input_proj_b.clear();
}
for lw in cpu.layers.iter_mut() {
lw.a_neg = lw.a_log.iter().map(|&v| -v.exp()).collect();
}
let mut trainer = MambaTrainer::new_full(
0,
&cpu,
cfg,
TrainSessionCfg {
input_dim,
batch,
seq_len,
lr: 1e-7,
weight_decay: 0.0,
},
dtype,
)
.expect("construct parallel-scan trainer");
let before = trainer.snapshot_master().expect("snapshot pre");
let in_scale = if matches!(dtype, WeightDtype::F16) {
0.001
} else {
1.0
};
let warmup_steps = if matches!(dtype, WeightDtype::F16) {
12
} else {
2
};
for s in 0..warmup_steps {
let m = trainer
.step(
&det_scaled(n, 0xA0 + s as u32, in_scale),
&det_scaled(n, 0xB0 + s as u32, in_scale),
)
.expect("eager step");
assert!(!m.graph_replayed);
if matches!(dtype, WeightDtype::F16) {
assert!(m.loss_scale.is_some());
}
}
trainer
.capture_graph()
.expect("capture parallel-scan graph");
assert!(trainer.has_graph());
for s in 0..5 {
let m = trainer
.step(
&det_scaled(n, 0xC0 + s as u32, in_scale),
&det_scaled(n, 0xD0 + s as u32, in_scale),
)
.expect("graph step");
assert!(m.graph_replayed, "post-capture must replay");
}
let after = trainer.snapshot_master().expect("snapshot post");
for (i, lw) in after.layers.iter().enumerate() {
assert!(
lw.in_proj_w.iter().all(|v| v.is_finite()),
"{dtype:?} L{i} in_proj_w non-finite after parallel-scan training"
);
assert!(
lw.out_proj_w.iter().all(|v| v.is_finite()),
"{dtype:?} L{i} out_proj_w non-finite after parallel-scan training"
);
}
let max_diff = before.layers[0]
.in_proj_w
.iter()
.zip(&after.layers[0].in_proj_w)
.map(|(a, b)| (a - b).abs())
.fold(0.0f32, f32::max);
eprintln!("parallel-scan {dtype:?} trainer: max |Δw| = {max_diff:.3e}");
assert!(max_diff > 0.0, "{dtype:?} weights did not move");
assert!(max_diff.is_finite());
}
#[test]
fn m1_trainer_parallel_scan_f32() {
run_parallel_scan_trainer(WeightDtype::F32);
}
#[test]
fn m1_trainer_parallel_scan_bf16() {
run_parallel_scan_trainer(WeightDtype::Bf16);
}
#[test]
fn m1_trainer_parallel_scan_f16() {
run_parallel_scan_trainer(WeightDtype::F16);
}
#[test]
fn a_log_actually_reaches_ssm_after_training() {
use mamba_rs::config::{MambaConfig, ScanMode};
use mamba_rs::mamba_ssm::gpu::trainer::{MambaTrainer, TrainSessionCfg};
use mamba_rs::weights::MambaWeights;
let cfg = MambaConfig {
d_model: 64,
n_layers: 2,
d_state: 16,
d_conv: 4,
expand: 2,
scan_mode: ScanMode::Sequential,
rms_norm_eps: 1e-5,
};
let input_dim = cfg.d_model;
let batch = 1;
let seq_len = 8;
let n = batch * seq_len * input_dim;
let mut cpu = MambaWeights::init(&cfg, input_dim, 0xA10_C0FFEE);
cpu.input_proj_w.clear();
cpu.input_proj_b.clear();
for lw in cpu.layers.iter_mut() {
lw.a_neg = lw.a_log.iter().map(|&v| -v.exp()).collect();
}
let a_log_init: Vec<f32> = cpu.layers.iter().flat_map(|lw| lw.a_log.clone()).collect();
let a_neg_init: Vec<f32> = a_log_init.iter().map(|&v| -v.exp()).collect();
let mut trainer = MambaTrainer::new_full(
0,
&cpu,
cfg,
TrainSessionCfg {
input_dim,
batch,
seq_len,
lr: 1e-3,
weight_decay: 0.0,
},
WeightDtype::Bf16,
)
.expect("build trainer");
for s in 0..5 {
trainer
.step(&det_scaled(n, 0xA0 + s, 1.0), &det_scaled(n, 0xB0 + s, 0.1))
.expect("training step");
}
let after = trainer.snapshot_master().expect("snapshot");
let a_log_after: Vec<f32> = after
.layers
.iter()
.flat_map(|lw| lw.a_log.clone())
.collect();
let a_neg_after = trainer.debug_a_neg_all().expect("download a_neg_all");
let a_log_max_delta: f32 = a_log_init
.iter()
.zip(&a_log_after)
.map(|(a, b): (&f32, &f32)| (a - b).abs())
.fold(0.0f32, f32::max);
assert!(
a_log_max_delta > 1e-5,
"a_log did not move after 5 steps (max delta {a_log_max_delta:.3e}) — optimizer broken"
);
let a_neg_max_delta: f32 = a_neg_init
.iter()
.zip(&a_neg_after)
.map(|(a, b): (&f32, &f32)| (a - b).abs())
.fold(0.0f32, f32::max);
assert!(
a_neg_max_delta > 1e-5,
"a_neg_all was NOT refreshed after AdamW updates to a_log \
(max delta {a_neg_max_delta:.3e}). This is the round-2 audit \
CRIT bug — SSM would run on stale A-matrix forever."
);
let mut worst_formula_err = 0.0f32;
for (log, neg) in a_log_after.iter().zip(a_neg_after.iter()) {
let expected = -log.exp();
let err = (expected - neg).abs();
if err > worst_formula_err {
worst_formula_err = err;
}
}
assert!(
worst_formula_err < 1e-3,
"a_neg_all != -exp(a_log) after refresh (max err {worst_formula_err:.3e}) \
— recompute formula broken"
);
eprintln!(
"a_log max delta = {a_log_max_delta:.3e} a_neg max delta = {a_neg_max_delta:.3e} \
formula err = {worst_formula_err:.3e}"
);
}
#[test]
fn m1_trainer_f16_production_lr_stable() {
use mamba_rs::config::{MambaConfig, ScanMode};
use mamba_rs::mamba_ssm::gpu::trainer::{MambaTrainer, TrainSessionCfg};
use mamba_rs::weights::MambaWeights;
let cfg = MambaConfig {
d_model: 64,
n_layers: 2,
d_state: 16,
d_conv: 4,
expand: 2,
scan_mode: ScanMode::Sequential,
rms_norm_eps: 1e-5,
};
let input_dim = cfg.d_model;
let batch = 2;
let seq_len = 32;
let n = batch * seq_len * input_dim;
let mut cpu = MambaWeights::init(&cfg, input_dim, 0xF16_5CA1E);
cpu.input_proj_w.clear();
cpu.input_proj_b.clear();
for lw in cpu.layers.iter_mut() {
lw.a_neg = lw.a_log.iter().map(|&v| -v.exp()).collect();
}
let mut trainer = MambaTrainer::new_full(
0,
&cpu,
cfg,
TrainSessionCfg {
input_dim,
batch,
seq_len,
lr: 1e-4,
weight_decay: 0.01,
},
WeightDtype::F16,
)
.expect("construct f16 prod-lr trainer");
let before = trainer.snapshot_master().expect("snapshot pre");
let mut scales: Vec<f32> = Vec::with_capacity(50);
let mut committed_steps = 0usize;
let mut overflow_count = 0usize;
let in_scale = 1e-3f32;
for s in 0..50 {
let m = trainer
.step(
&det_scaled(n, 0xA0 + s as u32, in_scale),
&det_scaled(n, 0xB0 + s as u32, in_scale),
)
.expect("f16 prod-lr step");
let scale = m.loss_scale.expect("f16 must report loss_scale");
let skipped = m.overflow_skipped.expect("f16 must report overflow");
scales.push(scale);
if skipped {
overflow_count += 1;
} else {
committed_steps += 1;
}
assert!(scale >= 1.0, "step {s}: scale {scale} < 1.0");
assert!(
scale <= (1u32 << 24) as f32,
"step {s}: scale {scale} > 2^24"
);
}
let after = trainer.snapshot_master().expect("snapshot post");
for (i, lw) in after.layers.iter().enumerate() {
for (name, slice) in [
("in_proj_w", lw.in_proj_w.as_slice()),
("out_proj_w", lw.out_proj_w.as_slice()),
("x_proj_w", lw.x_proj_w.as_slice()),
("dt_proj_w", lw.dt_proj_w.as_slice()),
] {
assert!(
slice.iter().all(|v| v.is_finite()),
"f16 L{i} {name} non-finite after 50 prod-lr steps"
);
}
}
assert!(
committed_steps > 0,
"all 50 steps were overflow-skipped — loss scaler stuck"
);
let max_diff = before.layers[0]
.in_proj_w
.iter()
.zip(&after.layers[0].in_proj_w)
.map(|(a, b)| (a - b).abs())
.fold(0.0f32, f32::max);
eprintln!(
"f16 prod-lr: committed={committed_steps}/50 overflow={overflow_count} scale_last={:.0} max|Δw|={max_diff:.3e}",
scales.last().copied().unwrap_or(0.0)
);
assert!(
max_diff > 1e-8,
"f16 weights barely moved after 50 prod-lr steps (max|Δw|={max_diff:.3e})"
);
assert!(max_diff.is_finite(), "max|Δw|={max_diff} non-finite");
}