use crate::error::{PolygraphError, Result}; use indexmap::IndexMap; use rustc_hash::FxHasher; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::hash::{Hash, Hasher}; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct NodeId(pub u32); #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub enum DataType { F32, F64, I32, I64, C64, C128, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct Shape { dims: Vec, } impl Shape { pub fn new(dims: Vec) -> Self { Self { dims } } pub fn dims(&self) -> &[usize] { &self.dims } pub fn ndim(&self) -> usize { self.dims.len() } pub fn size(&self) -> usize { self.dims.iter().product() } } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub enum SparseFormat { CSR, CSC, COO, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub enum AggregationType { Sum, Mean, Max, Min, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub enum IRNodeType { MatMul { transpose_a: bool, transpose_b: bool, }, SparseMatMul { format: SparseFormat, }, GraphConvolution { aggregation: AggregationType, }, FFT { inverse: bool, axes: Vec, }, ElementwiseMul, Add, ReLU, Conditional { condition_id: NodeId, }, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct IRNode { id: NodeId, node_type: IRNodeType, inputs: Vec, output_dtypes: Vec, output_shapes: Vec, } impl IRNode { pub fn new( id: NodeId, node_type: IRNodeType, inputs: Vec, output_dtypes: Vec, output_shapes: Vec, ) -> Self { Self { id, node_type, inputs, output_dtypes, output_shapes, } } pub fn try_new( id: NodeId, node_type: IRNodeType, inputs: Vec, output_dtypes: Vec, output_shapes: Vec, ) -> Result { // Validate node configuration match &node_type { IRNodeType::MatMul { .. } | IRNodeType::SparseMatMul { .. } => { if inputs.len() < 2 { return Err(PolygraphError::InvalidNodeConfiguration { message: format!( "MatMul operations require at least 2 inputs, got {}", inputs.len() ), }); } } IRNodeType::GraphConvolution { .. } => { if inputs.len() < 2 { return Err(PolygraphError::InvalidNodeConfiguration { message: "GraphConvolution requires at least 2 inputs (features, adjacency)" .to_string(), }); } } IRNodeType::FFT { .. } => { if inputs.is_empty() { return Err(PolygraphError::InvalidNodeConfiguration { message: "FFT requires at least 1 input".to_string(), }); } } IRNodeType::ElementwiseMul | IRNodeType::Add => { if inputs.is_empty() { return Err(PolygraphError::InvalidNodeConfiguration { message: "Elementwise operations require at least 1 input".to_string(), }); } } IRNodeType::ReLU => { if inputs.len() != 1 { return Err(PolygraphError::InvalidNodeConfiguration { message: format!("ReLU requires exactly 1 input, got {}", inputs.len()), }); } } IRNodeType::Conditional { .. } => { if inputs.len() < 2 { return Err(PolygraphError::InvalidNodeConfiguration { message: "Conditional requires at least 2 inputs".to_string(), }); } } } Ok(Self { id, node_type, inputs, output_dtypes, output_shapes, }) } pub fn id(&self) -> NodeId { self.id } pub fn node_type(&self) -> &IRNodeType { &self.node_type } pub fn inputs(&self) -> &[NodeId] { &self.inputs } pub fn outputs(&self) -> &[DataType] { &self.output_dtypes } pub fn output_dtypes(&self) -> &[DataType] { &self.output_dtypes } pub fn output_shapes(&self) -> &[Shape] { &self.output_shapes } pub fn signature(&self) -> String { let mut hasher = FxHasher::default(); // Hash node type, inputs structure, output types and shapes // But NOT the specific node IDs self.node_type.hash(&mut hasher); self.inputs.len().hash(&mut hasher); self.output_dtypes.hash(&mut hasher); for shape in &self.output_shapes { shape.dims.hash(&mut hasher); } format!("{:016x}", hasher.finish()) } } impl Hash for IRNodeType { fn hash(&self, state: &mut H) { match self { Self::MatMul { transpose_a, transpose_b, } => { 0u8.hash(state); transpose_a.hash(state); transpose_b.hash(state); } Self::SparseMatMul { format } => { 1u8.hash(state); format.hash(state); } Self::GraphConvolution { aggregation } => { 2u8.hash(state); aggregation.hash(state); } Self::FFT { inverse, axes } => { 3u8.hash(state); inverse.hash(state); axes.hash(state); } Self::ElementwiseMul => 4u8.hash(state), Self::Add => 5u8.hash(state), Self::ReLU => 6u8.hash(state), Self::Conditional { condition_id: _ } => { 7u8.hash(state); // Don't hash the actual condition_id for signature purposes } } } } impl Hash for SparseFormat { fn hash(&self, state: &mut H) { match self { Self::CSR => 0u8.hash(state), Self::CSC => 1u8.hash(state), Self::COO => 2u8.hash(state), } } } impl Hash for AggregationType { fn hash(&self, state: &mut H) { match self { Self::Sum => 0u8.hash(state), Self::Mean => 1u8.hash(state), Self::Max => 2u8.hash(state), Self::Min => 3u8.hash(state), } } } impl Hash for DataType { fn hash(&self, state: &mut H) { match self { Self::F32 => 0u8.hash(state), Self::F64 => 1u8.hash(state), Self::I32 => 2u8.hash(state), Self::I64 => 3u8.hash(state), Self::C64 => 4u8.hash(state), Self::C128 => 5u8.hash(state), } } } #[derive(Debug, Clone)] pub struct IRGraph { nodes: IndexMap, edges: HashMap>, // adjacency list outputs: Vec, } impl IRGraph { pub fn new() -> Self { Self { nodes: IndexMap::new(), edges: HashMap::new(), outputs: Vec::new(), } } pub fn add_node(&mut self, node: IRNode) { let node_id = node.id(); // Add edges for inputs for input_id in node.inputs() { self.edges.entry(*input_id).or_default().push(node_id); } self.nodes.insert(node_id, node); } pub fn node_count(&self) -> usize { self.nodes.len() } pub fn contains_node(&self, id: NodeId) -> bool { self.nodes.contains_key(&id) } pub fn get_node(&self, id: NodeId) -> Option<&IRNode> { self.nodes.get(&id) } pub fn mark_output(&mut self, id: NodeId) { if !self.outputs.contains(&id) { self.outputs.push(id); } } pub fn output_shapes(&self) -> Vec> { self.outputs .iter() .filter_map(|&id| self.get_node(id)) .map(|node| node.output_shapes().to_vec()) .collect() } pub fn output_dtypes(&self) -> Vec> { self.outputs .iter() .filter_map(|&id| self.get_node(id)) .map(|node| node.output_dtypes().to_vec()) .collect() } pub fn estimated_memory_usage(&self) -> usize { self.nodes .values() .map(|node| { node.output_shapes() .iter() .zip(node.output_dtypes()) .map(|(shape, dtype)| { let element_size = match dtype { DataType::F32 | DataType::I32 => 4, DataType::F64 | DataType::I64 | DataType::C64 => 8, DataType::C128 => 16, }; shape.size() * element_size }) .sum::() }) .sum() } pub fn nodes(&self) -> impl Iterator { self.nodes.values() } pub fn remove_node(&mut self, id: NodeId) { if let Some(node) = self.nodes.shift_remove(&id) { // Remove from edges for input_id in node.inputs() { if let Some(users) = self.edges.get_mut(input_id) { users.retain(|&user_id| user_id != id); } } self.edges.remove(&id); // Remove from outputs if present self.outputs.retain(|&output_id| output_id != id); } } pub fn users(&self, id: NodeId) -> Option<&[NodeId]> { self.edges.get(&id).map(|v| v.as_slice()) } } impl Default for IRGraph { fn default() -> Self { Self::new() } }