use crate::{OperationIr, ScalarIr, TensorId, TensorStatus};
use alloc::vec::Vec;
use burn_backend::Slice;
use hashbrown::HashSet;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct GraphIr {
pub operations: Vec<OperationIr>,
pub inputs: Vec<TensorId>,
pub outputs: Vec<TensorId>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GraphBoundary {
pub inputs: Vec<TensorId>,
pub outputs: Vec<TensorId>,
}
impl GraphIr {
pub fn new(operations: Vec<OperationIr>) -> Self {
let boundary = Self::classify(&operations);
Self {
operations,
inputs: boundary.inputs,
outputs: boundary.outputs,
}
}
pub fn classify(operations: &[OperationIr]) -> GraphBoundary {
let mut referenced = Vec::new();
let mut referenced_set = HashSet::new();
let mut produced = Vec::new();
let mut produced_set = HashSet::new();
let mut consumed = HashSet::new();
for operation in operations {
if let OperationIr::Drop(tensor) = operation {
consumed.insert(tensor.id);
}
if !matches!(operation, OperationIr::Init(_)) {
for tensor in operation.outputs() {
if produced_set.insert(tensor.id) {
produced.push(tensor.id);
}
}
}
for tensor in operation.nodes() {
if referenced_set.insert(tensor.id) {
referenced.push(tensor.id);
}
if tensor.status == TensorStatus::ReadWrite {
consumed.insert(tensor.id);
}
}
}
let inputs = referenced
.into_iter()
.filter(|id| !produced_set.contains(id))
.collect();
let outputs = produced
.into_iter()
.filter(|id| !consumed.contains(id))
.collect();
GraphBoundary { inputs, outputs }
}
pub fn len(&self) -> usize {
self.operations.len()
}
pub fn is_empty(&self) -> bool {
self.operations.is_empty()
}
}
impl From<Vec<OperationIr>> for GraphIr {
fn from(operations: Vec<OperationIr>) -> Self {
Self::new(operations)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct GraphId(pub u64);
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GraphBindings {
pub tensors: Vec<(TensorId, TensorId)>,
pub shapes: Vec<usize>,
pub scalars: Vec<ScalarIr>,
pub ranges: Vec<Slice>,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{CustomOpIr, TensorIr};
use burn_backend::{DType, Shape};
fn tensor(id: u64) -> TensorIr {
TensorIr::uninit(TensorId::new(id), Shape::new([1]), DType::F32)
}
#[test]
fn graph_boundary_is_in_first_use_order() {
let first = OperationIr::Custom(CustomOpIr::new(
"first",
&[tensor(9), tensor(3)],
&[tensor(5)],
));
let second = OperationIr::Custom(CustomOpIr::new("second", &[tensor(5)], &[tensor(7)]));
let graph = GraphIr::new(vec![first, second, OperationIr::Drop(tensor(5))]);
assert_eq!(graph.inputs, vec![TensorId::new(9), TensorId::new(3)]);
assert_eq!(graph.outputs, vec![TensorId::new(7)]);
}
#[test]
fn graph_normalization_is_deterministic() {
let operations = vec![OperationIr::Custom(CustomOpIr::new(
"op",
&[tensor(4), tensor(2), tensor(4)],
&[tensor(8)],
))];
assert_eq!(GraphIr::new(operations.clone()), GraphIr::new(operations));
}
#[test]
fn borrowed_classification_matches_graph_construction() {
let operations = vec![
OperationIr::Custom(CustomOpIr::new(
"first",
&[tensor(4), tensor(2)],
&[tensor(8)],
)),
OperationIr::Custom(CustomOpIr::new("second", &[tensor(8)], &[tensor(9)])),
];
let boundary = GraphIr::classify(&operations);
let graph = GraphIr::new(operations);
assert_eq!(boundary.inputs, graph.inputs);
assert_eq!(boundary.outputs, graph.outputs);
}
}