use crate::core::dtype::DType;
use crate::core::op::Op;
use std::collections::HashMap;
pub type TensorId = String;
pub type Attrs = HashMap<String, AttrValue>;
#[derive(Clone, Debug)]
pub enum AttrValue {
Int(i64),
Float(f32),
String(String),
Ints(Vec<i64>),
Floats(Vec<f32>),
Bool(bool),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum BackendHint {
Cpu,
WgpuRs,
Honeycrisp,
Nox,
}
#[derive(Clone, Debug, PartialEq)]
pub enum Dim {
Fixed(usize),
Dynamic(String),
}
pub type Shape = Vec<Dim>;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Residency {
Resident,
Cached,
Streamed,
}
#[derive(Clone, Debug)]
pub struct BufferLifetime {
pub produced_at: usize,
pub consumed_at: usize,
pub size_bytes: usize,
}
#[derive(Clone, Debug)]
pub struct MemoryPlan {
pub lifetimes: HashMap<TensorId, BufferLifetime>,
pub peak_bytes: usize,
}
pub struct Graph {
pub nodes: Vec<Node>,
pub tensors: HashMap<TensorId, TensorMeta>,
pub weights: HashMap<String, WeightData>,
}
pub struct Node {
pub id: usize,
pub op: Op,
pub inputs: Vec<TensorId>,
pub outputs: Vec<TensorId>,
pub attrs: Attrs,
pub backend_hint: Option<BackendHint>,
}
#[derive(Clone, Debug)]
pub struct TensorMeta {
pub shape: Shape,
pub dtype: DType,
pub residency: Residency,
}
pub struct WeightData {
pub data: Vec<u8>,
pub shape: Vec<usize>,
pub dtype: DType,
pub needs_transpose: bool,
}
impl TensorMeta {
pub fn fixed(shape: Vec<usize>, dtype: DType) -> Self {
Self {
shape: shape.into_iter().map(Dim::Fixed).collect(),
dtype,
residency: Residency::Streamed,
}
}
pub fn weight(shape: Vec<usize>, dtype: DType) -> Self {
Self {
shape: shape.into_iter().map(Dim::Fixed).collect(),
dtype,
residency: Residency::Resident,
}
}
pub fn fixed_shape(&self) -> Option<Vec<usize>> {
self.shape
.iter()
.map(|d| match d {
Dim::Fixed(n) => Some(*n),
Dim::Dynamic(_) => None,
})
.collect()
}
pub fn fixed_bytes(&self) -> Option<usize> {
self.fixed_shape()
.map(|s| self.dtype.bytes_for(s.iter().product::<usize>()))
}
}
impl Graph {
pub fn new() -> Self {
Self {
nodes: Vec::new(),
tensors: HashMap::new(),
weights: HashMap::new(),
}
}
pub fn add_node(&mut self, op: Op, inputs: Vec<TensorId>, outputs: Vec<TensorId>) -> usize {
let id = self.nodes.len();
self.nodes.push(Node {
id,
op,
inputs,
outputs,
attrs: HashMap::new(),
backend_hint: None,
});
id
}
pub fn add_node_with_attrs(
&mut self,
op: Op,
inputs: Vec<TensorId>,
outputs: Vec<TensorId>,
attrs: Attrs,
backend_hint: Option<BackendHint>,
) -> usize {
let id = self.nodes.len();
self.nodes.push(Node {
id,
op,
inputs,
outputs,
attrs,
backend_hint,
});
id
}
pub fn add_tensor(&mut self, name: TensorId, meta: TensorMeta) {
self.tensors.insert(name, meta);
}
pub fn add_weight(&mut self, name: String, data: WeightData) {
self.weights.insert(name, data);
}
pub fn get_weight(&self, name: &str) -> Option<&WeightData> {
self.weights.get(name)
}
pub fn len(&self) -> usize {
self.nodes.len()
}
pub fn is_empty(&self) -> bool {
self.nodes.is_empty()
}
pub fn stateful_ops(&self) -> Vec<usize> {
self.nodes
.iter()
.enumerate()
.filter(|(_, n)| n.op.is_stateful())
.map(|(i, _)| i)
.collect()
}
pub fn build_consumers(&self) -> HashMap<TensorId, Vec<usize>> {
let mut map: HashMap<TensorId, Vec<usize>> = HashMap::new();
for (i, node) in self.nodes.iter().enumerate() {
for input in &node.inputs {
map.entry(input.clone()).or_default().push(i);
}
}
map
}
pub fn eliminate_dead_nodes(&mut self) -> usize {
let mut consumed: std::collections::HashSet<String> = self
.nodes
.iter()
.flat_map(|n| n.inputs.iter().cloned())
.collect();
let mut alive = vec![true; self.nodes.len()];
let mut removed = 0;
for i in (0..self.nodes.len()).rev() {
let all_outputs_dead = self.nodes[i]
.outputs
.iter()
.all(|o| !consumed.contains(o));
if all_outputs_dead
&& !self.nodes[i].op.is_stateful()
&& !self.nodes[i].outputs.is_empty()
{
alive[i] = false;
removed += 1;
for input in &self.nodes[i].inputs {
let still_consumed = self.nodes.iter().enumerate().any(|(j, n)| {
j != i && alive[j] && n.inputs.contains(input)
});
if !still_consumed {
consumed.remove(input);
}
}
}
}
if removed > 0 {
let mut new_nodes: Vec<Node> = Vec::new();
for (i, node) in self.nodes.drain(..).enumerate() {
if alive[i] {
new_nodes.push(node);
}
}
for (new_id, node) in new_nodes.iter_mut().enumerate() {
node.id = new_id;
}
self.nodes = new_nodes;
log::info!("graph dead-node elimination: removed {removed} nodes");
}
removed
}
pub fn topological_sort(&mut self) -> bool {
let n = self.nodes.len();
if n == 0 {
return true;
}
let mut producer: HashMap<String, usize> = HashMap::new();
for (i, node) in self.nodes.iter().enumerate() {
for output in &node.outputs {
producer.insert(output.clone(), i);
}
}
let mut in_degree = vec![0usize; n];
let mut dependents: Vec<Vec<usize>> = vec![Vec::new(); n];
for (i, node) in self.nodes.iter().enumerate() {
for input in &node.inputs {
if let Some(&prod) = producer.get(input) {
if prod != i {
dependents[prod].push(i);
in_degree[i] += 1;
}
}
}
}
let mut queue: std::collections::VecDeque<usize> = std::collections::VecDeque::new();
for i in 0..n {
if in_degree[i] == 0 {
queue.push_back(i);
}
}
let mut order: Vec<usize> = Vec::with_capacity(n);
while let Some(idx) = queue.pop_front() {
order.push(idx);
for &dep in &dependents[idx] {
in_degree[dep] -= 1;
if in_degree[dep] == 0 {
queue.push_back(dep);
}
}
}
if order.len() != n {
log::error!("graph topological sort: cycle detected");
return false;
}
let mut slots: Vec<Option<Node>> = self.nodes.drain(..).map(Some).collect();
for &idx in &order {
if let Some(node) = slots[idx].take() {
self.nodes.push(node);
}
}
for (new_id, node) in self.nodes.iter_mut().enumerate() {
node.id = new_id;
}
true
}
pub fn memory_plan(&self) -> MemoryPlan {
let mut lifetimes: HashMap<TensorId, BufferLifetime> = HashMap::new();
for (i, node) in self.nodes.iter().enumerate() {
for output in &node.outputs {
if let Some(meta) = self.tensors.get(output) {
if let Some(bytes) = meta.fixed_bytes() {
lifetimes.insert(
output.clone(),
BufferLifetime {
produced_at: i,
consumed_at: i,
size_bytes: bytes,
},
);
}
}
}
}
for (i, node) in self.nodes.iter().enumerate() {
for input in &node.inputs {
if let Some(lt) = lifetimes.get_mut(input) {
if i > lt.consumed_at {
lt.consumed_at = i;
}
}
}
}
let mut peak_bytes = 0usize;
for i in 0..self.nodes.len() {
let alive_bytes: usize = lifetimes
.values()
.filter(|lt| lt.produced_at <= i && lt.consumed_at >= i)
.map(|lt| lt.size_bytes)
.sum();
if alive_bytes > peak_bytes {
peak_bytes = alive_bytes;
}
}
peak_bytes += self.weights.values().map(|w| w.data.len()).sum::<usize>();
MemoryPlan {
lifetimes,
peak_bytes,
}
}
pub fn summary(&self) -> String {
let mut op_counts: HashMap<&str, usize> = HashMap::new();
for node in &self.nodes {
*op_counts.entry(node.op.name()).or_insert(0) += 1;
}
let mut counts: Vec<_> = op_counts.into_iter().collect();
counts.sort_by(|a, b| b.1.cmp(&a.1));
let mut s = format!(
"Graph: {} nodes, {} weights\n",
self.nodes.len(),
self.weights.len()
);
for (op, count) in counts {
s.push_str(&format!(" {op}: {count}\n"));
}
let stateful = self.stateful_ops();
if !stateful.is_empty() {
s.push_str(&format!(
" Stateful ops: {} (KV cache nodes)\n",
stateful.len()
));
}
s
}
}
impl Default for Graph {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_graph_sorts_and_plans() {
let mut g = Graph::new();
assert!(g.topological_sort());
let plan = g.memory_plan();
assert_eq!(plan.peak_bytes, 0);
}
#[test]
fn toposort_reorders_dependency() {
let mut g = Graph::new();
g.add_node(Op::Add, vec!["x".into(), "y".into()], vec!["z".into()]);
g.add_node(Op::Mul, vec![], vec!["x".into()]);
g.add_node(Op::Sub, vec![], vec!["y".into()]);
assert!(g.topological_sort());
let z_idx = g.nodes.iter().position(|n| n.outputs.contains(&"z".to_string())).unwrap();
let x_idx = g.nodes.iter().position(|n| n.outputs.contains(&"x".to_string())).unwrap();
let y_idx = g.nodes.iter().position(|n| n.outputs.contains(&"y".to_string())).unwrap();
assert!(x_idx < z_idx);
assert!(y_idx < z_idx);
}
#[test]
fn dead_node_elimination() {
let mut g = Graph::new();
g.add_node(Op::Mul, vec![], vec!["used".into()]);
g.add_node(Op::Mul, vec![], vec!["dead".into()]);
g.add_node(Op::Add, vec!["used".into()], vec!["out".into()]);
let removed = g.eliminate_dead_nodes();
assert!(removed >= 1);
}
#[test]
fn build_consumers_indexes_inputs() {
let mut g = Graph::new();
g.add_node(Op::Mul, vec![], vec!["x".into()]);
g.add_node(Op::Add, vec!["x".into()], vec!["y".into()]);
g.add_node(Op::Sub, vec!["x".into()], vec!["z".into()]);
let consumers = g.build_consumers();
assert_eq!(consumers.get("x").map(|v| v.len()), Some(2));
}
}