1use std::fmt;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
16pub struct NodeId(pub u32);
17
18impl fmt::Display for NodeId {
19 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
20 write!(f, "N{}", self.0)
21 }
22}
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
26pub struct BufferId(pub u32);
27
28impl fmt::Display for BufferId {
29 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
30 write!(f, "B{}", self.0)
31 }
32}
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
39pub struct StreamId(pub u32);
40
41impl StreamId {
42 pub const DEFAULT: Self = Self(0);
44}
45
46impl fmt::Display for StreamId {
47 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
48 if self.0 == 0 {
49 write!(f, "S:default")
50 } else {
51 write!(f, "S{}", self.0)
52 }
53 }
54}
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
62pub struct KernelConfig {
63 pub grid: (u32, u32, u32),
65 pub block: (u32, u32, u32),
67 pub shared_mem_bytes: u32,
69}
70
71impl KernelConfig {
72 #[must_use]
78 pub fn linear(num_blocks: u32, threads_per_block: u32, shared_mem_bytes: u32) -> Self {
79 Self {
80 grid: (num_blocks, 1, 1),
81 block: (threads_per_block, 1, 1),
82 shared_mem_bytes,
83 }
84 }
85
86 #[must_use]
88 pub fn total_threads(&self) -> u64 {
89 let g = self.grid;
90 let b = self.block;
91 u64::from(g.0)
92 * u64::from(g.1)
93 * u64::from(g.2)
94 * u64::from(b.0)
95 * u64::from(b.1)
96 * u64::from(b.2)
97 }
98
99 #[must_use]
101 pub fn total_blocks(&self) -> u64 {
102 let g = self.grid;
103 u64::from(g.0) * u64::from(g.1) * u64::from(g.2)
104 }
105}
106
107impl fmt::Display for KernelConfig {
108 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
109 let (gx, gy, gz) = self.grid;
110 let (bx, by, bz) = self.block;
111 write!(
112 f,
113 "grid=({gx},{gy},{gz}) block=({bx},{by},{bz}) smem={}B",
114 self.shared_mem_bytes
115 )
116 }
117}
118
119#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
125pub enum MemcpyDir {
126 HostToDevice,
128 DeviceToHost,
130 DeviceToDevice,
132 PeerToPeer { src_device: u32, dst_device: u32 },
134}
135
136impl fmt::Display for MemcpyDir {
137 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
138 match self {
139 Self::HostToDevice => write!(f, "HtoD"),
140 Self::DeviceToHost => write!(f, "DtoH"),
141 Self::DeviceToDevice => write!(f, "DtoD"),
142 Self::PeerToPeer {
143 src_device,
144 dst_device,
145 } => {
146 write!(f, "PeerToPeer(dev{src_device}→dev{dst_device})")
147 }
148 }
149 }
150}
151
152#[derive(Debug, Clone, PartialEq)]
158pub enum NodeKind {
159 KernelLaunch {
161 function_name: String,
163 config: KernelConfig,
165 fusible: bool,
167 },
168
169 Memcpy {
171 dir: MemcpyDir,
173 size_bytes: usize,
175 },
176
177 Memset {
179 size_bytes: usize,
181 value: u8,
183 },
184
185 EventRecord,
187
188 EventWait,
190
191 HostCallback {
193 label: String,
195 },
196
197 Barrier,
199
200 Conditional {
202 condition_buf: BufferId,
204 true_branch: Vec<NodeId>,
206 false_branch: Vec<NodeId>,
208 },
209}
210
211impl NodeKind {
212 #[must_use]
214 pub fn is_compute(&self) -> bool {
215 matches!(self, Self::KernelLaunch { .. })
216 }
217
218 #[must_use]
220 pub fn is_memory_op(&self) -> bool {
221 matches!(self, Self::Memcpy { .. } | Self::Memset { .. })
222 }
223
224 #[must_use]
226 pub fn is_fusible(&self) -> bool {
227 match self {
228 Self::KernelLaunch { fusible, .. } => *fusible,
229 _ => false,
230 }
231 }
232
233 #[must_use]
235 pub fn function_name(&self) -> Option<&str> {
236 match self {
237 Self::KernelLaunch { function_name, .. } => Some(function_name.as_str()),
238 _ => None,
239 }
240 }
241
242 #[must_use]
244 pub fn tag(&self) -> &'static str {
245 match self {
246 Self::KernelLaunch { .. } => "kernel",
247 Self::Memcpy { .. } => "memcpy",
248 Self::Memset { .. } => "memset",
249 Self::EventRecord => "event_record",
250 Self::EventWait => "event_wait",
251 Self::HostCallback { .. } => "host_cb",
252 Self::Barrier => "barrier",
253 Self::Conditional { .. } => "conditional",
254 }
255 }
256}
257
258impl fmt::Display for NodeKind {
259 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
260 match self {
261 Self::KernelLaunch {
262 function_name,
263 config,
264 fusible,
265 } => {
266 let fuse = if *fusible { " [fusible]" } else { "" };
267 write!(f, "KernelLaunch({function_name}, {config}{fuse})")
268 }
269 Self::Memcpy { dir, size_bytes } => {
270 write!(f, "Memcpy({dir}, {size_bytes}B)")
271 }
272 Self::Memset { size_bytes, value } => {
273 write!(f, "Memset({size_bytes}B, 0x{value:02x})")
274 }
275 Self::EventRecord => write!(f, "EventRecord"),
276 Self::EventWait => write!(f, "EventWait"),
277 Self::HostCallback { label } => write!(f, "HostCallback({label})"),
278 Self::Barrier => write!(f, "Barrier"),
279 Self::Conditional { condition_buf, .. } => {
280 write!(f, "Conditional(cond={condition_buf})")
281 }
282 }
283 }
284}
285
286#[derive(Debug, Clone)]
298pub struct GraphNode {
299 pub id: NodeId,
301 pub kind: NodeKind,
303 pub inputs: Vec<BufferId>,
305 pub outputs: Vec<BufferId>,
307 pub stream_hint: Option<StreamId>,
309 pub cost_hint: u64,
311 pub name: Option<String>,
313}
314
315impl GraphNode {
316 #[must_use]
318 pub fn new(id: NodeId, kind: NodeKind) -> Self {
319 Self {
320 id,
321 kind,
322 inputs: Vec::new(),
323 outputs: Vec::new(),
324 stream_hint: None,
325 cost_hint: 1,
326 name: None,
327 }
328 }
329
330 #[must_use]
332 pub fn with_inputs(mut self, inputs: impl IntoIterator<Item = BufferId>) -> Self {
333 self.inputs.extend(inputs);
334 self
335 }
336
337 #[must_use]
339 pub fn with_outputs(mut self, outputs: impl IntoIterator<Item = BufferId>) -> Self {
340 self.outputs.extend(outputs);
341 self
342 }
343
344 #[must_use]
346 pub fn with_stream(mut self, stream: StreamId) -> Self {
347 self.stream_hint = Some(stream);
348 self
349 }
350
351 #[must_use]
353 pub fn with_cost(mut self, cost: u64) -> Self {
354 self.cost_hint = cost;
355 self
356 }
357
358 #[must_use]
360 pub fn with_name(mut self, name: impl Into<String>) -> Self {
361 self.name = Some(name.into());
362 self
363 }
364
365 #[must_use]
367 pub fn display_name(&self) -> String {
368 self.name.clone().unwrap_or_else(|| self.id.to_string())
369 }
370
371 #[must_use]
373 pub fn is_source(&self) -> bool {
374 self.inputs.is_empty()
375 }
376
377 #[must_use]
379 pub fn is_sink(&self) -> bool {
380 self.outputs.is_empty()
381 }
382}
383
384impl fmt::Display for GraphNode {
385 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
386 let name = self.display_name();
387 write!(f, "[{}] {}", name, self.kind)?;
388 if !self.inputs.is_empty() {
389 let ins: Vec<String> = self.inputs.iter().map(|b| b.to_string()).collect();
390 write!(f, " ←({})", ins.join(","))?;
391 }
392 if !self.outputs.is_empty() {
393 let outs: Vec<String> = self.outputs.iter().map(|b| b.to_string()).collect();
394 write!(f, " →({})", outs.join(","))?;
395 }
396 Ok(())
397 }
398}
399
400#[derive(Debug, Clone)]
410pub struct BufferDescriptor {
411 pub id: BufferId,
413 pub size_bytes: usize,
415 pub name: Option<String>,
417 pub external: bool,
419 pub alignment: usize,
421}
422
423impl BufferDescriptor {
424 #[must_use]
426 pub fn new(id: BufferId, size_bytes: usize) -> Self {
427 Self {
428 id,
429 size_bytes,
430 name: None,
431 external: false,
432 alignment: 256,
433 }
434 }
435
436 #[must_use]
438 pub fn external(mut self) -> Self {
439 self.external = true;
440 self
441 }
442
443 #[must_use]
449 pub fn with_alignment(mut self, align: usize) -> Self {
450 debug_assert!(
451 align > 0 && align.is_power_of_two(),
452 "alignment must be a non-zero power of two"
453 );
454 self.alignment = align;
455 self
456 }
457
458 #[must_use]
460 pub fn with_name(mut self, name: impl Into<String>) -> Self {
461 self.name = Some(name.into());
462 self
463 }
464}
465
466impl fmt::Display for BufferDescriptor {
467 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
468 let name = self.name.as_deref().unwrap_or("<unnamed>");
469 let ext = if self.external { " [ext]" } else { "" };
470 write!(
471 f,
472 "Buffer({}, {} \"{}\"{})",
473 self.id, self.size_bytes, name, ext
474 )
475 }
476}
477
478#[cfg(test)]
483mod tests {
484 use super::*;
485
486 #[test]
489 fn node_id_display() {
490 assert_eq!(NodeId(0).to_string(), "N0");
491 assert_eq!(NodeId(42).to_string(), "N42");
492 }
493
494 #[test]
495 fn buffer_id_display() {
496 assert_eq!(BufferId(0).to_string(), "B0");
497 assert_eq!(BufferId(7).to_string(), "B7");
498 }
499
500 #[test]
501 fn stream_id_default_display() {
502 assert_eq!(StreamId::DEFAULT.to_string(), "S:default");
503 assert_eq!(StreamId(1).to_string(), "S1");
504 }
505
506 #[test]
507 fn node_id_ordering() {
508 assert!(NodeId(1) > NodeId(0));
509 assert!(NodeId(0) < NodeId(100));
510 let mut ids = vec![NodeId(3), NodeId(1), NodeId(2)];
511 ids.sort();
512 assert_eq!(ids, vec![NodeId(1), NodeId(2), NodeId(3)]);
513 }
514
515 #[test]
518 fn kernel_config_linear() {
519 let cfg = KernelConfig::linear(4, 256, 1024);
520 assert_eq!(cfg.grid, (4, 1, 1));
521 assert_eq!(cfg.block, (256, 1, 1));
522 assert_eq!(cfg.shared_mem_bytes, 1024);
523 }
524
525 #[test]
526 fn kernel_config_total_threads() {
527 let cfg = KernelConfig {
528 grid: (4, 2, 1),
529 block: (32, 4, 2),
530 shared_mem_bytes: 0,
531 };
532 assert_eq!(cfg.total_threads(), 4 * 2 * 32 * 4 * 2);
533 }
534
535 #[test]
536 fn kernel_config_total_blocks() {
537 let cfg = KernelConfig {
538 grid: (3, 3, 3),
539 block: (8, 8, 8),
540 shared_mem_bytes: 0,
541 };
542 assert_eq!(cfg.total_blocks(), 27);
543 }
544
545 #[test]
546 fn kernel_config_display() {
547 let cfg = KernelConfig::linear(8, 128, 0);
548 let s = cfg.to_string();
549 assert!(s.contains("grid=(8,1,1)"));
550 assert!(s.contains("block=(128,1,1)"));
551 }
552
553 #[test]
556 fn memcpy_dir_display() {
557 assert_eq!(MemcpyDir::HostToDevice.to_string(), "HtoD");
558 assert_eq!(MemcpyDir::DeviceToHost.to_string(), "DtoH");
559 assert_eq!(MemcpyDir::DeviceToDevice.to_string(), "DtoD");
560 let p2p = MemcpyDir::PeerToPeer {
561 src_device: 0,
562 dst_device: 1,
563 };
564 assert!(p2p.to_string().contains("PeerToPeer"));
565 }
566
567 #[test]
570 fn node_kind_is_compute() {
571 let k = NodeKind::KernelLaunch {
572 function_name: "add".into(),
573 config: KernelConfig::linear(1, 32, 0),
574 fusible: true,
575 };
576 assert!(k.is_compute());
577 assert!(
578 !NodeKind::Memset {
579 size_bytes: 1024,
580 value: 0
581 }
582 .is_compute()
583 );
584 }
585
586 #[test]
587 fn node_kind_is_memory_op() {
588 let m = NodeKind::Memcpy {
589 dir: MemcpyDir::HostToDevice,
590 size_bytes: 128,
591 };
592 assert!(m.is_memory_op());
593 let ms = NodeKind::Memset {
594 size_bytes: 64,
595 value: 0xff,
596 };
597 assert!(ms.is_memory_op());
598 let k = NodeKind::KernelLaunch {
599 function_name: "k".into(),
600 config: KernelConfig::linear(1, 32, 0),
601 fusible: false,
602 };
603 assert!(!k.is_memory_op());
604 }
605
606 #[test]
607 fn node_kind_is_fusible() {
608 let f = NodeKind::KernelLaunch {
609 function_name: "add".into(),
610 config: KernelConfig::linear(1, 32, 0),
611 fusible: true,
612 };
613 assert!(f.is_fusible());
614 let nf = NodeKind::KernelLaunch {
615 function_name: "custom".into(),
616 config: KernelConfig::linear(1, 32, 0),
617 fusible: false,
618 };
619 assert!(!nf.is_fusible());
620 assert!(!NodeKind::Barrier.is_fusible());
621 }
622
623 #[test]
624 fn node_kind_function_name() {
625 let k = NodeKind::KernelLaunch {
626 function_name: "scale".into(),
627 config: KernelConfig::linear(1, 32, 0),
628 fusible: false,
629 };
630 assert_eq!(k.function_name(), Some("scale"));
631 assert_eq!(NodeKind::Barrier.function_name(), None);
632 }
633
634 #[test]
635 fn node_kind_tag() {
636 assert_eq!(
637 NodeKind::KernelLaunch {
638 function_name: "x".into(),
639 config: KernelConfig::linear(1, 1, 0),
640 fusible: false
641 }
642 .tag(),
643 "kernel"
644 );
645 assert_eq!(
646 NodeKind::Memcpy {
647 dir: MemcpyDir::HostToDevice,
648 size_bytes: 1
649 }
650 .tag(),
651 "memcpy"
652 );
653 assert_eq!(
654 NodeKind::Memset {
655 size_bytes: 1,
656 value: 0
657 }
658 .tag(),
659 "memset"
660 );
661 assert_eq!(NodeKind::EventRecord.tag(), "event_record");
662 assert_eq!(NodeKind::EventWait.tag(), "event_wait");
663 assert_eq!(NodeKind::Barrier.tag(), "barrier");
664 assert_eq!(
665 NodeKind::HostCallback { label: "x".into() }.tag(),
666 "host_cb"
667 );
668 }
669
670 #[test]
671 fn node_kind_display_kernel() {
672 let k = NodeKind::KernelLaunch {
673 function_name: "vector_add".into(),
674 config: KernelConfig::linear(4, 256, 0),
675 fusible: true,
676 };
677 let s = k.to_string();
678 assert!(s.contains("vector_add"));
679 assert!(s.contains("fusible"));
680 }
681
682 #[test]
683 fn node_kind_display_memcpy() {
684 let m = NodeKind::Memcpy {
685 dir: MemcpyDir::DeviceToHost,
686 size_bytes: 4096,
687 };
688 let s = m.to_string();
689 assert!(s.contains("DtoH"));
690 assert!(s.contains("4096"));
691 }
692
693 #[test]
696 fn graph_node_new() {
697 let n = GraphNode::new(NodeId(5), NodeKind::Barrier);
698 assert_eq!(n.id, NodeId(5));
699 assert!(n.inputs.is_empty());
700 assert!(n.outputs.is_empty());
701 assert!(n.stream_hint.is_none());
702 assert_eq!(n.cost_hint, 1);
703 assert!(n.name.is_none());
704 }
705
706 #[test]
707 fn graph_node_builder_pattern() {
708 let n = GraphNode::new(NodeId(0), NodeKind::Barrier)
709 .with_inputs([BufferId(0), BufferId(1)])
710 .with_outputs([BufferId(2)])
711 .with_stream(StreamId(1))
712 .with_cost(100)
713 .with_name("barrier_join");
714 assert_eq!(n.inputs.len(), 2);
715 assert_eq!(n.outputs.len(), 1);
716 assert_eq!(n.stream_hint, Some(StreamId(1)));
717 assert_eq!(n.cost_hint, 100);
718 assert_eq!(n.display_name(), "barrier_join");
719 }
720
721 #[test]
722 fn graph_node_is_source_and_sink() {
723 let source = GraphNode::new(NodeId(0), NodeKind::Barrier).with_outputs([BufferId(0)]);
724 assert!(source.is_source());
725 assert!(!source.is_sink());
726
727 let sink = GraphNode::new(NodeId(1), NodeKind::Barrier).with_inputs([BufferId(0)]);
728 assert!(!sink.is_source());
729 assert!(sink.is_sink());
730 }
731
732 #[test]
733 fn graph_node_display_name_fallback() {
734 let n = GraphNode::new(NodeId(7), NodeKind::Barrier);
735 assert_eq!(n.display_name(), "N7");
736 }
737
738 #[test]
739 fn graph_node_display() {
740 let n = GraphNode::new(
741 NodeId(3),
742 NodeKind::Memcpy {
743 dir: MemcpyDir::HostToDevice,
744 size_bytes: 512,
745 },
746 )
747 .with_inputs([BufferId(0)])
748 .with_outputs([BufferId(1)])
749 .with_name("upload");
750 let s = n.to_string();
751 assert!(s.contains("upload"));
752 assert!(s.contains("HtoD"));
753 assert!(s.contains("B0"));
754 assert!(s.contains("B1"));
755 }
756
757 #[test]
760 fn buffer_descriptor_new() {
761 let b = BufferDescriptor::new(BufferId(0), 4096);
762 assert_eq!(b.id, BufferId(0));
763 assert_eq!(b.size_bytes, 4096);
764 assert!(!b.external);
765 assert_eq!(b.alignment, 256);
766 }
767
768 #[test]
769 fn buffer_descriptor_builder() {
770 let b = BufferDescriptor::new(BufferId(1), 1024)
771 .external()
772 .with_alignment(512)
773 .with_name("weights");
774 assert!(b.external);
775 assert_eq!(b.alignment, 512);
776 assert_eq!(b.name.as_deref(), Some("weights"));
777 }
778
779 #[test]
780 fn buffer_descriptor_display() {
781 let b = BufferDescriptor::new(BufferId(2), 8192).with_name("activations");
782 let s = b.to_string();
783 assert!(s.contains("B2"));
784 assert!(s.contains("8192"));
785 assert!(s.contains("activations"));
786 }
787}