use std::collections::BTreeSet;
use crate::{
DType, Map, RT, Set, Tensor, ZyxError,
backend::Buffer,
dtype::Constant,
graph::{Graph, GraphId},
kernel::OpId,
runtime::{Runtime, TensorData},
shape::Dim,
slab::SlabId,
symbolic::Expr,
tensor::TensorId,
};
#[cfg_attr(feature = "py", pyo3::pyclass)]
pub struct Tape {
graph_id: GraphId,
}
impl Tape {
pub fn new<'a>(params: impl IntoIterator<Item = &'a Tensor>) -> Result<Tape, ZyxError> {
let mut rt = RT.lock();
let graph_id = rt.graphs.push(Graph::new());
for p in params {
rt.promote_to_graph(p.id, graph_id)?;
}
Ok(Tape { graph_id })
}
pub fn empty() -> Tape {
Self::new(std::iter::empty()).unwrap()
}
pub fn add(&self, tensor: &Tensor) -> Result<(), ZyxError> {
let mut rt = RT.lock();
rt.promote_to_graph(tensor.id, self.graph_id)?;
Ok(())
}
pub fn extend<'a>(&self, params: impl IntoIterator<Item = &'a Tensor>) -> Result<(), ZyxError> {
let mut rt = RT.lock();
for p in params {
rt.promote_to_graph(p.id, self.graph_id)?;
}
Ok(())
}
}
impl Tape {
#[must_use]
pub fn gradient<'a>(&self, target: &Tensor, sources: impl IntoIterator<Item = &'a Tensor>) -> Vec<Tensor> {
let sources: Vec<TensorId> = sources.into_iter().map(Tensor::id).collect();
let mut rt = RT.lock();
let grads: Map<TensorId, TensorId> = rt.gradient(target.id(), sources.iter().copied().collect(), self.graph_id);
sources
.into_iter()
.map(|x: TensorId| {
let id = match grads.get(&x) {
Some(&id) => id,
None => {
let shape = rt.resolve_shape(x);
let dtype = rt.dtype(x);
let ids: Vec<TensorId> = shape.iter().map(|&d| rt.new_constant_tensor(Constant::idx(d))).collect();
let stid = if ids.is_empty() {
TensorId::NULL
} else {
let s = rt.stack(&ids).unwrap();
for id in &ids {
rt.release(*id);
}
s
};
rt.new_full(stid, dtype.zero_constant())
}
};
Tensor { id }
})
.collect()
}
pub fn realize<'a>(self, tensors: impl IntoIterator<Item = &'a Tensor>) -> Result<(), ZyxError> {
let mut rt = RT.lock();
let graph_id = self.graph_id;
let output_pairs: Vec<(TensorId, OpId)> = tensors
.into_iter()
.map(|t| {
let class_id = match rt.tensors[t.id] {
TensorData::Graph { class_id, .. }
| TensorData::GraphLeaf { class_id, .. }
| TensorData::Promoted { class_id, .. } => class_id,
TensorData::Eager { .. }
| TensorData::Leaf { .. }
| TensorData::PendingLeaf { .. }
| TensorData::Symbolic { .. } => panic!(
"Tape::realize was given a tensor that never entered the tape's graph \
(tid {}, data {:?}).\n\
This is a caller mistake, not a zyx bug: the tensor is eager — it was \
created outside the tape scope, or built entirely from eager inputs, so \
there is no graph class for realize to materialize.\n\
How to fix: realize only tensors whose computation this tape traced. \
Promote tensors you build from with `tape.add(&t)?` before the ops, or \
give the chain at least one promoted operand — ops mixing an eager tensor \
with a graph tensor are pulled into the graph automatically; an all-eager \
chain stays eager.",
t.id(),
rt.tensors[t.id]
),
};
(t.id, class_id)
})
.collect();
let output_tids: Vec<TensorId> = output_pairs.iter().map(|(tid, _)| *tid).collect();
let output_classes: Vec<OpId> = output_pairs.iter().map(|(_, cid)| *cid).collect();
debug_assert!(rt.graphs.contains_id(graph_id));
rt.debug_assert_pre_realize(graph_id);
let output_set: BTreeSet<OpId> = output_classes.iter().copied().collect();
let cache_key = rt.plan_cache_key(graph_id, &output_set);
if let Some(plan) = rt.plan_cache.get(&cache_key) {
let mut class_buf: Map<OpId, Buffer> = Map::default();
let mut class_vars: Map<OpId, Constant> = Map::default();
for &cid in &plan.leaf_classes {
let &tid = rt.graphs[graph_id].leaf_map.get(&cid).unwrap();
if let Some(buf_id) = rt.leaf_buffer(tid) {
class_buf.insert(cid, buf_id);
} else {
let value = rt.resolve_symbolic(tid).expect("leaf class tid resolves neither to a buffer nor a variable");
class_vars.insert(cid, value);
}
}
rt.execute_plan(cache_key, &mut class_buf, &class_vars)?;
let mut handed_out: BTreeSet<Buffer> = BTreeSet::new();
for (_, &buf) in output_classes.iter().map(|cid| (cid, &class_buf[cid])) {
if !handed_out.insert(buf) {
buf.pool.retain(buf.buffer_id);
}
}
for (&tid, &cid) in output_tids.iter().zip(output_classes.iter()) {
rt.eagerify(tid, class_buf[&cid]);
}
rt.debug_assert_no_stray_buffers(graph_id, &output_tids);
return Ok(());
}
let plan = rt.compile_graph(graph_id, &output_set)?;
let mut class_buf: Map<OpId, Buffer> = Map::default();
let mut class_vars: Map<OpId, Constant> = Map::default();
for &cid in &plan.leaf_classes {
let &tid = rt.graphs[graph_id].leaf_map.get(&cid).unwrap();
if let Some(buf_id) = rt.leaf_buffer(tid) {
class_buf.insert(cid, buf_id);
} else {
let value = rt.resolve_symbolic(tid).expect("leaf class tid resolves neither to a buffer nor a variable");
class_vars.insert(cid, value);
}
}
rt.plan_cache.insert(cache_key, plan);
rt.execute_plan(cache_key, &mut class_buf, &class_vars)?;
let mut handed_out: BTreeSet<Buffer> = BTreeSet::new();
for &buf in output_classes.iter().map(|cid| &class_buf[cid]) {
if !handed_out.insert(buf) {
buf.pool.retain(buf.buffer_id);
}
}
for (&tid, &cid) in output_tids.iter().zip(output_classes.iter()) {
rt.eagerify(tid, class_buf[&cid]);
}
rt.debug_assert_no_stray_buffers(graph_id, &output_tids);
Ok(())
}
}
impl Drop for Tape {
fn drop(&mut self) {
let mut rt = RT.lock();
let graph_id = self.graph_id;
let leafs: Vec<TensorId> = rt.graphs[graph_id].leaf_map.values().copied().collect();
let eager_leafs: Set<TensorId> =
leafs.iter().copied().filter(|&tid| matches!(rt.tensors[tid], TensorData::Eager { .. })).collect();
let affiliated: Vec<TensorId> = rt
.tensors
.iter()
.filter_map(|(tid, td)| match td {
TensorData::Graph { graph_id: g, .. } | TensorData::Promoted { graph_id: g, .. } if *g == graph_id => {
if leafs.contains(&tid) { None } else { Some(tid) }
}
_ => None,
})
.collect();
for &tid in affiliated.iter().chain(&leafs) {
if !rt.tensors.contains_id(tid) {
continue;
}
match rt.tensors[tid] {
TensorData::Promoted { rc, kernel_id, .. } => {
if rc > 0 && !kernel_id.is_null() {
let n = rt.kernels[kernel_id].loads.iter().filter(|&&t| t == tid).count() as u16;
let disowned = n > 0 && rc == n && !rt.kernels[kernel_id].outputs.contains(&tid);
if disowned {
rt.release(tid);
} else {
rt.eagerify(tid, Buffer::NULL);
}
}
}
TensorData::GraphLeaf { buffer: buffer_id, rc, .. } => {
if rc > 0 {
rt.graphs[graph_id].ref_count -= 1;
match &mut rt.tensors[tid] {
TensorData::GraphLeaf { graph_id, class_id, .. } => {
*graph_id = GraphId::NULL;
*class_id = OpId::NULL;
}
_ => unreachable!(),
}
let (shape_id, dtype, rc) = match rt.tensors[tid] {
TensorData::GraphLeaf { shape_id, dtype, rc, .. } => (shape_id, dtype, rc),
_ => unreachable!(),
};
rt.tensors[tid] = TensorData::Leaf { shape_id, dtype, buffer: buffer_id, rc };
}
}
TensorData::Graph { rc, .. } => {
if rc > 0 {
rt.graphs[graph_id].ref_count -= 1;
match &mut rt.tensors[tid] {
TensorData::Graph { graph_id, class_id, .. } => {
*graph_id = GraphId::NULL;
*class_id = OpId::NULL;
}
_ => unreachable!(),
}
}
}
TensorData::Eager { .. } => {
if !eager_leafs.contains(&tid) {
rt.graphs[graph_id].ref_count -= 1;
}
}
TensorData::Symbolic { expr, .. } => {
debug_assert!(
matches!(rt.exprs[expr], Expr::Variable { .. }),
"affiliated symbolic tensor is not a variable: {:?}",
rt.exprs[expr]
);
rt.graphs[graph_id].ref_count -= 1;
}
TensorData::Leaf { .. } => {
}
ref t => unreachable!("affiliated tensor changed variant: {t:?}"),
};
}
for tid in leafs {
if rt.tensors.contains_id(tid) {
rt.release(tid);
}
}
for tid in affiliated {
if !rt.tensors.contains_id(tid) {
continue;
}
let rc = match rt.tensors[tid] {
TensorData::Promoted { rc, .. } | TensorData::Graph { rc, .. } => rc,
TensorData::Eager { .. } => continue,
_ => panic!("affiliated wrong"),
};
if rc == 0 {
panic!("How is this possible?");
}
}
rt.assert_graph_inventory(graph_id);
rt.graphs[graph_id].mark_dead();
if rt.graphs[graph_id].ref_count == 0 {
rt.remove_dead_graph(graph_id);
}
}
}
impl Tape {
pub fn freeze<'a>(self, outputs: impl IntoIterator<Item = &'a Tensor>) -> Result<FrozenTape, ZyxError> {
let mut rt = RT.lock();
let graph_id = self.graph_id;
let outputs: Vec<(OpId, Vec<Dim>, DType)> = outputs
.into_iter()
.map(|t| {
let class_id = match rt.tensors[t.id] {
TensorData::Graph { class_id, .. } | TensorData::Promoted { class_id, .. } => class_id,
ref td => panic!("non-graph tensor in freeze: tid {t} data {td:?}"),
};
(class_id, rt.resolve_shape(t.id), rt.dtype(t.id))
})
.collect();
debug_assert!(rt.graphs.contains_id(graph_id));
rt.debug_assert_pre_realize(graph_id);
let output_set: BTreeSet<OpId> = outputs.iter().map(|x| x.0).collect();
let cache_key = rt.plan_cache_key(graph_id, &output_set);
if rt.plan_cache.contains_key(&cache_key) {
return Ok(FrozenTape { cache_key, outputs });
}
let plan = rt.compile_graph(graph_id, &output_set)?;
rt.plan_cache.insert(cache_key, plan);
Ok(FrozenTape { cache_key, outputs })
}
}
#[cfg_attr(feature = "py", pyo3::pyclass)]
pub struct FrozenTape {
cache_key: u64,
outputs: Vec<(OpId, Vec<Dim>, DType)>,
}
impl FrozenTape {
pub fn replay<'a>(&self, inputs: impl IntoIterator<Item = &'a Tensor>) -> Result<Vec<Tensor>, ZyxError> {
let mut rt = RT.lock();
let mut class_buf: Map<OpId, Buffer> = Map::default();
let mut class_vars: Map<OpId, Constant> = Map::default();
for (tensor, &cid) in inputs.into_iter().zip(rt.plan_cache[&self.cache_key].leaf_classes.iter()) {
if let Some(buf_id) = rt.leaf_buffer(tensor.id) {
let expected = rt.plan_cache[&self.cache_key].leaf_pools.get(&cid).copied();
if expected != Some(buf_id.pool) {
return Err(ZyxError::frozen_plan_stale(
format!(
"frozen tape replayed with leaf class {cid:?} in pool {:?}, but the frozen plan compiled it in pool {:?} — bindings changed since freeze, re-freeze the tape",
buf_id.pool,
expected
)
.into(),
));
}
class_buf.insert(cid, buf_id);
} else {
let value = rt.resolve_symbolic(tensor.id).expect("replay input resolves neither to a buffer nor a variable");
class_vars.insert(cid, value);
}
}
rt.execute_plan(self.cache_key, &mut class_buf, &class_vars)?;
let mut outputs = Vec::new();
for (cid, shape, dtype) in self.outputs.iter() {
let ids: Vec<TensorId> = shape.iter().map(|&d| rt.new_constant_tensor(Constant::idx(d))).collect();
let stid = if ids.is_empty() {
TensorId::NULL
} else {
let s = rt.stack(&ids).unwrap();
for id in &ids {
rt.release(*id);
}
s
};
let tid = rt.new_eager_tensor(stid, *dtype, class_buf[cid]);
outputs.push(Tensor::from_id(tid));
}
Ok(outputs)
}
}
impl Runtime {
fn debug_assert_no_stray_buffers(&self, graph_id: GraphId, outputs: &[TensorId]) {
if cfg!(debug_assertions) {
let output_set: Set<TensorId> = outputs.iter().copied().collect();
for (tid, td) in self.tensors.iter() {
let (affiliated, class_id) = match td {
TensorData::Graph { class_id: c, graph_id: g, .. }
| TensorData::Promoted { class_id: c, graph_id: g, .. } => (*g == graph_id, *c),
_ => continue,
};
if affiliated
&& !output_set.contains(&tid)
&& !self.graphs[graph_id].is_leaf(class_id)
&& !self.graphs[graph_id].is_after(class_id)
{
debug_assert!(
self.leaf_buffer(tid).is_none(),
"non-leaf, non-output graph tensor {tid} realized after execute_plan"
);
}
}
}
}
}