Skip to main content

oxicuda_graph/
builder.rs

1//! Ergonomic builder API for constructing `ComputeGraph`s.
2//!
3//! `GraphBuilder` provides a fluent interface for assembling a computation
4//! graph, handling ID allocation and optional automatic data-flow edge
5//! inference transparently.
6//!
7//! # Example
8//!
9//! ```rust
10//! # use oxicuda_graph::builder::GraphBuilder;
11//! # use oxicuda_graph::node::MemcpyDir;
12//! let mut b = GraphBuilder::new();
13//!
14//! let upload   = b.add_memcpy("upload",   MemcpyDir::HostToDevice, 4096);
15//! let weights  = b.alloc_buffer("weights",  4096);
16//! let compute  = b.add_kernel("gemm", 32, 256, 0).fusible(false).finish();
17//! let download = b.add_memcpy("download", MemcpyDir::DeviceToHost, 4096);
18//!
19//! b.dep(upload, compute).dep(compute, download);
20//!
21//! let graph = b.build().unwrap();
22//! assert_eq!(graph.node_count(), 3);
23//! ```
24
25use crate::error::GraphResult;
26use crate::graph::ComputeGraph;
27use crate::node::{
28    BufferDescriptor, BufferId, GraphNode, KernelConfig, MemcpyDir, NodeId, NodeKind, StreamId,
29};
30
31// ---------------------------------------------------------------------------
32// KernelNodeBuilder — fluent kernel-node configuration
33// ---------------------------------------------------------------------------
34
35/// Fluent builder for a single kernel launch node.
36///
37/// Obtained via [`GraphBuilder::add_kernel`].
38pub struct KernelNodeBuilder<'a> {
39    parent: &'a mut GraphBuilder,
40    name: String,
41    function_name: String,
42    config: KernelConfig,
43    fusible: bool,
44    inputs: Vec<BufferId>,
45    outputs: Vec<BufferId>,
46    stream: Option<StreamId>,
47    cost: u64,
48}
49
50impl<'a> KernelNodeBuilder<'a> {
51    /// Marks this kernel as fusible with adjacent element-wise kernels.
52    #[must_use]
53    pub fn fusible(mut self, v: bool) -> Self {
54        self.fusible = v;
55        self
56    }
57
58    /// Sets input buffer IDs for this kernel.
59    #[must_use]
60    pub fn inputs(mut self, ids: impl IntoIterator<Item = BufferId>) -> Self {
61        self.inputs.extend(ids);
62        self
63    }
64
65    /// Sets output buffer IDs for this kernel.
66    #[must_use]
67    pub fn outputs(mut self, ids: impl IntoIterator<Item = BufferId>) -> Self {
68        self.outputs.extend(ids);
69        self
70    }
71
72    /// Sets the preferred stream for this node.
73    #[must_use]
74    pub fn on_stream(mut self, s: StreamId) -> Self {
75        self.stream = Some(s);
76        self
77    }
78
79    /// Sets the cost hint for the scheduler.
80    #[must_use]
81    pub fn cost(mut self, c: u64) -> Self {
82        self.cost = c;
83        self
84    }
85
86    /// Finishes configuration and registers the node with the parent builder.
87    ///
88    /// Returns the allocated `NodeId`.
89    pub fn finish(self) -> NodeId {
90        let kind = NodeKind::KernelLaunch {
91            function_name: self.function_name,
92            config: self.config,
93            fusible: self.fusible,
94        };
95        let mut node = GraphNode::new(NodeId(0), kind)
96            .with_inputs(self.inputs)
97            .with_outputs(self.outputs)
98            .with_name(self.name)
99            .with_cost(self.cost);
100        if let Some(s) = self.stream {
101            node = node.with_stream(s);
102        }
103        self.parent.graph.add_node(node)
104    }
105}
106
107// ---------------------------------------------------------------------------
108// GraphBuilder
109// ---------------------------------------------------------------------------
110
111/// Builds a [`ComputeGraph`] via a fluent API.
112///
113/// The builder accumulates nodes, buffers, and explicit dependency edges.
114/// Calling [`build`](GraphBuilder::build) finalises the graph and optionally
115/// infers data-flow edges from buffer read/write relationships.
116#[derive(Debug, Default)]
117pub struct GraphBuilder {
118    graph: ComputeGraph,
119    /// Whether to automatically infer data-flow edges from buffer I/O.
120    auto_infer_edges: bool,
121}
122
123impl GraphBuilder {
124    /// Creates a new empty builder.
125    #[must_use]
126    pub fn new() -> Self {
127        Self {
128            graph: ComputeGraph::new(),
129            auto_infer_edges: true,
130        }
131    }
132
133    /// Configures whether buffer-based data-flow edges are inferred on build.
134    ///
135    /// Default: `true`.
136    #[must_use]
137    pub fn with_auto_infer_edges(mut self, v: bool) -> Self {
138        self.auto_infer_edges = v;
139        self
140    }
141
142    // -----------------------------------------------------------------------
143    // Buffer allocation
144    // -----------------------------------------------------------------------
145
146    /// Allocates a named device buffer and returns its `BufferId`.
147    pub fn alloc_buffer(&mut self, name: &str, size_bytes: usize) -> BufferId {
148        self.graph
149            .add_buffer(BufferDescriptor::new(BufferId(0), size_bytes).with_name(name))
150    }
151
152    /// Allocates an external (caller-managed) buffer.
153    pub fn alloc_external_buffer(&mut self, name: &str, size_bytes: usize) -> BufferId {
154        self.graph.add_buffer(
155            BufferDescriptor::new(BufferId(0), size_bytes)
156                .with_name(name)
157                .external(),
158        )
159    }
160
161    // -----------------------------------------------------------------------
162    // Node addition helpers
163    // -----------------------------------------------------------------------
164
165    /// Starts a fluent kernel-node configuration.
166    ///
167    /// Call `.finish()` on the returned builder to register the node.
168    pub fn add_kernel(
169        &mut self,
170        name: &str,
171        num_blocks: u32,
172        threads_per_block: u32,
173        shared_mem: u32,
174    ) -> KernelNodeBuilder<'_> {
175        KernelNodeBuilder {
176            parent: self,
177            name: name.to_owned(),
178            function_name: name.to_owned(),
179            config: KernelConfig::linear(num_blocks, threads_per_block, shared_mem),
180            fusible: true,
181            inputs: Vec::new(),
182            outputs: Vec::new(),
183            stream: None,
184            cost: 1,
185        }
186    }
187
188    /// Adds a kernel launch node with the given function name and 3-D grid/block.
189    pub fn add_kernel_3d(
190        &mut self,
191        name: &str,
192        grid: (u32, u32, u32),
193        block: (u32, u32, u32),
194        shared_mem: u32,
195    ) -> NodeId {
196        let kind = NodeKind::KernelLaunch {
197            function_name: name.to_owned(),
198            config: KernelConfig {
199                grid,
200                block,
201                shared_mem_bytes: shared_mem,
202            },
203            fusible: false,
204        };
205        self.graph
206            .add_node(GraphNode::new(NodeId(0), kind).with_name(name))
207    }
208
209    /// Adds a host-to-device or device-to-host memcpy node.
210    pub fn add_memcpy(&mut self, name: &str, dir: MemcpyDir, size_bytes: usize) -> NodeId {
211        let kind = NodeKind::Memcpy { dir, size_bytes };
212        self.graph
213            .add_node(GraphNode::new(NodeId(0), kind).with_name(name))
214    }
215
216    /// Adds a memset node.
217    pub fn add_memset(&mut self, name: &str, size_bytes: usize, value: u8) -> NodeId {
218        let kind = NodeKind::Memset { size_bytes, value };
219        self.graph
220            .add_node(GraphNode::new(NodeId(0), kind).with_name(name))
221    }
222
223    /// Adds an event-record node.
224    pub fn add_event_record(&mut self, name: &str) -> NodeId {
225        self.graph
226            .add_node(GraphNode::new(NodeId(0), NodeKind::EventRecord).with_name(name))
227    }
228
229    /// Adds an event-wait node.
230    pub fn add_event_wait(&mut self, name: &str) -> NodeId {
231        self.graph
232            .add_node(GraphNode::new(NodeId(0), NodeKind::EventWait).with_name(name))
233    }
234
235    /// Adds a barrier (no-op synchronisation) node.
236    pub fn add_barrier(&mut self, name: &str) -> NodeId {
237        self.graph
238            .add_node(GraphNode::new(NodeId(0), NodeKind::Barrier).with_name(name))
239    }
240
241    /// Adds a host-callback node.
242    pub fn add_host_callback(&mut self, name: &str) -> NodeId {
243        let kind = NodeKind::HostCallback {
244            label: name.to_owned(),
245        };
246        self.graph
247            .add_node(GraphNode::new(NodeId(0), kind).with_name(name))
248    }
249
250    /// Adds a raw `GraphNode` (full control over the node).
251    pub fn add_raw(&mut self, node: GraphNode) -> NodeId {
252        self.graph.add_node(node)
253    }
254
255    // -----------------------------------------------------------------------
256    // Convenience: annotate existing nodes
257    // -----------------------------------------------------------------------
258
259    /// Attaches input buffers to an existing node.
260    ///
261    /// Returns `self` for chaining.
262    pub fn set_inputs(
263        &mut self,
264        node: NodeId,
265        inputs: impl IntoIterator<Item = BufferId>,
266    ) -> &mut Self {
267        if let Ok(n) = self.graph.node_mut(node) {
268            n.inputs.extend(inputs);
269        }
270        self
271    }
272
273    /// Attaches output buffers to an existing node.
274    pub fn set_outputs(
275        &mut self,
276        node: NodeId,
277        outputs: impl IntoIterator<Item = BufferId>,
278    ) -> &mut Self {
279        if let Ok(n) = self.graph.node_mut(node) {
280            n.outputs.extend(outputs);
281        }
282        self
283    }
284
285    // -----------------------------------------------------------------------
286    // Dependency edges
287    // -----------------------------------------------------------------------
288
289    /// Adds a dependency edge `from → to`.
290    ///
291    /// Returns `&mut self` for chaining. On error, the builder records the
292    /// error and `build()` will propagate it.
293    pub fn dep(&mut self, from: NodeId, to: NodeId) -> &mut Self {
294        // Errors are propagated lazily via build().
295        let _ = self.graph.add_edge(from, to);
296        self
297    }
298
299    /// Adds a linear dependency chain: `nodes[0] → nodes[1] → … → nodes[n-1]`.
300    pub fn chain(&mut self, nodes: &[NodeId]) -> &mut Self {
301        for w in nodes.windows(2) {
302            self.dep(w[0], w[1]);
303        }
304        self
305    }
306
307    /// Adds "fan-in" edges: all `predecessors` must complete before `join`.
308    pub fn fan_in(&mut self, predecessors: &[NodeId], join: NodeId) -> &mut Self {
309        for &p in predecessors {
310            self.dep(p, join);
311        }
312        self
313    }
314
315    /// Adds "fan-out" edges: `fork` must complete before all `successors` start.
316    pub fn fan_out(&mut self, fork: NodeId, successors: &[NodeId]) -> &mut Self {
317        for &s in successors {
318            self.dep(fork, s);
319        }
320        self
321    }
322
323    // -----------------------------------------------------------------------
324    // Finalise
325    // -----------------------------------------------------------------------
326
327    /// Finalises the builder and returns the assembled `ComputeGraph`.
328    ///
329    /// If `auto_infer_edges` is `true` (the default), data-flow dependency
330    /// edges are inferred from buffer input/output annotations before
331    /// returning.
332    ///
333    /// # Errors
334    ///
335    /// Propagates any `GraphError` accumulated during the build process
336    /// (e.g., cycles introduced by data-flow edge inference).
337    pub fn build(mut self) -> GraphResult<ComputeGraph> {
338        if self.auto_infer_edges {
339            self.graph.infer_data_edges()?;
340        }
341        Ok(self.graph)
342    }
343}
344
345// ---------------------------------------------------------------------------
346// Tests
347// ---------------------------------------------------------------------------
348
349#[cfg(test)]
350mod tests {
351    use super::*;
352    use crate::node::MemcpyDir;
353
354    #[test]
355    fn builder_empty_build() {
356        let b = GraphBuilder::new();
357        let g = b.build().unwrap();
358        assert!(g.is_empty());
359    }
360
361    #[test]
362    fn builder_alloc_buffer() {
363        let mut b = GraphBuilder::new();
364        let buf0 = b.alloc_buffer("input", 1024);
365        let buf1 = b.alloc_buffer("output", 2048);
366        assert_eq!(buf0, BufferId(0));
367        assert_eq!(buf1, BufferId(1));
368        let g = b.build().unwrap();
369        assert_eq!(g.buffer_count(), 2);
370        assert_eq!(g.buffer(buf0).unwrap().size_bytes, 1024);
371    }
372
373    #[test]
374    fn builder_add_kernel_chain() {
375        let mut b = GraphBuilder::new();
376        let k0 = b.add_kernel("k0", 4, 256, 0).finish();
377        let k1 = b.add_kernel("k1", 4, 256, 0).finish();
378        let k2 = b.add_kernel("k2", 4, 256, 0).finish();
379        b.chain(&[k0, k1, k2]);
380        let g = b.build().unwrap();
381        assert_eq!(g.node_count(), 3);
382        assert_eq!(g.edge_count(), 2);
383        assert!(g.is_reachable(k0, k2));
384    }
385
386    #[test]
387    fn builder_kernel_fusible_flag() {
388        let mut b = GraphBuilder::new();
389        let id = b.add_kernel("custom", 1, 32, 0).fusible(false).finish();
390        let g = b.build().unwrap();
391        assert!(!g.node(id).unwrap().kind.is_fusible());
392    }
393
394    #[test]
395    fn builder_add_memcpy() {
396        let mut b = GraphBuilder::new();
397        let up = b.add_memcpy("upload", MemcpyDir::HostToDevice, 4096);
398        let g = b.build().unwrap();
399        assert_eq!(g.node_count(), 1);
400        let node = g.node(up).unwrap();
401        assert!(node.kind.is_memory_op());
402    }
403
404    #[test]
405    fn builder_add_memset() {
406        let mut b = GraphBuilder::new();
407        let ms = b.add_memset("zero", 8192, 0x00);
408        let g = b.build().unwrap();
409        let node = g.node(ms).unwrap();
410        if let NodeKind::Memset { value, .. } = node.kind {
411            assert_eq!(value, 0x00);
412        } else {
413            panic!("expected Memset");
414        }
415    }
416
417    #[test]
418    fn builder_add_barrier_and_event() {
419        let mut b = GraphBuilder::new();
420        let barrier = b.add_barrier("sync");
421        let ev = b.add_event_record("ev0");
422        let ew = b.add_event_wait("ew0");
423        b.chain(&[barrier, ev, ew]);
424        let g = b.build().unwrap();
425        assert_eq!(g.node_count(), 3);
426    }
427
428    #[test]
429    fn builder_fan_in_fan_out() {
430        let mut b = GraphBuilder::new();
431        let src = b.add_barrier("src");
432        let a = b.add_kernel("a", 1, 32, 0).finish();
433        let c = b.add_kernel("c", 1, 32, 0).finish();
434        let d = b.add_kernel("d", 1, 32, 0).finish();
435        let sink = b.add_barrier("sink");
436        b.fan_out(src, &[a, c, d]);
437        b.fan_in(&[a, c, d], sink);
438        let g = b.build().unwrap();
439        assert_eq!(g.edge_count(), 6);
440        assert!(g.is_reachable(src, sink));
441        assert_eq!(g.sources(), vec![src]);
442        assert_eq!(g.sinks(), vec![sink]);
443    }
444
445    #[test]
446    fn builder_dep_returns_self_for_chaining() {
447        let mut b = GraphBuilder::new();
448        let a = b.add_barrier("a");
449        let b_node = b.add_barrier("b");
450        let c = b.add_barrier("c");
451        b.dep(a, b_node).dep(b_node, c);
452        let g = b.build().unwrap();
453        assert_eq!(g.edge_count(), 2);
454    }
455
456    #[test]
457    fn builder_auto_infer_edges_from_buffers() {
458        let mut b = GraphBuilder::new();
459        let buf = b.alloc_buffer("tmp", 512);
460        let writer = b.add_barrier("writer");
461        let reader = b.add_barrier("reader");
462        b.set_outputs(writer, [buf]);
463        b.set_inputs(reader, [buf]);
464        let g = b.build().unwrap(); // auto_infer_edges=true
465        assert!(g.is_reachable(writer, reader));
466    }
467
468    #[test]
469    fn builder_no_auto_infer_edges_disabled() {
470        let mut b = GraphBuilder::new().with_auto_infer_edges(false);
471        let buf = b.alloc_buffer("tmp", 512);
472        let writer = b.add_barrier("writer");
473        let reader = b.add_barrier("reader");
474        b.set_outputs(writer, [buf]);
475        b.set_inputs(reader, [buf]);
476        let g = b.build().unwrap();
477        // No inferred edges, so no reachability unless explicitly added.
478        assert!(!g.is_reachable(writer, reader));
479    }
480
481    #[test]
482    fn builder_add_raw_node() {
483        let mut b = GraphBuilder::new();
484        let n = GraphNode::new(NodeId(0), NodeKind::Barrier)
485            .with_name("raw_barrier")
486            .with_cost(42);
487        let id = b.add_raw(n);
488        let g = b.build().unwrap();
489        assert_eq!(g.node(id).unwrap().cost_hint, 42);
490    }
491
492    #[test]
493    fn builder_add_host_callback() {
494        let mut b = GraphBuilder::new();
495        let cb = b.add_host_callback("sync_point");
496        let g = b.build().unwrap();
497        if let NodeKind::HostCallback { label } = &g.node(cb).unwrap().kind {
498            assert_eq!(label, "sync_point");
499        } else {
500            panic!("expected HostCallback");
501        }
502    }
503
504    #[test]
505    fn builder_set_inputs_and_outputs() {
506        let mut b = GraphBuilder::new();
507        let buf0 = b.alloc_buffer("a", 1);
508        let buf1 = b.alloc_buffer("b", 1);
509        let node = b.add_barrier("n");
510        b.set_inputs(node, [buf0]);
511        b.set_outputs(node, [buf1]);
512        let g = b.build().unwrap();
513        let n = g.node(node).unwrap();
514        assert!(n.inputs.contains(&buf0));
515        assert!(n.outputs.contains(&buf1));
516    }
517
518    #[test]
519    fn builder_add_kernel_with_3d_grid() {
520        let mut b = GraphBuilder::new();
521        let id = b.add_kernel_3d("conv2d", (4, 4, 1), (8, 8, 1), 0);
522        let g = b.build().unwrap();
523        if let NodeKind::KernelLaunch { config, .. } = g.node(id).unwrap().kind {
524            assert_eq!(config.grid, (4, 4, 1));
525            assert_eq!(config.block, (8, 8, 1));
526        } else {
527            panic!("expected KernelLaunch");
528        }
529    }
530
531    #[test]
532    fn builder_alloc_external_buffer() {
533        let mut b = GraphBuilder::new();
534        let id = b.alloc_external_buffer("model_weights", 65536);
535        let g = b.build().unwrap();
536        assert!(g.buffer(id).unwrap().external);
537    }
538
539    #[test]
540    fn builder_kernel_with_buffers_and_cost() {
541        let mut b = GraphBuilder::new();
542        let inp = b.alloc_buffer("input", 4096);
543        let out = b.alloc_buffer("output", 4096);
544        let id = b
545            .add_kernel("scale", 8, 128, 0)
546            .inputs([inp])
547            .outputs([out])
548            .cost(100)
549            .finish();
550        let g = b.build().unwrap();
551        let n = g.node(id).unwrap();
552        assert_eq!(n.cost_hint, 100);
553        assert!(n.inputs.contains(&inp));
554        assert!(n.outputs.contains(&out));
555    }
556}