use std::collections::HashMap;
use std::sync::Arc;
use smallvec::SmallVec;
use static_assertions::assert_impl_all;
use crate::{Element, Numerics, Tensor};
use crate::backend::NumericsScope;
use crate::graph::{
Adjoints, Field, Gradients, Kinship, Network, Origin, Parameters, SlotStore, Structure, Symbol,
ValueId,
};
use crate::op::{Op, SlotId};
use super::Entry;
assert_impl_all!(Run<f64>: Send, Sync);
#[derive(Debug)]
pub(crate) enum Provenance {
Complete,
Sliced { computed: Vec<bool> },
Observed { readable: Arc<Vec<bool>> },
Training { readable: Arc<Vec<bool>> },
}
impl Provenance {
fn mask(&self) -> Option<&[bool]> {
match self {
Provenance::Complete => None,
Provenance::Sliced { computed } => Some(computed),
Provenance::Observed { readable } | Provenance::Training { readable, .. } => {
Some(readable)
}
}
}
fn differentiable(&self) -> bool {
!matches!(self, Provenance::Observed { .. })
}
}
#[derive(Debug)]
pub struct Run<E> {
structure: Structure<Tensor<E>>,
field: Field<E>,
provenance: Provenance,
numerics: Numerics,
}
impl<E: Element> Run<E> {
pub(crate) fn new(
structure: Structure<Tensor<E>>,
origin: Origin,
values: Vec<Tensor<E>>,
provenance: Provenance,
numerics: Numerics,
) -> Self {
debug_assert_eq!(structure.len(), values.len());
if let Some(mask) = provenance.mask() {
debug_assert_eq!(structure.len(), mask.len());
}
Self {
structure,
field: Field::new(origin, values),
provenance,
numerics,
}
}
fn computed(&self, index: usize) -> bool {
match self.provenance.mask() {
Some(mask) => mask[index],
None => true,
}
}
fn locate(&self, symbol: Symbol) -> usize {
Kinship::over(self.field.origin(), self.field.len())
.locate(symbol, "symbol was allocated after this run")
}
pub fn of(&self, symbol: Symbol) -> &Tensor<E> {
let index = self.locate(symbol);
assert!(
self.computed(index),
"value was not computed by this target-sliced run; add it to the targets"
);
&self.field.payloads()[index]
}
pub fn field(&self) -> &Field<E> {
&self.field
}
pub fn recorded_gradients(&self, adjoints: &Adjoints) -> Parameters<E> {
let mut recorded: HashMap<usize, Tensor<E>> = HashMap::new();
for &(parameter, gradient) in adjoints.pairs() {
let index = self.locate(parameter);
assert!(
matches!(self.structure.ops.get(index), Some(Op::Parameter(_))),
"recorded gradients fill parameter slots; a `wrt` entry of \
these adjoints is not a parameter"
);
let payload = self.of(gradient).clone();
assert_eq!(
payload.shape(),
self.structure
.shapes
.get(index)
.expect("shapes cover the run")
.clone(),
"recorded gradient shape does not match its parameter's"
);
recorded.insert(index, payload);
}
let values = self.field.payloads();
let rows = self
.structure
.ops
.iter()
.enumerate()
.filter(|(_, op)| matches!(op, Op::Parameter(_)))
.map(|(index, _)| {
let payload = recorded
.remove(&index)
.unwrap_or_else(|| values[index].zero_like());
(ValueId(index), payload)
});
Parameters::from_rows(self.field.origin(), rows)
}
}
impl<E: Element> Run<E> {
pub fn backward(&self, output: Symbol) -> Gradients<E> {
let output_index = self.locate(output);
let values = self.field.payloads();
assert!(
self.computed(output_index),
"value was not computed by this target-sliced run; add it to the targets"
);
assert!(
self.provenance.differentiable(),
"this run came from a forward-only plan, whose liveness pass freed \
the buffers backward reads; compile with `Entry::backward` to differentiate"
);
assert_eq!(
values[output_index].shape().rank(),
0,
"backward requires a scalar target; reduce it with `sum` first"
);
assert_eq!(
self.structure
.shapes
.get(output_index)
.expect("shapes cover the run")
.rank(),
0,
"backward requires a scalar target; reduce it with `sum` first"
);
let _numerics = NumericsScope::enter(self.numerics);
let mut gradients: Vec<Tensor<E>> = values.iter().map(|value| value.zero_like()).collect();
gradients[output_index] = values[output_index].one_like();
let mut ancestors = vec![false; output_index + 1];
ancestors[output_index] = true;
for index in (0..=output_index).rev() {
if !ancestors[index] {
continue;
}
let op = self
.structure
.ops
.get(index)
.expect("the freeze cannot shrink");
let links = self
.structure
.operands
.get(index)
.expect("the freeze cannot shrink")
.as_slice();
let operands: SmallVec<[&Tensor<E>; 2]> =
links.iter().map(|link| &values[link.index()]).collect();
let gradient = gradients[index].clone();
let cotangents = op.backward(&operands, &values[index], &gradient);
debug_assert_eq!(cotangents.len(), links.len());
for (&link, cotangent) in links.iter().zip(cotangents) {
if let Some(contribution) = cotangent {
let slot = link.index();
ancestors[slot] = true;
gradients[slot] = gradients[slot].clone() + contribution;
}
}
}
Field::new(self.field.origin(), gradients)
}
}
#[cfg(test)]
#[path = "tests/run_tests.rs"]
mod tests;
impl<E: Element> Network<E> {
pub fn forward(
&self,
parameters: &Parameters<E>,
feeds: impl IntoIterator<Item = (Symbol, Tensor<E>)>,
) -> Run<E> {
self.run(parameters, None, Numerics::Exact, feeds)
}
fn assert_covering(&self, parameters: &Parameters<E>) {
assert!(
parameters.origin() == self.origin(),
"parameters belong to a different network"
);
assert_eq!(
parameters.len(),
self.parameters_len(),
"parameters do not cover this network's parameter slots; \
carry them across a reopen with `Parameters::carried`"
);
}
pub(crate) fn interpret_entry(
&self,
entry: &Entry,
parameters: &Parameters<E>,
feeds: impl IntoIterator<Item = (Symbol, Tensor<E>)>,
) -> Run<E> {
let targets: Vec<ValueId> = entry
.roots
.iter()
.chain(&entry.observe)
.map(|&target| self.locate(target))
.collect();
self.run(parameters, Some(targets), entry.numerics, feeds)
}
fn input_slot(&self, id: ValueId) -> Option<SlotId> {
match self
.structure()
.ops
.get(id.index())
.expect("`ValueId` is in bounds for its network")
{
Op::Input(input) => Some(input.0),
_ => None,
}
}
fn run(
&self,
parameters: &Parameters<E>,
targets: Option<Vec<ValueId>>,
numerics: Numerics,
feeds: impl IntoIterator<Item = (Symbol, Tensor<E>)>,
) -> Run<E> {
self.assert_covering(parameters);
let _numerics = NumericsScope::enter(numerics);
let mut bindings = Vec::new();
for (symbol, payload) in feeds {
let id = self.locate(symbol);
let slot = self.input_slot(id).expect("only inputs can be fed");
let declared = self
.structure()
.shapes
.get(id.index())
.expect("shapes cover the network");
assert_eq!(
&payload.shape(),
declared,
"fed payload must match the input's recorded shape"
);
bindings.push((slot, payload));
}
let inputs = SlotStore::overlaid(self.inputs(), bindings);
let structure = self.structure();
let computed = targets.map(|targets| structure.ancestors(targets));
let mut values = Vec::with_capacity(structure.len());
for (index, (op, links)) in structure
.ops
.iter()
.zip(structure.operands.iter())
.enumerate()
{
let skipped = matches!(&computed, Some(wanted) if !wanted[index]);
let value = if skipped {
let shape = structure
.shapes
.get(index)
.expect("shapes cover the network")
.clone();
Tensor::counted(shape, 0)
} else {
let operands: SmallVec<[&Tensor<E>; 2]> = links
.as_slice()
.iter()
.map(|link| &values[link.index()])
.collect();
let value = op.forward(&operands, parameters.payloads(), inputs.payloads());
debug_assert_eq!(
value.shape(),
*structure
.shapes
.get(index)
.expect("shapes cover the network"),
"operation output shape disagrees with the recorded shape at node {index}"
);
value
};
values.push(value);
}
let provenance = match computed {
Some(computed) => Provenance::Sliced { computed },
None => Provenance::Complete,
};
Run::new(
structure.clone(),
self.origin(),
values,
provenance,
numerics,
)
}
}