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().expect("graph builder produces a valid DAG");
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().expect("test graph builds successfully");
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().expect("test graph builds successfully");
369        assert_eq!(g.buffer_count(), 2);
370        assert_eq!(
371            g.buffer(buf0).expect("buf0 registered in graph").size_bytes,
372            1024
373        );
374    }
375
376    #[test]
377    fn builder_add_kernel_chain() {
378        let mut b = GraphBuilder::new();
379        let k0 = b.add_kernel("k0", 4, 256, 0).finish();
380        let k1 = b.add_kernel("k1", 4, 256, 0).finish();
381        let k2 = b.add_kernel("k2", 4, 256, 0).finish();
382        b.chain(&[k0, k1, k2]);
383        let g = b.build().expect("test graph builds successfully");
384        assert_eq!(g.node_count(), 3);
385        assert_eq!(g.edge_count(), 2);
386        assert!(g.is_reachable(k0, k2));
387    }
388
389    #[test]
390    fn builder_kernel_fusible_flag() {
391        let mut b = GraphBuilder::new();
392        let id = b.add_kernel("custom", 1, 32, 0).fusible(false).finish();
393        let g = b.build().expect("test graph builds successfully");
394        assert!(
395            !g.node(id)
396                .expect("node registered in graph")
397                .kind
398                .is_fusible()
399        );
400    }
401
402    #[test]
403    fn builder_add_memcpy() {
404        let mut b = GraphBuilder::new();
405        let up = b.add_memcpy("upload", MemcpyDir::HostToDevice, 4096);
406        let g = b.build().expect("test graph builds successfully");
407        assert_eq!(g.node_count(), 1);
408        let node = g.node(up).expect("memcpy node registered in graph");
409        assert!(node.kind.is_memory_op());
410    }
411
412    #[test]
413    fn builder_add_memset() {
414        let mut b = GraphBuilder::new();
415        let ms = b.add_memset("zero", 8192, 0x00);
416        let g = b.build().expect("test graph builds successfully");
417        let node = g.node(ms).expect("memset node registered in graph");
418        if let NodeKind::Memset { value, .. } = node.kind {
419            assert_eq!(value, 0x00);
420        } else {
421            panic!("expected Memset");
422        }
423    }
424
425    #[test]
426    fn builder_add_barrier_and_event() {
427        let mut b = GraphBuilder::new();
428        let barrier = b.add_barrier("sync");
429        let ev = b.add_event_record("ev0");
430        let ew = b.add_event_wait("ew0");
431        b.chain(&[barrier, ev, ew]);
432        let g = b.build().expect("test graph builds successfully");
433        assert_eq!(g.node_count(), 3);
434    }
435
436    #[test]
437    fn builder_fan_in_fan_out() {
438        let mut b = GraphBuilder::new();
439        let src = b.add_barrier("src");
440        let a = b.add_kernel("a", 1, 32, 0).finish();
441        let c = b.add_kernel("c", 1, 32, 0).finish();
442        let d = b.add_kernel("d", 1, 32, 0).finish();
443        let sink = b.add_barrier("sink");
444        b.fan_out(src, &[a, c, d]);
445        b.fan_in(&[a, c, d], sink);
446        let g = b.build().expect("test graph builds successfully");
447        assert_eq!(g.edge_count(), 6);
448        assert!(g.is_reachable(src, sink));
449        assert_eq!(g.sources(), vec![src]);
450        assert_eq!(g.sinks(), vec![sink]);
451    }
452
453    #[test]
454    fn builder_dep_returns_self_for_chaining() {
455        let mut b = GraphBuilder::new();
456        let a = b.add_barrier("a");
457        let b_node = b.add_barrier("b");
458        let c = b.add_barrier("c");
459        b.dep(a, b_node).dep(b_node, c);
460        let g = b.build().expect("test graph builds successfully");
461        assert_eq!(g.edge_count(), 2);
462    }
463
464    #[test]
465    fn builder_auto_infer_edges_from_buffers() {
466        let mut b = GraphBuilder::new();
467        let buf = b.alloc_buffer("tmp", 512);
468        let writer = b.add_barrier("writer");
469        let reader = b.add_barrier("reader");
470        b.set_outputs(writer, [buf]);
471        b.set_inputs(reader, [buf]);
472        let g = b.build().expect("test graph builds successfully"); // auto_infer_edges=true
473        assert!(g.is_reachable(writer, reader));
474    }
475
476    #[test]
477    fn builder_no_auto_infer_edges_disabled() {
478        let mut b = GraphBuilder::new().with_auto_infer_edges(false);
479        let buf = b.alloc_buffer("tmp", 512);
480        let writer = b.add_barrier("writer");
481        let reader = b.add_barrier("reader");
482        b.set_outputs(writer, [buf]);
483        b.set_inputs(reader, [buf]);
484        let g = b.build().expect("test graph builds successfully");
485        // No inferred edges, so no reachability unless explicitly added.
486        assert!(!g.is_reachable(writer, reader));
487    }
488
489    #[test]
490    fn builder_add_raw_node() {
491        let mut b = GraphBuilder::new();
492        let n = GraphNode::new(NodeId(0), NodeKind::Barrier)
493            .with_name("raw_barrier")
494            .with_cost(42);
495        let id = b.add_raw(n);
496        let g = b.build().expect("test graph builds successfully");
497        assert_eq!(g.node(id).expect("node registered in graph").cost_hint, 42);
498    }
499
500    #[test]
501    fn builder_add_host_callback() {
502        let mut b = GraphBuilder::new();
503        let cb = b.add_host_callback("sync_point");
504        let g = b.build().expect("test graph builds successfully");
505        if let NodeKind::HostCallback { label } =
506            &g.node(cb).expect("callback node registered in graph").kind
507        {
508            assert_eq!(label, "sync_point");
509        } else {
510            panic!("expected HostCallback");
511        }
512    }
513
514    #[test]
515    fn builder_set_inputs_and_outputs() {
516        let mut b = GraphBuilder::new();
517        let buf0 = b.alloc_buffer("a", 1);
518        let buf1 = b.alloc_buffer("b", 1);
519        let node = b.add_barrier("n");
520        b.set_inputs(node, [buf0]);
521        b.set_outputs(node, [buf1]);
522        let g = b.build().expect("test graph builds successfully");
523        let n = g.node(node).expect("barrier node registered in graph");
524        assert!(n.inputs.contains(&buf0));
525        assert!(n.outputs.contains(&buf1));
526    }
527
528    #[test]
529    fn builder_add_kernel_with_3d_grid() {
530        let mut b = GraphBuilder::new();
531        let id = b.add_kernel_3d("conv2d", (4, 4, 1), (8, 8, 1), 0);
532        let g = b.build().expect("test graph builds successfully");
533        if let NodeKind::KernelLaunch { config, .. } =
534            g.node(id).expect("kernel node registered in graph").kind
535        {
536            assert_eq!(config.grid, (4, 4, 1));
537            assert_eq!(config.block, (8, 8, 1));
538        } else {
539            panic!("expected KernelLaunch");
540        }
541    }
542
543    #[test]
544    fn builder_alloc_external_buffer() {
545        let mut b = GraphBuilder::new();
546        let id = b.alloc_external_buffer("model_weights", 65536);
547        let g = b.build().expect("test graph builds successfully");
548        assert!(
549            g.buffer(id)
550                .expect("external buffer registered in graph")
551                .external
552        );
553    }
554
555    #[test]
556    fn builder_kernel_with_buffers_and_cost() {
557        let mut b = GraphBuilder::new();
558        let inp = b.alloc_buffer("input", 4096);
559        let out = b.alloc_buffer("output", 4096);
560        let id = b
561            .add_kernel("scale", 8, 128, 0)
562            .inputs([inp])
563            .outputs([out])
564            .cost(100)
565            .finish();
566        let g = b.build().expect("test graph builds successfully");
567        let n = g.node(id).expect("node registered in graph");
568        assert_eq!(n.cost_hint, 100);
569        assert!(n.inputs.contains(&inp));
570        assert!(n.outputs.contains(&out));
571    }
572}