use std::collections::HashMap;
use rlx_ir::infer::GraphExt;
use rlx_ir::{DType, Graph, NodeId, Op, Shape};
use crate::{CompileOptions, CompiledGraph, Device, Session};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TrainableParam {
pub name: String,
pub node: NodeId,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct AdamSpec {
pub lr: f32,
pub beta1: f32,
pub beta2: f32,
pub eps: f32,
pub weight_decay: f32,
}
impl Default for AdamSpec {
fn default() -> Self {
Self {
lr: 1e-3,
beta1: 0.9,
beta2: 0.999,
eps: 1e-8,
weight_decay: 0.0,
}
}
}
impl AdamSpec {
pub fn new(lr: f32) -> Self {
Self {
lr,
..Self::default()
}
}
}
#[derive(Debug)]
pub enum FuseError {
NoLoss,
NoTrainableParams,
UnknownParam(String),
MissingInitialValue(String),
}
impl std::fmt::Display for FuseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::NoLoss => write!(
f,
"the forward graph has no outputs; the first must be the loss"
),
Self::NoTrainableParams => write!(f, "no trainable parameters were given"),
Self::UnknownParam(name) => write!(f, "'{name}' is not a Param in the forward graph"),
Self::MissingInitialValue(name) => {
write!(f, "no initial value for trainable parameter '{name}'")
}
}
}
}
impl std::error::Error for FuseError {}
#[derive(Clone, Debug)]
struct FusedParam {
name: String,
m_name: String,
v_name: String,
scale_name: String,
p_out: usize,
m_out: usize,
v_out: usize,
}
struct FusedStep {
graph: Graph,
params: Vec<FusedParam>,
}
fn fused_adam_graph(
forward: &Graph,
wrt: &[TrainableParam],
spec: &AdamSpec,
) -> Result<FusedStep, FuseError> {
if forward.outputs.is_empty() {
return Err(FuseError::NoLoss);
}
if wrt.is_empty() {
return Err(FuseError::NoTrainableParams);
}
let wrt_ids: Vec<NodeId> = wrt.iter().map(|s| s.node).collect();
let mut g = rlx_opt::autodiff::grad_with_loss(forward, &wrt_ids);
let f = DType::F32;
let grad_start = g
.outputs
.len()
.checked_sub(wrt.len())
.ok_or(FuseError::NoTrainableParams)?;
let mut outputs: Vec<NodeId> = g.outputs[..grad_start].to_vec();
let grads: Vec<NodeId> = g.outputs[grad_start..].to_vec();
let lr = g.input("__lr", Shape::scalar(f));
let bc1 = g.input("__bc1", Shape::scalar(f));
let bc2 = g.input("__bc2", Shape::scalar(f));
let c_b1 = g.constant(spec.beta1 as f64, f);
let c_1mb1 = g.constant((1.0 - spec.beta1) as f64, f);
let c_b2 = g.constant(spec.beta2 as f64, f);
let c_1mb2 = g.constant((1.0 - spec.beta2) as f64, f);
let c_eps = g.constant(spec.eps as f64, f);
let lr_wd = (spec.weight_decay != 0.0).then(|| {
let c_wd = g.constant(spec.weight_decay as f64, f);
g.mul(lr, c_wd)
});
let mut params = Vec::with_capacity(wrt.len());
let mut param_ids = Vec::with_capacity(wrt.len());
for (i, slot) in wrt.iter().enumerate() {
let p = g
.param_id(&slot.name)
.ok_or_else(|| FuseError::UnknownParam(slot.name.clone()))?;
param_ids.push(p);
let shape = g.shape(p).clone();
let grad = grads[i];
let m_name = format!("{}__m", slot.name);
let v_name = format!("{}__v", slot.name);
let scale_name = format!("{}__scale", slot.name);
let m = g.input(&m_name, shape.clone());
let v = g.input(&v_name, shape.clone());
let gate = g.input(&scale_name, Shape::scalar(f));
let m_decayed = g.mul(m, c_b1);
let g_scaled = g.mul(grad, c_1mb1);
let m_new = g.add(m_decayed, g_scaled);
let sq = g.mul(grad, grad);
let v_decayed = g.mul(v, c_b2);
let sq_scaled = g.mul(sq, c_1mb2);
let v_new = g.add(v_decayed, sq_scaled);
let mhat = g.div(m_new, bc1);
let vhat = g.div(v_new, bc2);
let root = g.sqrt(vhat);
let denom = g.add(root, c_eps);
let ratio = g.div(mhat, denom);
let update = g.mul(ratio, lr);
let gated = g.mul(update, gate);
let stepped = g.sub(p, gated);
let p_new = match lr_wd {
Some(lrwd) => {
let decay = g.mul(p, lrwd);
let gated_decay = g.mul(decay, gate);
g.sub(stepped, gated_decay)
}
None => stepped,
};
let one = g.constant(1.0, f);
let hold = g.sub(one, gate);
let m_taken = g.mul(m_new, gate);
let m_held = g.mul(m, hold);
let m_kept = g.add(m_taken, m_held);
let v_taken = g.mul(v_new, gate);
let v_held = g.mul(v, hold);
let v_kept = g.add(v_taken, v_held);
let p_out = outputs.len();
outputs.push(p_new);
let m_out = outputs.len();
outputs.push(m_kept);
let v_out = outputs.len();
outputs.push(v_kept);
params.push(FusedParam {
name: slot.name.clone(),
m_name,
v_name,
scale_name,
p_out,
m_out,
v_out,
});
}
g.set_outputs(outputs);
for (slot, &p) in wrt.iter().zip(¶m_ids) {
g.node_mut(p).op = Op::Input {
name: slot.name.clone(),
};
}
Ok(FusedStep { graph: g, params })
}
pub struct ResidentTrainer {
compiled: CompiledGraph,
params: Vec<FusedParam>,
spec: AdamSpec,
step_count: i32,
leading_outputs: usize,
param_vals: HashMap<String, Vec<f32>>,
m_vals: HashMap<String, Vec<f32>>,
v_vals: HashMap<String, Vec<f32>>,
gates: HashMap<String, f32>,
resident: bool,
}
impl ResidentTrainer {
pub fn new(
forward: &Graph,
wrt: &[TrainableParam],
initial: &HashMap<String, Vec<f32>>,
spec: &AdamSpec,
device: Device,
) -> Result<Self, FuseError> {
Self::with_options(forward, wrt, initial, spec, device, &CompileOptions::new())
}
pub fn with_options(
forward: &Graph,
wrt: &[TrainableParam],
initial: &HashMap<String, Vec<f32>>,
spec: &AdamSpec,
device: Device,
options: &CompileOptions,
) -> Result<Self, FuseError> {
let leading_outputs = forward.outputs.len();
let step = fused_adam_graph(forward, wrt, spec)?;
let mut compiled = Session::new(device).compile_with(step.graph, options);
let trainable: Vec<&str> = step.params.iter().map(|p| p.name.as_str()).collect();
for (name, data) in initial {
if !trainable.contains(&name.as_str()) {
compiled.set_param(name, data);
}
}
let mut param_vals = HashMap::new();
let mut m_vals = HashMap::new();
let mut v_vals = HashMap::new();
let mut gates = HashMap::new();
for fp in &step.params {
let start = initial
.get(&fp.name)
.ok_or_else(|| FuseError::MissingInitialValue(fp.name.clone()))?
.clone();
let zeros = vec![0.0f32; start.len()];
param_vals.insert(fp.name.clone(), start);
m_vals.insert(fp.name.clone(), zeros.clone());
v_vals.insert(fp.name.clone(), zeros);
gates.insert(fp.name.clone(), 1.0);
}
let resident =
try_bind_resident(&mut compiled, &step.params, ¶m_vals, &m_vals, &v_vals);
Ok(Self {
compiled,
params: step.params,
spec: *spec,
step_count: 0,
leading_outputs,
param_vals,
m_vals,
v_vals,
gates,
resident,
})
}
pub fn is_resident(&self) -> bool {
self.resident
}
pub fn device(&self) -> Device {
self.compiled.device()
}
pub fn steps(&self) -> u64 {
self.step_count as u64
}
pub fn set_lr(&mut self, lr: f32) {
self.spec.lr = lr;
}
pub fn lr(&self) -> f32 {
self.spec.lr
}
pub fn set_frozen(&mut self, name: &str, frozen: bool) -> bool {
match self.gates.get_mut(name) {
Some(gate) => {
*gate = if frozen { 0.0 } else { 1.0 };
true
}
None => false,
}
}
pub fn is_frozen(&self, name: &str) -> bool {
self.gates.get(name).is_some_and(|g| *g == 0.0)
}
pub fn trainable(&self) -> impl Iterator<Item = &str> {
self.params.iter().map(|p| p.name.as_str())
}
pub fn step(&mut self, inputs: &[(&str, &[f32])]) -> Vec<Vec<f32>> {
self.step_count += 1;
let seed = [1.0f32];
let lr = [self.spec.lr];
let bc1 = [1.0 - self.spec.beta1.powi(self.step_count)];
let bc2 = [1.0 - self.spec.beta2.powi(self.step_count)];
let gate_values: Vec<(String, [f32; 1])> = self
.params
.iter()
.map(|fp| (fp.scale_name.clone(), [self.gates[&fp.name]]))
.collect();
let mut run_inputs: Vec<(&str, &[f32])> = inputs.to_vec();
run_inputs.push(("d_output", &seed));
run_inputs.push(("__lr", &lr));
run_inputs.push(("__bc1", &bc1));
run_inputs.push(("__bc2", &bc2));
for (name, value) in &gate_values {
run_inputs.push((name.as_str(), value));
}
let wanted: Vec<usize> = (0..self.leading_outputs).collect();
if self.resident {
self.compiled.run_read_outputs(&run_inputs, Some(&wanted))
} else {
for fp in &self.params {
run_inputs.push((fp.name.as_str(), &self.param_vals[&fp.name]));
run_inputs.push((fp.m_name.as_str(), &self.m_vals[&fp.name]));
run_inputs.push((fp.v_name.as_str(), &self.v_vals[&fp.name]));
}
let outs = self.compiled.run(&run_inputs);
for fp in &self.params {
copy_out(&mut self.param_vals, &fp.name, outs.get(fp.p_out));
copy_out(&mut self.m_vals, &fp.name, outs.get(fp.m_out));
copy_out(&mut self.v_vals, &fp.name, outs.get(fp.v_out));
}
outs.into_iter().take(self.leading_outputs).collect()
}
}
pub fn forward_only(&mut self, inputs: &[(&str, &[f32])]) -> Vec<Vec<f32>> {
let seed = [1.0f32];
let lr = [self.spec.lr];
let t = self.step_count.max(1);
let bc1 = [1.0 - self.spec.beta1.powi(t)];
let bc2 = [1.0 - self.spec.beta2.powi(t)];
let held: Vec<(String, [f32; 1])> = self
.params
.iter()
.map(|fp| (fp.scale_name.clone(), [0.0f32]))
.collect();
let mut run_inputs: Vec<(&str, &[f32])> = inputs.to_vec();
run_inputs.push(("d_output", &seed));
run_inputs.push(("__lr", &lr));
run_inputs.push(("__bc1", &bc1));
run_inputs.push(("__bc2", &bc2));
for (name, value) in &held {
run_inputs.push((name.as_str(), value));
}
let wanted: Vec<usize> = (0..self.leading_outputs).collect();
if self.resident {
self.compiled.run_read_outputs(&run_inputs, Some(&wanted))
} else {
for fp in &self.params {
run_inputs.push((fp.name.as_str(), &self.param_vals[&fp.name]));
run_inputs.push((fp.m_name.as_str(), &self.m_vals[&fp.name]));
run_inputs.push((fp.v_name.as_str(), &self.v_vals[&fp.name]));
}
let outs = self.compiled.run(&run_inputs);
outs.into_iter().take(self.leading_outputs).collect()
}
}
pub fn params(&mut self) -> HashMap<String, Vec<f32>> {
if self.resident {
for fp in &self.params {
if let Some(v) = self.compiled.read_gpu_handle(&fp.name) {
self.param_vals.insert(fp.name.clone(), v);
}
}
}
self.param_vals.clone()
}
pub fn set_params(&mut self, values: &HashMap<String, Vec<f32>>) {
for (name, data) in values {
if let Some(slot) = self.param_vals.get_mut(name) {
let n = slot.len().min(data.len());
slot[..n].copy_from_slice(&data[..n]);
} else {
self.compiled.set_param(name, data);
}
}
if self.resident {
for fp in &self.params {
self.compiled
.bind_gpu_handle(&fp.name, &self.param_vals[&fp.name]);
self.compiled.set_gpu_handle_feed(&fp.name, fp.p_out);
}
}
}
pub fn moments(&mut self) -> Vec<(String, Vec<f32>, Vec<f32>)> {
if self.resident {
for fp in &self.params {
if let Some(m) = self.compiled.read_gpu_handle(&fp.m_name) {
self.m_vals.insert(fp.name.clone(), m);
}
if let Some(v) = self.compiled.read_gpu_handle(&fp.v_name) {
self.v_vals.insert(fp.name.clone(), v);
}
}
}
self.params
.iter()
.map(|fp| {
(
fp.name.clone(),
self.m_vals[&fp.name].clone(),
self.v_vals[&fp.name].clone(),
)
})
.collect()
}
pub fn restore_moments(
&mut self,
moments: &[(String, Vec<f32>, Vec<f32>)],
step_count: u64,
) -> bool {
let mut all = true;
for (name, m, v) in moments {
if let Some(slot) = self.m_vals.get_mut(name) {
let n = slot.len().min(m.len());
slot[..n].copy_from_slice(&m[..n]);
} else {
all = false;
}
if let Some(slot) = self.v_vals.get_mut(name) {
let n = slot.len().min(v.len());
slot[..n].copy_from_slice(&v[..n]);
} else {
all = false;
}
}
self.step_count = step_count as i32;
if self.resident {
for fp in &self.params {
self.compiled
.bind_gpu_handle(&fp.m_name, &self.m_vals[&fp.name]);
self.compiled
.bind_gpu_handle(&fp.v_name, &self.v_vals[&fp.name]);
self.compiled.set_gpu_handle_feed(&fp.m_name, fp.m_out);
self.compiled.set_gpu_handle_feed(&fp.v_name, fp.v_out);
}
}
all
}
}
fn copy_out(map: &mut HashMap<String, Vec<f32>>, name: &str, out: Option<&Vec<f32>>) {
if let (Some(dst), Some(src)) = (map.get_mut(name), out) {
let n = dst.len().min(src.len());
dst[..n].copy_from_slice(&src[..n]);
}
}
fn try_bind_resident(
compiled: &mut CompiledGraph,
params: &[FusedParam],
param_vals: &HashMap<String, Vec<f32>>,
m_vals: &HashMap<String, Vec<f32>>,
v_vals: &HashMap<String, Vec<f32>>,
) -> bool {
let mut ok = true;
for fp in params {
ok &= compiled.bind_gpu_handle(&fp.name, ¶m_vals[&fp.name]);
ok &= compiled.bind_gpu_handle(&fp.m_name, &m_vals[&fp.name]);
ok &= compiled.bind_gpu_handle(&fp.v_name, &v_vals[&fp.name]);
ok &= compiled.set_gpu_handle_feed(&fp.name, fp.p_out);
ok &= compiled.set_gpu_handle_feed(&fp.m_name, fp.m_out);
ok &= compiled.set_gpu_handle_feed(&fp.v_name, fp.v_out);
}
ok
}
#[cfg(test)]
mod tests_support {
use super::*;
use rlx_ir::infer::GraphExt;
pub fn regression(batch: usize, dim: usize) -> (Graph, Vec<TrainableParam>) {
let mut g = Graph::new("mse");
let x = g.input("x", Shape::new(&[batch, dim], DType::F32));
let t = g.input("t", Shape::new(&[batch, dim], DType::F32));
let w = g.param("w", Shape::new(&[dim, dim], DType::F32));
let b = g.param("b", Shape::new(&[1, dim], DType::F32));
let pred = g.mm(x, w);
let biased = g.add(pred, b);
let diff = g.sub(biased, t);
let sq = g.mul(diff, diff);
let loss = g.mean(sq, vec![0, 1], false);
g.set_outputs(vec![loss]);
let wrt = vec![
TrainableParam {
name: "w".into(),
node: w,
},
TrainableParam {
name: "b".into(),
node: b,
},
];
(g, wrt)
}
pub fn initial(dim: usize) -> HashMap<String, Vec<f32>> {
let mut map = HashMap::new();
map.insert(
"w".to_string(),
(0..dim * dim).map(|i| 0.01 * (i % 7) as f32).collect(),
);
map.insert("b".to_string(), vec![0.0; dim]);
map
}
}
#[cfg(test)]
mod tests {
use super::tests_support::*;
use super::*;
use rlx_ir::infer::GraphExt;
#[test]
fn the_fused_update_matches_a_host_optimizer() {
const DIM: usize = 4;
const BATCH: usize = 2;
let spec = AdamSpec {
lr: 0.05,
weight_decay: 0.01,
..AdamSpec::default()
};
let x: Vec<f32> = vec![1.0, 0.5, -0.5, 2.0, 0.25, -1.0, 1.5, 0.0];
let t: Vec<f32> = vec![1.0, -1.0, 0.5, 0.0, 0.0, 2.0, -1.0, 1.0];
let (forward, wrt) = regression(BATCH, DIM);
let start = initial(DIM);
let mut fused =
ResidentTrainer::new(&forward, &wrt, &start, &spec, Device::Cpu).expect("fuses");
let mut fused_losses = Vec::new();
for _ in 0..12 {
let out = fused.step(&[("x", &x), ("t", &t)]);
fused_losses.push(out[0][0]);
}
let fused_params = fused.params();
let (forward2, _) = regression(BATCH, DIM);
let w_id = forward2.param_id("w").unwrap();
let b_id = forward2.param_id("b").unwrap();
let backward = rlx_opt::autodiff::grad_with_loss(&forward2, &[w_id, b_id]);
let mut compiled = Session::new(Device::Cpu).compile(backward);
let mut opt = rlx_optim::AdamW::new(spec.lr);
opt.beta1 = spec.beta1;
opt.beta2 = spec.beta2;
opt.eps = spec.eps;
opt.weight_decay = spec.weight_decay;
let mut host: HashMap<String, Vec<f32>> = initial(DIM);
let mut host_losses = Vec::new();
for _ in 0..12 {
compiled.set_param("w", &host["w"]);
compiled.set_param("b", &host["b"]);
let outs = compiled.run(&[("x", &x), ("t", &t), ("d_output", &[1.0])]);
host_losses.push(outs[0][0]);
let shapes: [(&str, Vec<usize>); 2] = [("w", vec![DIM, DIM]), ("b", vec![1, DIM])];
for (i, (name, shape)) in shapes.iter().enumerate() {
let param = host.get_mut(*name).unwrap();
rlx_optim::Optimizer::step(&mut opt, name, shape, param, &outs[1 + i]);
}
rlx_optim::Optimizer::end_iteration(&mut opt);
}
assert_eq!(
fused_losses[0].to_bits(),
host_losses[0].to_bits(),
"the first step should be bit-identical: {} vs {}",
fused_losses[0],
host_losses[0]
);
const REL: f32 = 1e-5;
for (step, (a, b)) in fused_losses.iter().zip(host_losses.iter()).enumerate() {
assert!(
(a - b).abs() <= REL * b.abs().max(1.0),
"step {step}: fused loss {a} vs host {b}"
);
}
for name in ["w", "b"] {
for (i, (a, b)) in fused_params[name].iter().zip(host[name].iter()).enumerate() {
assert!(
(a - b).abs() <= REL * b.abs().max(1.0),
"{name}[{i}]: fused {a} vs host {b}"
);
}
}
assert!(
fused_losses[11] < fused_losses[0] * 0.5,
"the fused trainer did not learn: {fused_losses:?}"
);
}
#[test]
fn freezing_holds_a_parameter_and_its_moments() {
const DIM: usize = 4;
let (forward, wrt) = regression(2, DIM);
let start = initial(DIM);
let mut trainer =
ResidentTrainer::new(&forward, &wrt, &start, &AdamSpec::new(0.1), Device::Cpu)
.expect("fuses");
let x: Vec<f32> = vec![1.0, 0.5, -0.5, 2.0, 0.25, -1.0, 1.5, 0.0];
let t: Vec<f32> = vec![1.0, -1.0, 0.5, 0.0, 0.0, 2.0, -1.0, 1.0];
assert!(trainer.set_frozen("b", true));
assert!(!trainer.set_frozen("nope", true));
assert!(trainer.is_frozen("b"));
for _ in 0..5 {
trainer.step(&[("x", &x), ("t", &t)]);
}
let held = trainer.params();
assert_eq!(held["b"], start["b"], "a frozen parameter moved");
assert_ne!(held["w"], start["w"], "the unfrozen parameter did not move");
let moments = trainer.moments();
let (_, m_b, v_b) = moments.iter().find(|(n, _, _)| n == "b").unwrap();
assert!(
m_b.iter().all(|x| *x == 0.0) && v_b.iter().all(|x| *x == 0.0),
"a frozen parameter accumulated moments"
);
trainer.set_frozen("b", false);
for _ in 0..5 {
trainer.step(&[("x", &x), ("t", &t)]);
}
assert_ne!(
trainer.params()["b"],
start["b"],
"unfreezing did not resume updates"
);
}
#[test]
fn moments_and_step_count_round_trip() {
const DIM: usize = 4;
let x: Vec<f32> = vec![1.0, 0.5, -0.5, 2.0, 0.25, -1.0, 1.5, 0.0];
let t: Vec<f32> = vec![1.0, -1.0, 0.5, 0.0, 0.0, 2.0, -1.0, 1.0];
let spec = AdamSpec::new(0.05);
let (forward, wrt) = regression(2, DIM);
let mut a =
ResidentTrainer::new(&forward, &wrt, &initial(DIM), &spec, Device::Cpu).unwrap();
for _ in 0..6 {
a.step(&[("x", &x), ("t", &t)]);
}
let snapshot = a.params();
let moments = a.moments();
let steps = a.steps();
let continued = {
let mut out = Vec::new();
for _ in 0..6 {
out.push(a.step(&[("x", &x), ("t", &t)])[0][0]);
}
out
};
let (forward2, wrt2) = regression(2, DIM);
let mut b =
ResidentTrainer::new(&forward2, &wrt2, &initial(DIM), &spec, Device::Cpu).unwrap();
b.set_params(&snapshot);
assert!(b.restore_moments(&moments, steps));
let resumed: Vec<f32> = (0..6)
.map(|_| b.step(&[("x", &x), ("t", &t)])[0][0])
.collect();
for (step, (a, b)) in continued.iter().zip(resumed.iter()).enumerate() {
assert_eq!(
a.to_bits(),
b.to_bits(),
"step {step}: resumed {b} != uninterrupted {a}"
);
}
}
#[test]
fn an_unknown_trainable_parameter_is_an_error_not_a_panic() {
let (forward, _) = regression(2, 4);
let wrt = vec![TrainableParam {
name: "nope".into(),
node: NodeId(0),
}];
match ResidentTrainer::new(
&forward,
&wrt,
&initial(4),
&AdamSpec::default(),
Device::Cpu,
) {
Err(FuseError::UnknownParam(name)) => assert_eq!(name, "nope"),
Err(other) => panic!("wrong error: {other}"),
Ok(_) => panic!("'nope' is not a Param, so this must fail"),
}
}
#[test]
fn aux_outputs_survive_the_fusion() {
let mut g = Graph::new("aux");
let x = g.input("x", Shape::new(&[2, 3], DType::F32));
let w = g.param("w", Shape::new(&[3, 3], DType::F32));
let logits = g.mm(x, w);
let sq = g.mul(logits, logits);
let loss = g.mean(sq, vec![0, 1], false);
g.set_outputs(vec![loss, logits]);
let wrt = vec![TrainableParam {
name: "w".into(),
node: w,
}];
let mut start = HashMap::new();
start.insert("w".to_string(), vec![0.1; 9]);
let mut trainer =
ResidentTrainer::new(&g, &wrt, &start, &AdamSpec::new(0.01), Device::Cpu).unwrap();
let out = trainer.step(&[("x", &[1.0, 2.0, 3.0, 4.0, 5.0, 6.0])]);
assert_eq!(
out.len(),
2,
"expected [loss, logits], got {} outputs",
out.len()
);
assert_eq!(out[0].len(), 1, "loss should be scalar");
assert_eq!(out[1].len(), 6, "logits should be [2, 3]");
}
}
#[cfg(test)]
mod forward_only_tests {
use super::tests_support::*;
use super::*;
#[test]
fn forward_only_moves_nothing() {
let (forward, wrt) = regression(2, 4);
let start = initial(4);
let mut trainer =
ResidentTrainer::new(&forward, &wrt, &start, &AdamSpec::new(0.05), Device::Cpu)
.unwrap();
let x: Vec<f32> = vec![1.0, 0.5, -0.5, 2.0, 0.25, -1.0, 1.5, 0.0];
let t: Vec<f32> = vec![1.0, -1.0, 0.5, 0.0, 0.0, 2.0, -1.0, 1.0];
for _ in 0..4 {
trainer.step(&[("x", &x), ("t", &t)]);
}
let before = trainer.params();
let steps_before = trainer.steps();
let moments_before = trainer.moments();
let evaluated = trainer.forward_only(&[("x", &x), ("t", &t)]);
assert_eq!(evaluated.len(), 1);
assert_eq!(
trainer.steps(),
steps_before,
"forward_only advanced the counter"
);
assert_eq!(trainer.params(), before, "forward_only moved the weights");
assert_eq!(
trainer.moments(),
moments_before,
"forward_only moved the moments"
);
let with_eval = trainer.step(&[("x", &x), ("t", &t)])[0][0];
let (forward2, wrt2) = regression(2, 4);
let mut clean = ResidentTrainer::new(
&forward2,
&wrt2,
&initial(4),
&AdamSpec::new(0.05),
Device::Cpu,
)
.unwrap();
for _ in 0..4 {
clean.step(&[("x", &x), ("t", &t)]);
}
let without_eval = clean.step(&[("x", &x), ("t", &t)])[0][0];
assert_eq!(
with_eval.to_bits(),
without_eval.to_bits(),
"a validation pass changed the trajectory: {with_eval} vs {without_eval}"
);
}
}