1use std::collections::{HashMap, HashSet, VecDeque};
15use std::fmt;
16use std::sync::{Arc, Mutex};
17use torsh_core::sync::MutexExt;
18
19use torsh_core::{
20 device::DeviceType,
21 dtype::TensorElement,
22 error::{Result, TorshError},
23};
24
25use crate::Tensor;
26
27pub type NodeId = usize;
29
30#[derive(Debug, Clone)]
32pub enum GraphOp {
33 Constant,
35 Add,
37 Mul,
39 Sub,
41 Div,
43 MatMul,
45 Reshape(Vec<usize>),
47 Transpose(usize, usize),
49 Sum(Option<i32>),
51 Mean(Option<i32>),
53 ReLU,
55 Sigmoid,
57 Tanh,
59 AddScalar(f64),
61 MulScalar(f64),
63 Custom(String),
65}
66
67impl fmt::Display for GraphOp {
68 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
69 match self {
70 GraphOp::Constant => write!(f, "Const"),
71 GraphOp::Add => write!(f, "Add"),
72 GraphOp::Mul => write!(f, "Mul"),
73 GraphOp::Sub => write!(f, "Sub"),
74 GraphOp::Div => write!(f, "Div"),
75 GraphOp::MatMul => write!(f, "MatMul"),
76 GraphOp::Reshape(shape) => write!(f, "Reshape({:?})", shape),
77 GraphOp::Transpose(d0, d1) => write!(f, "Transpose({}, {})", d0, d1),
78 GraphOp::Sum(dim) => write!(f, "Sum({:?})", dim),
79 GraphOp::Mean(dim) => write!(f, "Mean({:?})", dim),
80 GraphOp::ReLU => write!(f, "ReLU"),
81 GraphOp::Sigmoid => write!(f, "Sigmoid"),
82 GraphOp::Tanh => write!(f, "Tanh"),
83 GraphOp::AddScalar(s) => write!(f, "AddScalar({})", s),
84 GraphOp::MulScalar(s) => write!(f, "MulScalar({})", s),
85 GraphOp::Custom(name) => write!(f, "Custom({})", name),
86 }
87 }
88}
89
90#[derive(Clone)]
92pub struct GraphNode<T: TensorElement> {
93 pub id: NodeId,
95 pub op: GraphOp,
97 pub inputs: Vec<NodeId>,
99 pub data: Option<Arc<Tensor<T>>>,
101 pub shape: Option<Vec<usize>>,
103 pub device: DeviceType,
105}
106
107impl<T: TensorElement> GraphNode<T> {
108 fn new(id: NodeId, op: GraphOp, inputs: Vec<NodeId>, device: DeviceType) -> Self {
110 Self {
111 id,
112 op,
113 inputs,
114 data: None,
115 shape: None,
116 device,
117 }
118 }
119
120 fn constant(id: NodeId, tensor: Tensor<T>) -> Self {
122 let device = tensor.device;
123 let shape = Some(tensor.shape().dims().to_vec());
124 Self {
125 id,
126 op: GraphOp::Constant,
127 inputs: Vec::new(),
128 data: Some(Arc::new(tensor)),
129 shape,
130 device,
131 }
132 }
133}
134
135pub struct ComputationGraph<T: TensorElement> {
137 nodes: HashMap<NodeId, GraphNode<T>>,
139 next_id: NodeId,
141 outputs: Vec<NodeId>,
143 cache: Arc<Mutex<HashMap<NodeId, Arc<Tensor<T>>>>>,
145}
146
147impl<T: TensorElement + Copy> ComputationGraph<T> {
148 pub fn new() -> Self {
150 Self {
151 nodes: HashMap::new(),
152 next_id: 0,
153 outputs: Vec::new(),
154 cache: Arc::new(Mutex::new(HashMap::new())),
155 }
156 }
157
158 pub fn constant(&mut self, tensor: Tensor<T>) -> NodeId {
160 let id = self.allocate_id();
161 let node = GraphNode::constant(id, tensor);
162 self.nodes.insert(id, node);
163 id
164 }
165
166 pub fn binary_op(
168 &mut self,
169 op: GraphOp,
170 left: NodeId,
171 right: NodeId,
172 device: DeviceType,
173 ) -> NodeId {
174 let id = self.allocate_id();
175 let node = GraphNode::new(id, op, vec![left, right], device);
176 self.nodes.insert(id, node);
177 id
178 }
179
180 pub fn unary_op(&mut self, op: GraphOp, input: NodeId, device: DeviceType) -> NodeId {
182 let id = self.allocate_id();
183 let node = GraphNode::new(id, op, vec![input], device);
184 self.nodes.insert(id, node);
185 id
186 }
187
188 pub fn mark_output(&mut self, node: NodeId) {
190 if !self.outputs.contains(&node) {
191 self.outputs.push(node);
192 }
193 }
194
195 pub fn num_nodes(&self) -> usize {
197 self.nodes.len()
198 }
199
200 pub fn num_outputs(&self) -> usize {
202 self.outputs.len()
203 }
204
205 fn allocate_id(&mut self) -> NodeId {
207 let id = self.next_id;
208 self.next_id += 1;
209 id
210 }
211
212 pub fn topological_sort(&self) -> Result<Vec<NodeId>> {
214 let mut in_degree: HashMap<NodeId, usize> = HashMap::new();
215 let mut adj_list: HashMap<NodeId, Vec<NodeId>> = HashMap::new();
216
217 for (&id, node) in &self.nodes {
219 in_degree.entry(id).or_insert(0);
220 for &input_id in &node.inputs {
221 adj_list.entry(input_id).or_insert_with(Vec::new).push(id);
222 *in_degree.entry(id).or_insert(0) += 1;
223 }
224 }
225
226 let mut queue: VecDeque<NodeId> = in_degree
228 .iter()
229 .filter(|(_, °ree)| degree == 0)
230 .map(|(&id, _)| id)
231 .collect();
232
233 let mut sorted = Vec::new();
234
235 while let Some(node_id) = queue.pop_front() {
236 sorted.push(node_id);
237
238 if let Some(neighbors) = adj_list.get(&node_id) {
239 for &neighbor in neighbors {
240 if let Some(degree) = in_degree.get_mut(&neighbor) {
241 *degree -= 1;
242 if *degree == 0 {
243 queue.push_back(neighbor);
244 }
245 }
246 }
247 }
248 }
249
250 if sorted.len() != self.nodes.len() {
251 return Err(TorshError::InvalidArgument(
252 "Graph contains cycles".to_string(),
253 ));
254 }
255
256 Ok(sorted)
257 }
258
259 pub fn optimize(&mut self) -> Result<()>
261 where
262 T: std::ops::Add<Output = T>
263 + std::ops::Sub<Output = T>
264 + std::ops::Mul<Output = T>
265 + std::ops::Div<Output = T>
266 + torsh_core::FloatElement,
267 {
268 self.fold_constants()?;
271
272 self.eliminate_dead_code();
274
275 Ok(())
278 }
279
280 fn fold_constants(&mut self) -> Result<()>
282 where
283 T: std::ops::Add<Output = T>
284 + std::ops::Sub<Output = T>
285 + std::ops::Mul<Output = T>
286 + std::ops::Div<Output = T>
287 + torsh_core::FloatElement,
288 {
289 let sorted = self.topological_sort()?;
290
291 for &node_id in &sorted {
292 let node = self
293 .nodes
294 .get(&node_id)
295 .expect("node_id should exist in nodes after topological sort")
296 .clone();
297
298 let all_constant = node.inputs.iter().all(|&input_id| {
300 if let Some(input_node) = self.nodes.get(&input_id) {
301 matches!(input_node.op, GraphOp::Constant)
302 } else {
303 false
304 }
305 });
306
307 if all_constant && !node.inputs.is_empty() {
308 if let Ok(result) = self.evaluate_node_internal(&node) {
310 let mut new_node = GraphNode::constant(node_id, result);
312 new_node.device = node.device;
313 self.nodes.insert(node_id, new_node);
314 }
315 }
316 }
317
318 Ok(())
319 }
320
321 fn eliminate_dead_code(&mut self) {
323 let mut reachable = HashSet::new();
324 let mut queue = VecDeque::from_iter(self.outputs.iter().copied());
325
326 while let Some(node_id) = queue.pop_front() {
328 if reachable.insert(node_id) {
329 if let Some(node) = self.nodes.get(&node_id) {
330 for &input_id in &node.inputs {
331 queue.push_back(input_id);
332 }
333 }
334 }
335 }
336
337 self.nodes.retain(|&id, _| reachable.contains(&id));
339 }
340
341 fn evaluate_node_internal(&self, node: &GraphNode<T>) -> Result<Tensor<T>>
343 where
344 T: std::ops::Add<Output = T>
345 + std::ops::Sub<Output = T>
346 + std::ops::Mul<Output = T>
347 + std::ops::Div<Output = T>
348 + torsh_core::FloatElement,
349 {
350 match &node.op {
351 GraphOp::Constant => node
352 .data
353 .as_ref()
354 .map(|t| (**t).clone())
355 .ok_or_else(|| TorshError::InvalidArgument("Constant has no data".to_string())),
356 GraphOp::Add => {
357 let left = self.get_input_tensor(node, 0)?;
358 let right = self.get_input_tensor(node, 1)?;
359 left.add_op(&right)
360 }
361 GraphOp::Mul => {
362 let left = self.get_input_tensor(node, 0)?;
363 let right = self.get_input_tensor(node, 1)?;
364 left.mul_op(&right)
365 }
366 GraphOp::Sub => {
367 let left = self.get_input_tensor(node, 0)?;
368 let right = self.get_input_tensor(node, 1)?;
369 left.sub(&right)
370 }
371 GraphOp::Div => {
372 let left = self.get_input_tensor(node, 0)?;
373 let right = self.get_input_tensor(node, 1)?;
374 left.div(&right)
375 }
376 GraphOp::AddScalar(s) => {
377 let input = self.get_input_tensor(node, 0)?;
378 let scalar = T::from_f64(*s).ok_or_else(|| {
379 TorshError::InvalidArgument("Cannot convert scalar to tensor type".to_string())
380 })?;
381 input.add_scalar(scalar)
382 }
383 GraphOp::MulScalar(s) => {
384 let input = self.get_input_tensor(node, 0)?;
385 let scalar = T::from_f64(*s).ok_or_else(|| {
386 TorshError::InvalidArgument("Cannot convert scalar to tensor type".to_string())
387 })?;
388 input.mul_scalar(scalar)
389 }
390 GraphOp::ReLU => {
391 let input = self.get_input_tensor(node, 0)?;
392 input.relu()
393 }
394 GraphOp::Sigmoid => {
395 let input = self.get_input_tensor(node, 0)?;
396 input.sigmoid()
397 }
398 GraphOp::Tanh => {
399 let input = self.get_input_tensor(node, 0)?;
400 input.tanh()
401 }
402 _ => Err(TorshError::InvalidArgument(format!(
403 "Unsupported operation: {}",
404 node.op
405 ))),
406 }
407 }
408
409 fn get_input_tensor(&self, node: &GraphNode<T>, index: usize) -> Result<Tensor<T>> {
411 let input_id = node.inputs.get(index).ok_or_else(|| {
412 TorshError::InvalidArgument(format!("Missing input {} for node {}", index, node.id))
413 })?;
414
415 let input_node = self.nodes.get(input_id).ok_or_else(|| {
416 TorshError::InvalidArgument(format!("Input node {} not found", input_id))
417 })?;
418
419 if let GraphOp::Constant = input_node.op {
420 input_node
421 .data
422 .as_ref()
423 .map(|t| (**t).clone())
424 .ok_or_else(|| TorshError::InvalidArgument("Constant has no data".to_string()))
425 } else {
426 Err(TorshError::InvalidArgument(
427 "Can only evaluate constants in internal evaluation".to_string(),
428 ))
429 }
430 }
431
432 pub fn execute(&self) -> Result<Vec<Tensor<T>>>
434 where
435 T: std::ops::Add<Output = T>
436 + std::ops::Sub<Output = T>
437 + std::ops::Mul<Output = T>
438 + std::ops::Div<Output = T>
439 + torsh_core::FloatElement,
440 {
441 let sorted = self.topological_sort()?;
442 let mut cache = self.cache.lock_or_recover();
443 cache.clear();
444
445 for &node_id in &sorted {
447 let node = self
448 .nodes
449 .get(&node_id)
450 .expect("node_id should exist in nodes after topological sort");
451
452 if cache.contains_key(&node_id) {
454 continue;
455 }
456
457 let result = self.evaluate_node_internal(node)?;
458 cache.insert(node_id, Arc::new(result));
459 }
460
461 let mut outputs = Vec::new();
463 for &output_id in &self.outputs {
464 if let Some(result) = cache.get(&output_id) {
465 outputs.push((**result).clone());
466 } else {
467 return Err(TorshError::InvalidArgument(format!(
468 "Output node {} not computed",
469 output_id
470 )));
471 }
472 }
473
474 Ok(outputs)
475 }
476
477 pub fn to_dot(&self) -> String {
479 let mut dot = String::from("digraph ComputationGraph {\n");
480 dot.push_str(" rankdir=BT;\n");
481 dot.push_str(" node [shape=box];\n\n");
482
483 for (id, node) in &self.nodes {
485 let label = format!("{}\\nid={}", node.op, id);
486 let color = if self.outputs.contains(id) {
487 "red"
488 } else if matches!(node.op, GraphOp::Constant) {
489 "lightblue"
490 } else {
491 "lightgray"
492 };
493
494 dot.push_str(&format!(
495 " {} [label=\"{}\", fillcolor={}, style=filled];\n",
496 id, label, color
497 ));
498 }
499
500 dot.push('\n');
501
502 for (id, node) in &self.nodes {
504 for (idx, &input_id) in node.inputs.iter().enumerate() {
505 dot.push_str(&format!(" {} -> {} [label=\"{}\"];\n", input_id, id, idx));
506 }
507 }
508
509 dot.push_str("}\n");
510 dot
511 }
512}
513
514impl<T: TensorElement + Copy> Default for ComputationGraph<T> {
515 fn default() -> Self {
516 Self::new()
517 }
518}
519
520#[cfg(test)]
521mod tests {
522 use super::*;
523 use crate::creation::*;
524
525 #[test]
526 fn test_graph_creation() {
527 let mut graph = ComputationGraph::<f32>::new();
528
529 let a = tensor_1d(&[1.0, 2.0, 3.0]).expect("tensor_1d creation should succeed");
530 let b = tensor_1d(&[4.0, 5.0, 6.0]).expect("tensor_1d creation should succeed");
531
532 let a_id = graph.constant(a);
533 let b_id = graph.constant(b);
534 let add_id = graph.binary_op(GraphOp::Add, a_id, b_id, DeviceType::Cpu);
535
536 graph.mark_output(add_id);
537
538 assert_eq!(graph.num_nodes(), 3);
539 assert_eq!(graph.num_outputs(), 1);
540 }
541
542 #[test]
543 fn test_topological_sort() {
544 let mut graph = ComputationGraph::<f32>::new();
545
546 let a = tensor_1d(&[1.0, 2.0]).expect("tensor_1d creation should succeed");
547 let b = tensor_1d(&[3.0, 4.0]).expect("tensor_1d creation should succeed");
548
549 let a_id = graph.constant(a);
550 let b_id = graph.constant(b);
551 let add_id = graph.binary_op(GraphOp::Add, a_id, b_id, DeviceType::Cpu);
552 let mul_id = graph.unary_op(GraphOp::MulScalar(2.0), add_id, DeviceType::Cpu);
553
554 let sorted = graph
555 .topological_sort()
556 .expect("topological sort should succeed");
557
558 assert_eq!(sorted.len(), 4);
560
561 let a_pos = sorted
563 .iter()
564 .position(|&id| id == a_id)
565 .expect("position should succeed");
566 let b_pos = sorted
567 .iter()
568 .position(|&id| id == b_id)
569 .expect("position should succeed");
570 let add_pos = sorted
571 .iter()
572 .position(|&id| id == add_id)
573 .expect("position should succeed");
574 let mul_pos = sorted
575 .iter()
576 .position(|&id| id == mul_id)
577 .expect("position should succeed");
578
579 assert!(a_pos < add_pos);
580 assert!(b_pos < add_pos);
581 assert!(add_pos < mul_pos);
582 }
583
584 #[test]
585 fn test_constant_folding() {
586 let mut graph = ComputationGraph::<f32>::new();
587
588 let a = tensor_1d(&[1.0, 2.0]).expect("tensor_1d creation should succeed");
589 let b = tensor_1d(&[3.0, 4.0]).expect("tensor_1d creation should succeed");
590
591 let a_id = graph.constant(a);
592 let b_id = graph.constant(b);
593 let add_id = graph.binary_op(GraphOp::Add, a_id, b_id, DeviceType::Cpu);
594
595 graph.mark_output(add_id);
596
597 assert_eq!(graph.num_nodes(), 3);
599
600 graph.optimize().expect("optimization should succeed");
602
603 let add_node = graph.nodes.get(&add_id).expect("get should succeed");
605 assert!(matches!(add_node.op, GraphOp::Constant));
606 }
607
608 #[test]
609 fn test_dead_code_elimination() {
610 let mut graph = ComputationGraph::<f32>::new();
611
612 let a = tensor_1d(&[1.0]).expect("tensor_1d creation should succeed");
613 let b = tensor_1d(&[2.0]).expect("tensor_1d creation should succeed");
614 let c = tensor_1d(&[3.0]).expect("tensor_1d creation should succeed");
615
616 let a_id = graph.constant(a);
617 let b_id = graph.constant(b);
618 let c_id = graph.constant(c);
619
620 let add_id = graph.binary_op(GraphOp::Add, a_id, b_id, DeviceType::Cpu);
622 graph.mark_output(add_id);
623
624 let _mul_id = graph.unary_op(GraphOp::MulScalar(2.0), c_id, DeviceType::Cpu);
626
627 assert_eq!(graph.num_nodes(), 5);
629
630 graph.optimize().expect("optimization should succeed");
632
633 assert_eq!(graph.num_nodes(), 1); }
637
638 #[test]
639 fn test_graph_execution() {
640 let mut graph = ComputationGraph::<f32>::new();
641
642 let a = tensor_1d(&[1.0, 2.0, 3.0]).expect("tensor_1d creation should succeed");
643 let b = tensor_1d(&[4.0, 5.0, 6.0]).expect("tensor_1d creation should succeed");
644
645 let a_id = graph.constant(a);
646 let b_id = graph.constant(b);
647 let add_id = graph.binary_op(GraphOp::Add, a_id, b_id, DeviceType::Cpu);
648
649 graph.mark_output(add_id);
650
651 let results = graph.execute().expect("execution should succeed");
652 assert_eq!(results.len(), 1);
653
654 let data = results[0]
655 .to_vec()
656 .expect("to_vec conversion should succeed");
657 assert_eq!(data, vec![5.0, 7.0, 9.0]);
658 }
659
660 #[test]
661 fn test_multiple_outputs() {
662 let mut graph = ComputationGraph::<f32>::new();
663
664 let a = tensor_1d(&[1.0, 2.0]).expect("tensor_1d creation should succeed");
665 let b = tensor_1d(&[3.0, 4.0]).expect("tensor_1d creation should succeed");
666
667 let a_id = graph.constant(a);
668 let b_id = graph.constant(b);
669 let add_id = graph.binary_op(GraphOp::Add, a_id, b_id, DeviceType::Cpu);
670 let mul_id = graph.binary_op(GraphOp::Mul, a_id, b_id, DeviceType::Cpu);
671
672 graph.mark_output(add_id);
673 graph.mark_output(mul_id);
674
675 let results = graph.execute().expect("execution should succeed");
676 assert_eq!(results.len(), 2);
677
678 let add_data = results[0]
679 .to_vec()
680 .expect("to_vec conversion should succeed");
681 let mul_data = results[1]
682 .to_vec()
683 .expect("to_vec conversion should succeed");
684
685 assert_eq!(add_data, vec![4.0, 6.0]);
686 assert_eq!(mul_data, vec![3.0, 8.0]);
687 }
688
689 #[test]
690 fn test_dot_generation() {
691 let mut graph = ComputationGraph::<f32>::new();
692
693 let a = tensor_1d(&[1.0]).expect("tensor_1d creation should succeed");
694 let a_id = graph.constant(a);
695 graph.mark_output(a_id);
696
697 let dot = graph.to_dot();
698
699 assert!(dot.contains("digraph ComputationGraph"));
700 assert!(dot.contains(&format!("id={}", a_id)));
701 }
702}