use std::collections::HashSet;
use std::sync::Arc;
use crate::array::{Data, Node, State};
use crate::kernels::{self, Operand};
use crate::primitive::Primitive;
use crate::{Array, DeviceKind, Error, Result, view};
pub fn eval(arrays: &[Array]) -> Result<()> {
for node in topological_order(arrays) {
evaluate(&node);
}
match arrays
.iter()
.find_map(|a| a.out().slot.get().and_then(|r| r.as_ref().err()))
{
Some(err) => Err(err.clone()),
None => Ok(()),
}
}
fn topological_order(arrays: &[Array]) -> Vec<Arc<Node>> {
let mut visited = HashSet::new();
let mut order = Vec::new();
let mut stack: Vec<(Arc<Node>, bool)> =
arrays.iter().map(|a| (a.node.clone(), false)).collect();
while let Some((node, expanded)) = stack.pop() {
if expanded {
order.push(node);
continue;
}
if !visited.insert(Arc::as_ptr(&node)) {
continue;
}
let inputs: Vec<Arc<Node>> = match &*node.lock() {
State::Pending { inputs, .. } => inputs.iter().map(|a| a.node.clone()).collect(),
State::Done => continue,
};
stack.push((node, true));
stack.extend(
inputs
.into_iter()
.filter(|n| !visited.contains(&Arc::as_ptr(n)))
.map(|n| (n, false)),
);
}
order
}
fn evaluate(node: &Node) {
let mut state = node.lock();
let State::Pending { primitive, inputs } = std::mem::replace(&mut *state, State::Done) else {
return; };
#[cfg(test)]
counter::inc();
match run(node, &primitive, &inputs) {
Ok(outputs) => {
for (output, data) in node.outputs.iter().zip(outputs) {
let _ = output.slot.set(Ok(data));
}
}
Err(err) => {
for output in &node.outputs {
let _ = output.slot.set(Err(err.clone()));
}
}
}
drop(state);
drop(inputs); }
fn run(node: &Node, primitive: &Primitive, inputs: &[Array]) -> Result<Vec<Data>> {
let mut operands: Vec<Operand<'_>> = Vec::with_capacity(inputs.len());
for input in inputs {
match input
.out()
.slot
.get()
.expect("inputs are evaluated before consumers")
{
Ok(data) => operands.push((input.out(), data)),
Err(err) => return Err(err.clone()),
}
}
if node.stream.device.kind != DeviceKind::Cpu {
return Err(Error::UnsupportedDevice(node.stream.device));
}
let new = |buffer| Ok(vec![Data::new(buffer)]);
let share = |offset| {
Ok(vec![Data {
buffer: operands[0].1.buffer.clone(),
offset,
}])
};
match primitive {
Primitive::Unary(op) => new(kernels::unary(*op, operands[0])?),
Primitive::Binary(op) => new(kernels::binary(*op, operands[0], operands[1])?),
Primitive::Select => new(kernels::select(operands[0], operands[1], operands[2])?),
Primitive::AsType(dtype) => new(kernels::cast(operands[0], *dtype)?),
Primitive::Reduce { op, axes } => new(kernels::reduce(*op, operands[0], axes)?),
Primitive::ArgReduce { op, axis } => new(kernels::arg_reduce(*op, operands[0], *axis)?),
Primitive::Matmul => new(kernels::matmul(operands[0], operands[1])?),
Primitive::Transpose(_) | Primitive::Broadcast(_) => share(operands[0].1.offset),
Primitive::Slice { .. } => {
let (meta, data) = operands[0];
let (_, delta) =
view::slice_geometry(&meta.strides, &primitive.dim_slices(meta.shape.len())?);
share(data.offset + delta)
}
Primitive::Reshape(shape) => {
let (meta, data) = operands[0];
if view::nocopy_reshape(&meta.shape, &meta.strides, shape).is_some() {
share(data.offset)
} else {
new(kernels::contiguous(meta, data)?)
}
}
#[cfg(test)]
Primitive::AddMultiply => {
use crate::primitive::BinaryOp;
Ok(vec![
Data::new(kernels::binary(BinaryOp::Add, operands[0], operands[1])?),
Data::new(kernels::binary(
BinaryOp::Multiply,
operands[0],
operands[1],
)?),
])
}
}
}
#[cfg(test)]
pub(crate) mod counter {
use std::cell::Cell;
thread_local!(static EVALUATED: Cell<usize> = const { Cell::new(0) });
pub(crate) fn inc() {
EVALUATED.set(EVALUATED.get() + 1);
}
pub(crate) fn take() -> usize {
EVALUATED.replace(0)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::buffer::live;
#[test]
fn shared_node_runs_once() {
let x = Array::from_slice(&[1.0f32, 2.0], &[2]).unwrap();
let shared = &x + &x;
let left = &shared * &x;
let right = &shared + &x;
let top = &left + &right;
counter::take();
eval(&[top.clone(), left.clone()]).unwrap();
assert_eq!(counter::take(), 4, "shared, left, right, top: once each");
top.eval().unwrap();
assert_eq!(counter::take(), 0, "already evaluated");
assert_eq!(top.to_vec::<f32>().unwrap(), vec![5.0, 14.0]);
}
#[test]
fn intermediates_freed() {
let before = live::count();
let mut x = Array::scalar(0i64);
let one = Array::scalar(1i64);
let steps = if cfg!(miri) { 100 } else { 10_000 };
for _ in 0..steps {
x = &x + &one;
}
x.eval().unwrap();
drop(one);
assert_eq!(
live::count() - before,
1,
"only the result's buffer is alive"
);
assert_eq!(x.item::<i64>().unwrap(), steps as i64);
}
#[test]
fn multi_output_once() {
let a = Array::from_slice(&[2i32, 3], &[2]).unwrap();
let b = Array::from_slice(&[5i32, 7], &[2]).unwrap();
let mut outs = Array::from_op(Primitive::AddMultiply, vec![a, b]).unwrap();
let product = outs.pop().unwrap();
let sum = outs.pop().unwrap();
counter::take();
eval(&[sum.clone(), product.clone()]).unwrap();
assert_eq!(counter::take(), 1);
drop(sum);
assert_eq!(product.to_vec::<i32>().unwrap(), vec![10, 21]);
}
#[test]
fn evaluation_detaches_inputs() {
let a = Array::scalar(1u8);
let c = &a + &a;
assert!(c.primitive().is_some());
c.eval().unwrap();
assert_eq!(c.primitive(), None);
assert_eq!(Arc::strong_count(&a.node), 1, "c no longer references a");
}
}