use std::collections::HashMap;
use std::error::Error;
use std::fmt;
use std::sync::Arc;
use crate::{ADKey, ADRuleError, ADRuleKind, ADRuleResult, Primitive};
use computegraph::graph::{Graph, GraphBuilder};
use computegraph::resolve::resolve;
use computegraph::{GraphOperation, OperationRole, ValueKey, ValueRef};
use crate::LinearizedGraph;
use super::trace::{Trace, TraceEdge, TraceNode};
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum EagerRecordError {
CountMismatch {
field: &'static str,
expected: usize,
actual: usize,
},
KeyMismatch {
field: &'static str,
index: usize,
},
TooManyOutputs {
actual: usize,
max: usize,
},
}
impl EagerRecordError {
pub(crate) fn count_mismatch(field: &'static str, expected: usize, actual: usize) -> Self {
Self::CountMismatch {
field,
expected,
actual,
}
}
pub(crate) fn key_mismatch(field: &'static str, index: usize) -> Self {
Self::KeyMismatch { field, index }
}
fn too_many_outputs(actual: usize) -> Self {
Self::TooManyOutputs {
actual,
max: u8::MAX as usize + 1,
}
}
}
impl fmt::Display for EagerRecordError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::CountMismatch {
field,
expected,
actual,
} => write!(f, "{field} expected {expected} entries, got {actual}"),
Self::KeyMismatch { field, index } => {
write!(f, "{field} does not match graph metadata at slot {index}")
}
Self::TooManyOutputs { actual, max } => write!(
f,
"eager recording supports at most {max} outputs, got {actual}"
),
}
}
}
impl Error for EagerRecordError {}
pub type EagerRecordResult<T> = Result<T, EagerRecordError>;
pub struct RecordedGraph<Op: GraphOperation> {
graph: Arc<Graph<Op>>,
input_keys: Vec<Op::InputKey>,
output_keys: Vec<ValueKey<Op>>,
}
impl<Op: GraphOperation> RecordedGraph<Op> {
pub fn new(
graph: Arc<Graph<Op>>,
input_keys: Vec<Op::InputKey>,
output_keys: Vec<ValueKey<Op>>,
) -> EagerRecordResult<Self> {
if graph.inputs().len() != input_keys.len() {
return Err(EagerRecordError::count_mismatch(
"RecordedGraph input keys",
graph.inputs().len(),
input_keys.len(),
));
}
if graph.outputs().len() != output_keys.len() {
return Err(EagerRecordError::count_mismatch(
"RecordedGraph output keys",
graph.outputs().len(),
output_keys.len(),
));
}
for (index, (&input_id, input_key)) in
graph.inputs().iter().zip(input_keys.iter()).enumerate()
{
if graph.values()[input_id].key != ValueKey::Input(input_key.clone()) {
return Err(EagerRecordError::key_mismatch(
"RecordedGraph input keys",
index,
));
}
}
for (index, (&output_id, output_key)) in
graph.outputs().iter().zip(output_keys.iter()).enumerate()
{
if &graph.values()[output_id].key != output_key {
return Err(EagerRecordError::key_mismatch(
"RecordedGraph output keys",
index,
));
}
}
Ok(Self {
graph,
input_keys,
output_keys,
})
}
pub fn from_primitive(op: Op, input_keys: Vec<Op::InputKey>) -> EagerRecordResult<Self> {
let mut builder = GraphBuilder::new();
let input_ids: Vec<_> = input_keys
.iter()
.cloned()
.map(|key| builder.add_input(key))
.collect();
let output_ids = builder.add_operation(
op,
input_ids
.iter()
.map(|local_id| ValueRef::Local(*local_id))
.collect(),
OperationRole::Primary,
);
builder.set_outputs(output_ids.clone());
let graph = Arc::new(builder.build());
let output_keys = output_ids
.iter()
.map(|output_id| graph.values()[*output_id].key.clone())
.collect();
Self::new(graph, input_keys, output_keys)
}
pub fn as_graph(&self) -> &Graph<Op> {
&self.graph
}
pub fn input_keys(&self) -> &[Op::InputKey] {
&self.input_keys
}
pub fn output_keys(&self) -> &[ValueKey<Op>] {
&self.output_keys
}
}
impl<Op: Primitive> RecordedGraph<Op>
where
Op::InputKey: ADKey,
{
pub(crate) fn linearize(
&self,
output_slots: &[usize],
ctx: &mut Op::ADContext,
) -> ADRuleResult<LinearizedGraph<Op>> {
let mut selected_outputs = Vec::with_capacity(output_slots.len());
for &slot in output_slots {
let Some(output_key) = self.output_keys.get(slot).cloned() else {
return Err(ADRuleError::invalid_input(
"tidu::eager::RecordedGraph",
ADRuleKind::Jvp,
format!(
"requested output slot {slot}, but graph has {} outputs",
self.output_keys.len()
),
));
};
selected_outputs.push(output_key);
}
let view = resolve(vec![Arc::clone(&self.graph)]);
let aliases = HashMap::new();
crate::linearize(&view, &selected_outputs, &self.input_keys, 0, ctx, &aliases)
}
}
pub struct EagerInput<Op: GraphOperation> {
pub key: ValueKey<Op>,
pub trace: Option<Trace<Op>>,
pub requires_grad: bool,
pub data: Arc<Op::Operand>,
}
pub struct EagerOutput<Op: GraphOperation> {
pub key: ValueKey<Op>,
pub trace: Option<Trace<Op>>,
pub requires_grad: bool,
pub output_slot: usize,
}
pub trait KeySource<Op: GraphOperation> {
fn fresh_input_key(&mut self) -> Op::InputKey;
}
pub struct Recorder<K> {
key_source: K,
}
impl<K> Recorder<K> {
pub fn new(key_source: K) -> Self {
Self { key_source }
}
pub fn key_source_mut(&mut self) -> &mut K {
&mut self.key_source
}
pub fn into_key_source(self) -> K {
self.key_source
}
pub fn fresh_input_keys<Op>(&mut self, count: usize) -> Vec<Op::InputKey>
where
Op: GraphOperation,
K: KeySource<Op>,
{
(0..count)
.map(|_| self.key_source.fresh_input_key())
.collect()
}
pub fn record_graph<Op>(
&mut self,
graph: RecordedGraph<Op>,
inputs: &[EagerInput<Op>],
outputs: &[Arc<Op::Operand>],
retained_values: HashMap<ValueKey<Op>, Arc<Op::Operand>>,
) -> EagerRecordResult<Vec<EagerOutput<Op>>>
where
Op: Primitive,
Op::InputKey: ADKey,
K: KeySource<Op>,
{
if inputs.len() != graph.input_keys().len() {
return Err(EagerRecordError::count_mismatch(
"Recorder::record_graph inputs",
graph.input_keys().len(),
inputs.len(),
));
}
if outputs.len() != graph.output_keys().len() {
return Err(EagerRecordError::count_mismatch(
"Recorder::record_graph outputs",
graph.output_keys().len(),
outputs.len(),
));
}
if outputs.len() > u8::MAX as usize + 1 {
return Err(EagerRecordError::too_many_outputs(outputs.len()));
}
let output_keys = fresh_value_keys(&mut self.key_source, outputs.len());
let requires_grad = inputs.iter().any(|input| input.requires_grad);
let trace = if requires_grad {
let saved_data = saved_graph_values(&graph, inputs, &retained_values);
Some(Trace::new(Arc::new(TraceNode::new(
graph,
output_keys.clone(),
saved_data,
inputs
.iter()
.map(|input| {
TraceEdge::new(
input.trace.as_ref().map(|trace| trace.node().clone()),
input.key.clone(),
input.requires_grad,
)
})
.collect(),
)?)))
} else {
None
};
Ok(output_keys
.into_iter()
.enumerate()
.map(|(output_slot, key)| EagerOutput {
key,
trace: trace.clone(),
requires_grad,
output_slot,
})
.collect())
}
}
fn saved_graph_values<Op: GraphOperation>(
graph: &RecordedGraph<Op>,
inputs: &[EagerInput<Op>],
retained_values: &HashMap<ValueKey<Op>, Arc<Op::Operand>>,
) -> HashMap<ValueKey<Op>, Arc<Op::Operand>> {
let mut saved = HashMap::with_capacity(inputs.len() + retained_values.len());
for (input_key, input) in graph.input_keys().iter().zip(inputs.iter()) {
saved.insert(ValueKey::Input(input_key.clone()), input.data.clone());
}
for (key, value) in retained_values {
saved.insert(key.clone(), value.clone());
}
saved
}
fn fresh_value_keys<Op: GraphOperation>(
key_source: &mut impl KeySource<Op>,
count: usize,
) -> Vec<ValueKey<Op>> {
(0..count)
.map(|_| ValueKey::Input(key_source.fresh_input_key()))
.collect()
}