use super::*;
#[test]
fn test_loop_for() {
let graph = FlowBuilder::from(Linear::on_device(2, 2, crate::tensor::test_device()).unwrap())
.loop_body(Doubler)
.for_n(3)
.build()
.unwrap();
let params = graph.parameters();
params[0].variable.set_data(from_f32(&[1.0, 0.0, 0.0, 1.0], &[2, 2]));
params[1].variable.set_data(from_f32(&[0.0, 0.0], &[2]));
let x = Variable::new(from_f32(&[1.0, 2.0], &[1, 2]), false);
let y = graph.forward(&x).unwrap();
let data = y.data().to_f32_vec().unwrap();
assert!((data[0] - 8.0).abs() < 1e-5, "1*2^3=8, got {}", data[0]);
assert!((data[1] - 16.0).abs() < 1e-5, "2*2^3=16, got {}", data[1]);
}
#[test]
fn test_loop_for_backward() {
let bias_step = BiasStep::new(2).unwrap();
let graph = FlowBuilder::from(Linear::on_device(2, 2, crate::tensor::test_device()).unwrap())
.loop_body(bias_step)
.for_n(3)
.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();
for p in graph.parameters() {
assert!(p.variable.grad().is_some(), "{} should have gradient", p.name);
}
let all_params = graph.parameters();
let bias_param = all_params.iter().find(|p| p.name == "loop_bias").unwrap();
let grad = bias_param.variable.grad().unwrap().to_f32_vec().unwrap();
assert!(
(grad[0] - 3.0).abs() < 1e-5,
"bias grad should be 3, got {}",
grad[0]
);
}
#[test]
fn test_loop_while() {
let graph = FlowBuilder::from(Linear::on_device(2, 2, crate::tensor::test_device()).unwrap())
.loop_body(Doubler)
.while_cond(ThresholdHalt::new(10.0), 20)
.build()
.unwrap();
let params = graph.parameters();
params[0].variable.set_data(from_f32(&[1.0, 0.0, 0.0, 1.0], &[2, 2]));
params[1].variable.set_data(from_f32(&[0.0, 0.0], &[2]));
let x = Variable::new(from_f32(&[1.0, 2.0], &[1, 2]), false);
let y = graph.forward(&x).unwrap();
let data = y.data().to_f32_vec().unwrap();
assert!((data[0] - 8.0).abs() < 1e-5, "got {}", data[0]);
assert!((data[1] - 16.0).abs() < 1e-5, "got {}", data[1]);
}
#[test]
fn test_loop_while_immediate_halt() {
let graph = FlowBuilder::from(Linear::on_device(2, 2, crate::tensor::test_device()).unwrap())
.loop_body(Doubler)
.while_cond(ThresholdHalt::new(0.5), 20)
.build()
.unwrap();
let params = graph.parameters();
params[0].variable.set_data(from_f32(&[1.0, 0.0, 0.0, 1.0], &[2, 2]));
params[1].variable.set_data(from_f32(&[0.0, 0.0], &[2]));
let x = Variable::new(from_f32(&[1.0, 2.0], &[1, 2]), false);
let y = graph.forward(&x).unwrap();
let data = y.data().to_f32_vec().unwrap();
assert!((data[0] - 1.0).abs() < 1e-5);
assert!((data[1] - 2.0).abs() < 1e-5);
}
#[test]
fn test_loop_until() {
let graph = FlowBuilder::from(Linear::on_device(2, 2, crate::tensor::test_device()).unwrap())
.loop_body(Doubler)
.until_cond(ThresholdHalt::new(10.0), 20)
.build()
.unwrap();
let params = graph.parameters();
params[0].variable.set_data(from_f32(&[1.0, 0.0, 0.0, 1.0], &[2, 2]));
params[1].variable.set_data(from_f32(&[0.0, 0.0], &[2]));
let x = Variable::new(from_f32(&[1.0, 2.0], &[1, 2]), false);
let y = graph.forward(&x).unwrap();
let data = y.data().to_f32_vec().unwrap();
assert!((data[0] - 8.0).abs() < 1e-5, "got {}", data[0]);
assert!((data[1] - 16.0).abs() < 1e-5, "got {}", data[1]);
}
#[test]
fn test_loop_until_at_least_once() {
let graph = FlowBuilder::from(Linear::on_device(2, 2, crate::tensor::test_device()).unwrap())
.loop_body(Doubler)
.until_cond(ThresholdHalt::new(0.5), 20)
.build()
.unwrap();
let params = graph.parameters();
params[0].variable.set_data(from_f32(&[1.0, 0.0, 0.0, 1.0], &[2, 2]));
params[1].variable.set_data(from_f32(&[0.0, 0.0], &[2]));
let x = Variable::new(from_f32(&[1.0, 2.0], &[1, 2]), false);
let y = graph.forward(&x).unwrap();
let data = y.data().to_f32_vec().unwrap();
assert!((data[0] - 2.0).abs() < 1e-5, "got {}", data[0]);
assert!((data[1] - 4.0).abs() < 1e-5, "got {}", data[1]);
}
#[test]
fn test_loop_parameters() {
let graph = FlowBuilder::from(Linear::on_device(2, 2, crate::tensor::test_device()).unwrap())
.loop_body(Linear::on_device(2, 2, crate::tensor::test_device()).unwrap())
.for_n(3)
.build()
.unwrap();
let params = graph.parameters();
assert_eq!(params.len(), 4);
}
#[test]
fn test_loop_while_parameters() {
let graph = FlowBuilder::from(Linear::on_device(2, 2, crate::tensor::test_device()).unwrap())
.loop_body(Linear::on_device(2, 2, crate::tensor::test_device()).unwrap())
.while_cond(Linear::on_device(2, 1, crate::tensor::test_device()).unwrap(), 10)
.build()
.unwrap();
let params = graph.parameters();
assert_eq!(params.len(), 6);
}
#[test]
fn test_loop_in_chain() {
let graph = FlowBuilder::from(Linear::on_device(3, 4, crate::tensor::test_device()).unwrap())
.loop_body(ReLU::new())
.for_n(3)
.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_loop_using_backward_ref() {
let graph = FlowBuilder::from(Identity)
.tag("ctx")
.loop_body(AddRefModule)
.for_n(3)
.using(&["ctx"])
.build()
.unwrap();
let x = Variable::new(from_f32(&[2.0, 3.0], &[1, 2]), false);
let y = graph.forward(&x).unwrap();
let data = y.data().to_f32_vec().unwrap();
assert!((data[0] - 8.0).abs() < 1e-5, "got {}", data[0]);
assert!((data[1] - 12.0).abs() < 1e-5, "got {}", data[1]);
}
#[test]
fn test_loop_using_backward_gradients() {
let graph = FlowBuilder::from(Linear::on_device(2, 2, crate::tensor::test_device()).unwrap())
.tag("ctx")
.loop_body(AddRefModule)
.for_n(2)
.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(), "input should have gradient");
for p in graph.parameters() {
assert!(p.variable.grad().is_some(), "{} should have gradient", p.name);
}
}
#[test]
fn test_loop_traces() {
let graph = FlowBuilder::from(Identity)
.loop_body(TracingDoubler::new())
.for_n(3)
.build()
.unwrap();
let x = Variable::new(from_f32(&[1.0, 2.0], &[1, 2]), false);
let y = graph.forward(&x).unwrap();
let data = y.data().to_f32_vec().unwrap();
assert!((data[0] - 8.0).abs() < 1e-5);
let traces = graph.traces("any").unwrap();
assert_eq!(traces.len(), 3, "3 iterations = 3 traces");
let t0 = traces[0].data().to_f32_vec().unwrap();
assert!((t0[0] - 2.0).abs() < 1e-5, "iter0: [2,4], got {}", t0[0]);
let t1 = traces[1].data().to_f32_vec().unwrap();
assert!((t1[0] - 4.0).abs() < 1e-5, "iter1: [4,8], got {}", t1[0]);
let t2 = traces[2].data().to_f32_vec().unwrap();
assert!((t2[0] - 8.0).abs() < 1e-5, "iter2: [8,16], got {}", t2[0]);
}
#[test]
fn test_loop_traces_cleared_each_forward() {
let graph = FlowBuilder::from(Identity)
.loop_body(TracingDoubler::new())
.for_n(2)
.build()
.unwrap();
let x = Variable::new(from_f32(&[1.0], &[1, 1]), false);
graph.forward(&x).unwrap();
let traces1 = graph.traces("any").unwrap();
assert_eq!(traces1.len(), 2);
graph.forward(&x).unwrap();
let traces2 = graph.traces("any").unwrap();
assert_eq!(traces2.len(), 2);
}
#[test]
fn test_loop_no_traces_without_trace_impl() {
let graph = FlowBuilder::from(Identity)
.loop_body(Doubler)
.for_n(3)
.build()
.unwrap();
let x = Variable::new(from_f32(&[1.0], &[1, 1]), false);
graph.forward(&x).unwrap();
assert!(graph.traces("any").is_none());
}
struct EmittingDoubler;
impl Module for EmittingDoubler {
fn forward(&self, input: &Variable) -> Result<Variable> {
forward_via_step(self, input)
}
fn as_loop_body(&self) -> Option<&dyn LoopBody> { Some(self) }
}
impl LoopBody for EmittingDoubler {
fn step(
&self,
input: &Variable,
_refs: &HashMap<String, Variable>,
emit: &mut TraceEmit<'_>,
) -> Result<Variable> {
let two_x = input.add(input)?;
let four_x = two_x.add(&two_x)?;
emit.publish("double", two_x.clone());
emit.publish("quad", four_x);
Ok(two_x)
}
}
struct SparseEmitter {
step_count: RefCell<usize>,
}
impl SparseEmitter {
fn new() -> Self { SparseEmitter { step_count: RefCell::new(0) } }
}
impl Module for SparseEmitter {
fn forward(&self, input: &Variable) -> Result<Variable> {
forward_via_step(self, input)
}
fn as_loop_body(&self) -> Option<&dyn LoopBody> { Some(self) }
fn reset(&self) { *self.step_count.borrow_mut() = 0; }
}
impl LoopBody for SparseEmitter {
fn step(
&self,
input: &Variable,
_refs: &HashMap<String, Variable>,
emit: &mut TraceEmit<'_>,
) -> Result<Variable> {
let i = *self.step_count.borrow();
*self.step_count.borrow_mut() += 1;
let out = input.add(input)?;
emit.publish("always", out.clone());
if i % 2 == 0 {
emit.publish("even_only", out.clone());
}
Ok(out)
}
}
struct DupEmitter;
impl Module for DupEmitter {
fn forward(&self, input: &Variable) -> Result<Variable> {
forward_via_step(self, input)
}
fn as_loop_body(&self) -> Option<&dyn LoopBody> { Some(self) }
}
impl LoopBody for DupEmitter {
fn step(
&self,
input: &Variable,
_refs: &HashMap<String, Variable>,
emit: &mut TraceEmit<'_>,
) -> Result<Variable> {
let two_x = input.add(input)?;
emit.publish("dup", two_x.clone());
emit.publish("dup", two_x.clone());
Ok(two_x)
}
}
#[test]
fn test_loop_body_emits_two_named_traces() {
let graph = FlowBuilder::from(Identity)
.loop_body(EmittingDoubler)
.for_n(3)
.build()
.unwrap();
let x = Variable::new(from_f32(&[1.0, 2.0], &[1, 2]), false);
let y = graph.forward(&x).unwrap();
let data = y.data().to_f32_vec().unwrap();
assert!((data[0] - 8.0).abs() < 1e-5, "final 8x = [8,16], got {}", data[0]);
let doubles = graph.traces("double").expect("double stream");
assert_eq!(doubles.len(), 3, "3 iterations = 3 emits of 'double'");
let quads = graph.traces("quad").expect("quad stream");
assert_eq!(quads.len(), 3, "3 iterations = 3 emits of 'quad'");
let d0 = doubles[0].data().to_f32_vec().unwrap();
assert!((d0[0] - 2.0).abs() < 1e-5);
let q0 = quads[0].data().to_f32_vec().unwrap();
assert!((q0[0] - 4.0).abs() < 1e-5);
let d2 = doubles[2].data().to_f32_vec().unwrap();
assert!((d2[0] - 8.0).abs() < 1e-5);
let q2 = quads[2].data().to_f32_vec().unwrap();
assert!((q2[0] - 16.0).abs() < 1e-5);
assert_eq!(graph.traces_named("double").unwrap().len(), 3);
assert_eq!(graph.traces_named("quad").unwrap().len(), 3);
assert!(graph.traces_named("nonexistent").is_none());
}
#[test]
fn test_loop_body_emit_cleared_each_forward() {
let graph = FlowBuilder::from(Identity)
.loop_body(EmittingDoubler)
.for_n(2)
.build()
.unwrap();
let x = Variable::new(from_f32(&[1.0], &[1, 1]), false);
graph.forward(&x).unwrap();
assert_eq!(graph.traces("double").unwrap().len(), 2);
graph.forward(&x).unwrap();
assert_eq!(graph.traces("double").unwrap().len(), 2);
assert_eq!(graph.traces("quad").unwrap().len(), 2);
}
#[test]
fn test_loop_body_emit_sparse() {
let graph = FlowBuilder::from(Identity)
.loop_body(SparseEmitter::new())
.for_n(4)
.build()
.unwrap();
let x = Variable::new(from_f32(&[1.0], &[1, 1]), false);
graph.forward(&x).unwrap();
assert_eq!(graph.traces("always").unwrap().len(), 4);
assert_eq!(graph.traces("even_only").unwrap().len(), 2);
}
#[should_panic(expected = "already published this step")]
#[test]
fn test_loop_body_emit_dup_panics() {
let graph = FlowBuilder::from(Identity)
.loop_body(DupEmitter)
.for_n(1)
.build()
.unwrap();
let x = Variable::new(from_f32(&[1.0], &[1, 1]), false);
let _ = graph.forward(&x);
}
#[test]
fn test_loop_for_batched() {
let graph = FlowBuilder::from(Identity)
.loop_body(Doubler)
.for_n(3)
.build()
.unwrap();
let x = Variable::new(from_f32(&[1.0, 2.0, 3.0, 4.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, 3.0, 4.0].iter().enumerate() {
let want = base * 8.0; assert!((d[i] - want).abs() < 1e-5, "elem {i}: want {want}, got {}", d[i]);
}
}
#[test]
fn test_loop_for_backward_batched() {
let graph = FlowBuilder::from(Identity)
.loop_body(Doubler)
.for_n(3)
.build()
.unwrap();
let x = Variable::new(from_f32(&[1.0, 2.0, 3.0, 4.0], &[2, 2]), true);
graph.forward(&x).unwrap().sum().unwrap().backward().unwrap();
let g = x.grad().expect("input must receive gradient").to_f32_vec().unwrap();
for (i, v) in g.iter().enumerate() {
assert!((v - 8.0).abs() < 1e-5, "elem {i} grad: want 8, got {v}");
}
}
#[test]
fn test_loop_traces_batched_keep_full_batch() {
let graph = FlowBuilder::from(Identity)
.loop_body(TracingDoubler::new())
.for_n(3)
.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 traces = graph.traces("any").unwrap();
assert_eq!(traces.len(), 3, "3 iterations = 3 traces");
for (iter, trace) in traces.iter().enumerate() {
assert_eq!(
trace.shape(),
vec![2, 2],
"trace {iter} must keep the whole batch"
);
let factor = 2.0f32.powi(iter as i32 + 1);
let d = trace.data().to_f32_vec().unwrap();
for (i, base) in [1.0f32, 2.0, 10.0, 20.0].iter().enumerate() {
let want = base * factor;
assert!(
(d[i] - want).abs() < 1e-4,
"trace {iter} elem {i}: want {want}, got {}",
d[i]
);
}
}
}