Skip to main content

atomr_accel_cuda/graph/
dot.rs

1//! `cudaGraphDebugDotPrint` round-trip — emit a Graphviz DOT file
2//! describing a captured graph for tooling / visualisation.
3//!
4//! cudarc's `cuGraphDebugDotPrint` writes the DOT to a file path, not
5//! a buffer. We use a temporary file under `std::env::temp_dir()` and
6//! read the contents back as a `String`.
7
8use std::ffi::CString;
9use std::fs;
10use std::path::PathBuf;
11
12use cudarc::driver::sys as driver_sys;
13
14use crate::error::GpuError;
15use crate::graph::GraphHandle;
16
17const LIB: &str = "graph";
18
19bitflags::bitflags! {
20    /// Verbosity flags accepted by `cuGraphDebugDotPrint`. Match
21    /// `CU_GRAPH_DEBUG_DOT_FLAGS_*`.
22    pub struct DotFlags: u32 {
23        const VERBOSE        = 1 << 0;
24        const KERNEL_NODE    = 1 << 2;
25        const MEMCPY_NODE    = 1 << 3;
26        const MEMSET_NODE    = 1 << 4;
27        const HOST_NODE      = 1 << 5;
28        const GRAPH_NODE     = 1 << 6;
29    }
30}
31
32impl Default for DotFlags {
33    fn default() -> Self {
34        DotFlags::empty()
35    }
36}
37
38/// Export `graph` as a Graphviz DOT string. Returns `Unrecoverable`
39/// on no-GPU hosts.
40pub fn export_dot(graph: &GraphHandle, flags: DotFlags) -> Result<String, GpuError> {
41    let path = temp_dot_path();
42    let cpath = CString::new(path.to_string_lossy().as_ref())
43        .map_err(|e| GpuError::Unrecoverable(format!("export_dot: bad path: {e}")))?;
44    let cu_graph = graph.cu_graph();
45    let s = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
46        // SAFETY: cu_graph is a caller-owned handle; cpath is a valid
47        // C string.
48        unsafe { driver_sys::cuGraphDebugDotPrint(cu_graph, cpath.as_ptr(), flags.bits()) }
49    }))
50    .map_err(|_| GpuError::Unrecoverable("export_dot: CUDA driver not loadable".into()))?;
51    if s != driver_sys::cudaError_enum::CUDA_SUCCESS {
52        return Err(GpuError::LibraryError {
53            lib: LIB,
54            msg: format!("cuGraphDebugDotPrint: {s:?}"),
55        });
56    }
57    let dot = fs::read_to_string(&path).map_err(|e| GpuError::LibraryError {
58        lib: LIB,
59        msg: format!("read DOT file {}: {e}", path.display()),
60    })?;
61    let _ = fs::remove_file(&path);
62    Ok(dot)
63}
64
65fn temp_dot_path() -> PathBuf {
66    let mut p = std::env::temp_dir();
67    let pid = std::process::id();
68    let nanos = std::time::SystemTime::now()
69        .duration_since(std::time::UNIX_EPOCH)
70        .map(|d| d.subsec_nanos())
71        .unwrap_or(0);
72    p.push(format!("atomr-accel-graph-{pid}-{nanos}.dot"));
73    p
74}
75
76#[cfg(test)]
77mod tests {
78    use super::*;
79
80    #[test]
81    fn dot_export_returns_nonempty_for_known_graph() {
82        // Mock-mode: synthetic GraphHandle. On no-GPU hosts we expect
83        // Unrecoverable (libcuda missing). On real hardware with a
84        // valid graph this would return a non-empty DOT string. We
85        // assert a non-panicking round-trip.
86        let g = GraphHandle::synthetic_for_tests();
87        let r = export_dot(&g, DotFlags::VERBOSE);
88        match r {
89            Ok(s) => {
90                // Real driver — must produce some DOT.
91                assert!(
92                    !s.is_empty(),
93                    "expected non-empty DOT string from real driver"
94                );
95            }
96            Err(GpuError::Unrecoverable(_)) => {}
97            Err(GpuError::LibraryError { .. }) => {}
98            other => panic!("unexpected: {other:?}"),
99        }
100    }
101}