pub(crate) use super::*;
pub(crate) use crate::autograd::Variable;
pub(crate) use crate::graph::{
ArgmaxSelector, FixedSelector, FlowBuilder, LearnedHalt, MergeOp, Reduce,
SigmoidRouter, SoftmaxRouter, ThresholdHalt,
};
pub(crate) use crate::nn::{
Identity, Linear, LoopBody, NamedInputModule, Optimizer, ReLU, SGD, Sigmoid,
TraceEmit, forward_via_step, mse_loss,
};
pub(crate) use crate::tensor::Tensor;
pub(crate) use std::collections::HashMap;
mod flow_and_routing;
mod loops;
mod map_and_inputs;
mod misc;
mod observation;
pub(super) fn from_f32(data: &[f32], shape: &[i64]) -> Tensor {
Tensor::from_f32(data, shape, crate::tensor::test_device()).unwrap()
}
pub(super) struct Doubler;
impl Module for Doubler {
fn forward(&self, input: &Variable) -> Result<Variable> {
input.add(input)
}
}
pub(super) struct BiasStep {
bias: Parameter,
}
impl BiasStep {
fn new(size: i64) -> Result<Self> {
let data = Tensor::zeros(&[size], crate::tensor::test_opts())?;
let var = Variable::new(data, true);
Ok(BiasStep {
bias: Parameter {
variable: var,
name: "loop_bias".to_string(),
},
})
}
}
impl Module for BiasStep {
fn forward(&self, input: &Variable) -> Result<Variable> {
input.add(&self.bias.variable)
}
fn parameters(&self) -> Vec<Parameter> {
vec![self.bias.clone()]
}
}
pub(super) struct AddRefModule;
impl Module for AddRefModule {
fn forward(&self, input: &Variable) -> Result<Variable> {
Ok(input.clone())
}
fn as_named_input(&self) -> Option<&dyn NamedInputModule> { Some(self) }
}
impl NamedInputModule for AddRefModule {
fn forward_named(
&self,
input: &Variable,
refs: &HashMap<String, Variable>,
) -> Result<Variable> {
if let Some(ctx) = refs.get("ctx") {
input.add(ctx)
} else {
Ok(input.clone())
}
}
}
#[test]
fn test_single_module() {
let l = Linear::on_device(3, 2, crate::tensor::test_device()).unwrap();
let graph = FlowBuilder::from(l).build().unwrap();
let x = Variable::new(from_f32(&[1.0, 2.0, 3.0], &[1, 3]), false);
let y = graph.forward(&x).unwrap();
assert_eq!(y.shape(), vec![1, 2]);
}
#[test]
fn test_linear_chain() {
let graph = FlowBuilder::from(Linear::on_device(3, 4, crate::tensor::test_device()).unwrap())
.through(ReLU::new())
.through(Linear::on_device(4, 2, crate::tensor::test_device()).unwrap())
.build()
.unwrap();
let x = Variable::new(from_f32(&[1.0, 2.0, 3.0], &[1, 3]), false);
let y = graph.forward(&x).unwrap();
assert_eq!(y.shape(), vec![1, 2]);
}
#[test]
fn test_also_residual() {
let l1 = Linear::on_device(3, 3, crate::tensor::test_device()).unwrap();
l1.weight.variable.set_data(from_f32(
&[1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0],
&[3, 3],
));
l1.bias
.as_ref()
.unwrap()
.variable
.set_data(from_f32(&[0.0, 0.0, 0.0], &[3]));
let l2 = Linear::on_device(3, 3, crate::tensor::test_device()).unwrap();
l2.weight.variable.set_data(from_f32(
&[1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0],
&[3, 3],
));
l2.bias
.as_ref()
.unwrap()
.variable
.set_data(from_f32(&[1.0, 1.0, 1.0], &[3]));
let graph = FlowBuilder::from(l1).also(l2).build().unwrap();
let x = Variable::new(from_f32(&[1.0, 2.0, 3.0], &[1, 3]), false);
let y = graph.forward(&x).unwrap();
let data = y.data().to_f32_vec().unwrap();
assert!((data[0] - 3.0).abs() < 1e-5);
assert!((data[1] - 5.0).abs() < 1e-5);
assert!((data[2] - 7.0).abs() < 1e-5);
}
#[test]
fn test_fork_basic() {
let l = Linear::on_device(2, 3, crate::tensor::test_device()).unwrap();
let graph = FlowBuilder::from(Identity)
.fork(l)
.tag("side")
.through(ReLU::new())
.build()
.unwrap();
let x = Variable::new(from_f32(&[1.0, -2.0], &[1, 2]), false);
let y = graph.forward(&x).unwrap();
assert_eq!(y.shape(), vec![1, 2]);
let data = y.data().to_f32_vec().unwrap();
assert!((data[0] - 1.0).abs() < 1e-5);
assert!((data[1] - 0.0).abs() < 1e-5);
let side = graph.tagged("side").unwrap();
assert_eq!(side.shape(), vec![1, 3]);
}
#[test]
fn test_fork_multiple() {
let head_a = Linear::on_device(4, 3, crate::tensor::test_device()).unwrap();
let head_b = Linear::on_device(4, 2, crate::tensor::test_device()).unwrap();
let graph = FlowBuilder::from(Linear::on_device(2, 4, crate::tensor::test_device()).unwrap())
.tag("latent")
.fork(head_a)
.tag("head_a")
.fork(head_b)
.tag("head_b")
.build()
.unwrap();
let x = Variable::new(from_f32(&[1.0, 2.0], &[1, 2]), false);
let y = graph.forward(&x).unwrap();
assert_eq!(y.shape(), vec![1, 4]);
let a = graph.tagged("head_a").unwrap();
assert_eq!(a.shape(), vec![1, 3]);
let b = graph.tagged("head_b").unwrap();
assert_eq!(b.shape(), vec![1, 2]);
}
#[test]
fn test_fork_backward() {
let graph = FlowBuilder::from(Linear::on_device(2, 4, crate::tensor::test_device()).unwrap())
.fork(Linear::on_device(4, 3, crate::tensor::test_device()).unwrap())
.tag("side")
.through(Linear::on_device(4, 1, crate::tensor::test_device()).unwrap())
.build()
.unwrap();
let x = Variable::new(from_f32(&[1.0, 2.0], &[1, 2]), true);
let y = graph.forward(&x).unwrap();
let side = graph.tagged("side").unwrap();
let loss = y.sum().unwrap().add(&side.sum().unwrap()).unwrap();
loss.backward().unwrap();
assert!(x.grad().is_some(), "input should have gradient");
for p in graph.parameters() {
assert!(p.variable.grad().is_some(), "{} should have gradient", p.name);
}
}
#[test]
fn test_split_merge_add() {
let graph = FlowBuilder::from(Linear::on_device(3, 3, crate::tensor::test_device()).unwrap())
.split(vec![Box::new(ReLU::new()), Box::new(Sigmoid::new())])
.merge(MergeOp::Add)
.build()
.unwrap();
let x = Variable::new(from_f32(&[1.0, -1.0, 2.0], &[1, 3]), false);
let y = graph.forward(&x).unwrap();
assert_eq!(y.shape(), vec![1, 3]);
}
#[test]
fn test_split_merge_mean() {
let l = Linear::on_device(2, 2, crate::tensor::test_device()).unwrap();
l.weight
.variable
.set_data(from_f32(&[1.0, 0.0, 0.0, 1.0], &[2, 2]));
l.bias
.as_ref()
.unwrap()
.variable
.set_data(from_f32(&[0.0, 0.0], &[2]));
let b1 = Linear::on_device(2, 2, crate::tensor::test_device()).unwrap();
b1.weight
.variable
.set_data(from_f32(&[1.0, 0.0, 0.0, 1.0], &[2, 2]));
b1.bias
.as_ref()
.unwrap()
.variable
.set_data(from_f32(&[0.0, 0.0], &[2]));
let b2 = Linear::on_device(2, 2, crate::tensor::test_device()).unwrap();
b2.weight
.variable
.set_data(from_f32(&[1.0, 0.0, 0.0, 1.0], &[2, 2]));
b2.bias
.as_ref()
.unwrap()
.variable
.set_data(from_f32(&[0.0, 0.0], &[2]));
let graph = FlowBuilder::from(l)
.split(vec![Box::new(b1), Box::new(b2)])
.merge(MergeOp::Mean)
.build()
.unwrap();
let x = Variable::new(from_f32(&[3.0, 7.0], &[1, 2]), false);
let y = graph.forward(&x).unwrap();
let data = y.data().to_f32_vec().unwrap();
assert!((data[0] - 3.0).abs() < 1e-5);
assert!((data[1] - 7.0).abs() < 1e-5);
}
#[test]
fn test_parameters() {
let graph = FlowBuilder::from(Linear::on_device(3, 4, crate::tensor::test_device()).unwrap())
.through(ReLU::new())
.through(Linear::on_device(4, 2, crate::tensor::test_device()).unwrap())
.build()
.unwrap();
let params = graph.parameters();
assert_eq!(params.len(), 4);
}
#[test]
fn test_graph_backward() {
let l1 = Linear::on_device(3, 2, crate::tensor::test_device()).unwrap();
let l2 = Linear::on_device(2, 1, crate::tensor::test_device()).unwrap();
let graph = FlowBuilder::from(l1)
.through(ReLU::new())
.through(l2)
.build()
.unwrap();
let x = Variable::new(from_f32(&[1.0, 2.0, 3.0], &[1, 3]), true);
let y = graph.forward(&x).unwrap();
let loss = y.sum().unwrap();
loss.backward().unwrap();
for p in graph.parameters() {
assert!(p.variable.grad().is_some(), "{} should have gradient", p.name);
}
assert!(x.grad().is_some());
}
#[test]
fn test_graph_as_module() {
let inner = FlowBuilder::from(Linear::on_device(3, 4, crate::tensor::test_device()).unwrap())
.through(ReLU::new())
.build()
.unwrap();
let outer = FlowBuilder::from(inner)
.through(Linear::on_device(4, 2, crate::tensor::test_device()).unwrap())
.build()
.unwrap();
let x = Variable::new(from_f32(&[1.0, 2.0, 3.0], &[1, 3]), false);
let y = outer.forward(&x).unwrap();
assert_eq!(y.shape(), vec![1, 2]);
assert_eq!(outer.parameters().len(), 4);
}
#[test]
fn test_training_loop() {
let graph = FlowBuilder::from(Linear::on_device(1, 1, crate::tensor::test_device()).unwrap())
.build()
.unwrap();
let params = graph.parameters();
let mut optim = SGD::new(¶ms, 0.01, 0.0);
let x = Variable::new(from_f32(&[1.0, 2.0, 3.0, 4.0], &[4, 1]), false);
let target = Variable::new(from_f32(&[3.0, 5.0, 7.0, 9.0], &[4, 1]), false);
let mut last_loss = f64::MAX;
for _ in 0..800 {
optim.zero_grad();
let pred = graph.forward(&x).unwrap();
let loss = mse_loss(&pred, &target).unwrap();
last_loss = loss.item().unwrap();
loss.backward().unwrap();
optim.step().unwrap();
}
assert!(last_loss < 0.01, "got loss={}", last_loss);
}
#[test]
fn test_also_backward() {
let graph = FlowBuilder::from(Linear::on_device(2, 2, crate::tensor::test_device()).unwrap())
.also(Linear::on_device(2, 2, crate::tensor::test_device()).unwrap())
.build()
.unwrap();
let x = Variable::new(from_f32(&[1.0, 2.0], &[1, 2]), true);
let y = graph.forward(&x).unwrap();
let loss = y.sum().unwrap();
loss.backward().unwrap();
assert!(x.grad().is_some());
for p in graph.parameters() {
assert!(p.variable.grad().is_some(), "{} should have gradient", p.name);
}
}
#[test]
fn test_split_merge_backward() {
let graph = FlowBuilder::from(Linear::on_device(2, 2, crate::tensor::test_device()).unwrap())
.split(vec![
Box::new(Linear::on_device(2, 2, crate::tensor::test_device()).unwrap()),
Box::new(Linear::on_device(2, 2, crate::tensor::test_device()).unwrap()),
])
.merge(MergeOp::Add)
.build()
.unwrap();
let x = Variable::new(from_f32(&[1.0, 2.0], &[1, 2]), true);
let y = graph.forward(&x).unwrap();
let loss = y.sum().unwrap();
loss.backward().unwrap();
assert!(x.grad().is_some());
for p in graph.parameters() {
assert!(p.variable.grad().is_some(), "{} should have gradient", p.name);
}
}
#[test]
fn test_build_error_open_streams() {
let result = FlowBuilder::from(Linear::on_device(2, 2, crate::tensor::test_device()).unwrap())
.split(vec![Box::new(ReLU::new()), Box::new(Sigmoid::new())])
.build();
assert!(result.is_err());
}
#[test]
fn test_build_error_duplicate_tag() {
let result = FlowBuilder::from(Linear::on_device(2, 2, crate::tensor::test_device()).unwrap())
.tag("features")
.through(ReLU::new())
.tag("features")
.build();
assert!(result.is_err());
}
#[test]
fn test_using_backward_ref() {
let l = Linear::on_device(2, 2, crate::tensor::test_device()).unwrap();
l.weight
.variable
.set_data(from_f32(&[1.0, 0.0, 0.0, 1.0], &[2, 2]));
l.bias
.as_ref()
.unwrap()
.variable
.set_data(from_f32(&[0.0, 0.0], &[2]));
let graph = FlowBuilder::from(l)
.tag("ctx")
.through(AddRefModule)
.using(&["ctx"])
.build()
.unwrap();
let x = Variable::new(from_f32(&[3.0, 5.0], &[1, 2]), false);
let y = graph.forward(&x).unwrap();
let data = y.data().to_f32_vec().unwrap();
assert!((data[0] - 6.0).abs() < 1e-5);
assert!((data[1] - 10.0).abs() < 1e-5);
}
#[test]
fn test_using_backward_gradients() {
let l = Linear::on_device(2, 2, crate::tensor::test_device()).unwrap();
let graph = FlowBuilder::from(l)
.tag("ctx")
.through(AddRefModule)
.using(&["ctx"])
.build()
.unwrap();
let x = Variable::new(from_f32(&[1.0, 2.0], &[1, 2]), true);
let y = graph.forward(&x).unwrap();
let loss = y.sum().unwrap();
loss.backward().unwrap();
assert!(x.grad().is_some());
for p in graph.parameters() {
assert!(p.variable.grad().is_some(), "{} should have gradient", p.name);
}
}
#[test]
fn test_using_error_plain_module() {
let result = FlowBuilder::from(Linear::on_device(2, 2, crate::tensor::test_device()).unwrap())
.tag("ctx")
.through(ReLU::new())
.using(&["ctx"])
.build();
assert!(result.is_err());
}
#[test]
fn test_using_error_unknown_tag() {
let result = FlowBuilder::from(Linear::on_device(2, 2, crate::tensor::test_device()).unwrap())
.through(AddRefModule)
.using(&["nonexistent"])
.build();
assert!(result.is_err());
}
pub(super) struct SumRefs;
impl Module for SumRefs {
fn forward(&self, input: &Variable) -> Result<Variable> {
Ok(input.clone())
}
fn as_named_input(&self) -> Option<&dyn NamedInputModule> { Some(self) }
}
impl NamedInputModule for SumRefs {
fn forward_named(
&self,
input: &Variable,
refs: &HashMap<String, Variable>,
) -> Result<Variable> {
let mut result = input.clone();
for v in refs.values() {
result = result.add(v)?;
}
Ok(result)
}
}
pub(super) struct NilSafeAdd;
impl Module for NilSafeAdd {
fn forward(&self, input: &Variable) -> Result<Variable> {
Ok(input.clone())
}
fn as_named_input(&self) -> Option<&dyn NamedInputModule> { Some(self) }
}
impl NamedInputModule for NilSafeAdd {
fn forward_named(
&self,
input: &Variable,
refs: &HashMap<String, Variable>,
) -> Result<Variable> {
if let Some(memory) = refs.get("memory") {
input.add(memory)
} else {
Ok(input.clone())
}
}
}
pub(super) struct ScalarSum;
impl Module for ScalarSum {
fn forward(&self, input: &Variable) -> Result<Variable> {
input.sum()
}
}
pub(super) struct LinearSched(f64);
impl crate::nn::Scheduler for LinearSched {
fn lr(&self, step: usize) -> f64 { step as f64 * self.0 }
}
pub(super) fn graph_with_optim(initial_lr: f64) -> (crate::graph::Graph, Variable) {
use crate::nn::SGD;
let dev = crate::tensor::test_device();
let graph = FlowBuilder::from(Linear::on_device(2, 1, dev).unwrap())
.build()
.unwrap();
graph.set_optimizer(|p| SGD::new(p, initial_lr, 0.0));
let x = Variable::new(from_f32(&[1.0, 2.0], &[1, 2]), false);
(graph, x)
}
pub(super) fn current_optim_lr(graph: &crate::graph::Graph) -> f64 {
graph.optimizer.borrow().as_ref().map(|o| o.lr()).unwrap()
}
pub(super) struct Tripler;
impl Module for Tripler {
fn forward(&self, input: &Variable) -> Result<Variable> {
input.add(&input.add(input)?)
}
fn parameters(&self) -> Vec<Parameter> { vec![] }
}
pub(super) struct TracingDoubler {
last_output: RefCell<Option<Variable>>,
}
impl TracingDoubler {
fn new() -> Self {
TracingDoubler {
last_output: RefCell::new(None),
}
}
}
impl Module for TracingDoubler {
fn forward(&self, input: &Variable) -> Result<Variable> {
let out = input.add(input)?;
*self.last_output.borrow_mut() = Some(out.clone());
Ok(out)
}
fn trace(&self) -> Option<Variable> {
self.last_output.borrow().clone()
}
}
#[test]
fn test_split_merge_mean_batched() {
let graph = FlowBuilder::from(Identity)
.split(vec![Box::new(Doubler), Box::new(Tripler)])
.merge(MergeOp::Mean)
.build()
.unwrap();
let x = Variable::new(from_f32(&[1.0, 2.0, 10.0, 20.0], &[2, 2]), false);
let y = graph.forward(&x).unwrap();
assert_eq!(y.shape(), vec![2, 2]);
let d = y.data().to_f32_vec().unwrap();
for (i, base) in [1.0f32, 2.0, 10.0, 20.0].iter().enumerate() {
let want = base * 2.5; assert!((d[i] - want).abs() < 1e-4, "elem {i}: want {want}, got {}", d[i]);
}
}
#[test]
fn test_using_ref_is_row_wise_batched() {
let graph = FlowBuilder::from(Identity)
.tag("ctx")
.through(AddRefModule)
.using(&["ctx"])
.build()
.unwrap();
let x = Variable::new(from_f32(&[1.0, 2.0, 10.0, 20.0], &[2, 2]), false);
let y = graph.forward(&x).unwrap();
let d = y.data().to_f32_vec().unwrap();
for (i, base) in [1.0f32, 2.0, 10.0, 20.0].iter().enumerate() {
let want = base * 2.0; assert!((d[i] - want).abs() < 1e-5, "elem {i}: want {want}, got {}", d[i]);
}
}
#[test]
fn test_split_merge_add_batched() {
let graph = FlowBuilder::from(Identity)
.split(vec![Box::new(Doubler), Box::new(Tripler)])
.merge(MergeOp::Add)
.build()
.unwrap();
let x = Variable::new(from_f32(&[1.0, 2.0, 10.0, 20.0], &[2, 2]), false);
let y = graph.forward(&x).unwrap();
assert_eq!(y.shape(), vec![2, 2]);
let d = y.data().to_f32_vec().unwrap();
for (i, base) in [1.0f32, 2.0, 10.0, 20.0].iter().enumerate() {
let want = base * 5.0; assert!((d[i] - want).abs() < 1e-4, "elem {i}: want {want}, got {}", d[i]);
}
}
#[test]
fn test_fork_batched_main_and_side_are_row_wise() {
let graph = FlowBuilder::from(Identity)
.fork(Tripler)
.tag("side")
.through(Doubler)
.build()
.unwrap();
let x = Variable::new(from_f32(&[1.0, -2.0, 10.0, -20.0], &[2, 2]), false);
let y = graph.forward(&x).unwrap();
assert_eq!(y.shape(), vec![2, 2]);
let main = y.data().to_f32_vec().unwrap();
for (i, base) in [1.0f32, -2.0, 10.0, -20.0].iter().enumerate() {
let want = base * 2.0;
assert!((main[i] - want).abs() < 1e-5, "main elem {i}: want {want}, got {}", main[i]);
}
let side = graph.tagged("side").unwrap().data().to_f32_vec().unwrap();
for (i, base) in [1.0f32, -2.0, 10.0, -20.0].iter().enumerate() {
let want = base * 3.0;
assert!((side[i] - want).abs() < 1e-5, "side elem {i}: want {want}, got {}", side[i]);
}
}