Skip to main content

oxicuda_graph/executor/
sequential.rs

1//! Sequential CPU-side executor for the `ExecutionPlan`.
2//!
3//! The sequential executor walks the plan steps in submission order on a
4//! single thread, simulating the logical execution without a GPU. It is
5//! primarily used for:
6//!
7//! * Unit testing of the compilation pipeline on machines without a GPU.
8//! * Correctness checking: the executor validates that all event waits have
9//!   a matching prior record on the correct stream.
10//! * Profiling: it counts operations by type for performance modelling.
11//!
12//! # Event model
13//!
14//! The sequential executor simulates events using a simple boolean set.
15//! `EventRecord(eid)` marks event `eid` as "fired". `EventWait(eid)` checks
16//! that the event has been fired; if it has not, an error is returned.
17//!
18//! Because the sequential executor runs in a single-threaded, deterministic
19//! order, all `EventRecord` steps necessarily precede the corresponding
20//! `EventWait` steps if the plan was built correctly.
21
22use std::collections::HashSet;
23
24use crate::error::{GraphError, GraphResult};
25use crate::executor::plan::{ExecutionPlan, PlanStep};
26use crate::node::StreamId;
27
28// ---------------------------------------------------------------------------
29// ExecutionStats
30// ---------------------------------------------------------------------------
31
32/// Statistics collected during a sequential execution run.
33#[derive(Debug, Clone, Default)]
34pub struct ExecutionStats {
35    /// Number of kernel launches executed (after fusion).
36    pub kernels_launched: usize,
37    /// Total bytes copied (memcpy steps).
38    pub bytes_copied: usize,
39    /// Total bytes set (memset steps).
40    pub bytes_set: usize,
41    /// Number of event records issued.
42    pub events_recorded: usize,
43    /// Number of event waits satisfied.
44    pub events_waited: usize,
45    /// Number of host callbacks invoked.
46    pub host_callbacks: usize,
47    /// Number of barrier steps.
48    pub barriers: usize,
49    /// Number of steps that ran on stream 0.
50    pub steps_on_stream0: usize,
51    /// Number of steps that ran on non-zero streams.
52    pub steps_on_other_streams: usize,
53}
54
55impl ExecutionStats {
56    /// Total steps executed.
57    pub fn total_steps(&self) -> usize {
58        self.kernels_launched
59            + self.events_recorded
60            + self.events_waited
61            + self.host_callbacks
62            + self.barriers
63            // memcpy and memset are subsumed into bytes_copied / bytes_set;
64            // count separately:
65            + self.steps_on_stream0
66            + self.steps_on_other_streams
67    }
68}
69
70// ---------------------------------------------------------------------------
71// SequentialExecutor
72// ---------------------------------------------------------------------------
73
74/// Simulates execution of an [`ExecutionPlan`] sequentially on the CPU.
75///
76/// Instantiate with a plan, then call [`run`](Self::run) to execute it.
77pub struct SequentialExecutor<'a> {
78    plan: &'a ExecutionPlan,
79}
80
81impl<'a> SequentialExecutor<'a> {
82    /// Creates a new executor for the given plan.
83    pub fn new(plan: &'a ExecutionPlan) -> Self {
84        Self { plan }
85    }
86
87    /// Executes all steps in submission order.
88    ///
89    /// Returns [`ExecutionStats`] summarising what was executed.
90    ///
91    /// # Errors
92    ///
93    /// * [`GraphError::InvalidPlan`] if an `EventWait` references an event
94    ///   that has not yet been recorded.
95    pub fn run(&self) -> GraphResult<ExecutionStats> {
96        let mut stats = ExecutionStats::default();
97        let mut fired_events: HashSet<usize> = HashSet::new();
98        let mut memcpy_bytes = 0usize;
99        let mut memset_bytes = 0usize;
100
101        for step in &self.plan.steps {
102            // Track stream statistics.
103            match step.stream() {
104                StreamId(0) => stats.steps_on_stream0 += 1,
105                _ => stats.steps_on_other_streams += 1,
106            }
107
108            match step {
109                PlanStep::KernelLaunch { .. } => {
110                    stats.kernels_launched += 1;
111                }
112                PlanStep::Memcpy { size_bytes, .. } => {
113                    memcpy_bytes += size_bytes;
114                }
115                PlanStep::Memset { size_bytes, .. } => {
116                    memset_bytes += size_bytes;
117                }
118                PlanStep::EventRecord { event_id, .. } => {
119                    fired_events.insert(*event_id);
120                    stats.events_recorded += 1;
121                }
122                PlanStep::EventWait { event_id, stream } => {
123                    if !fired_events.contains(event_id) {
124                        return Err(GraphError::InvalidPlan(format!(
125                            "EventWait for event {event_id} on stream {stream} but event was never recorded"
126                        )));
127                    }
128                    stats.events_waited += 1;
129                }
130                PlanStep::HostCallback { .. } => {
131                    stats.host_callbacks += 1;
132                }
133                PlanStep::Barrier { .. } => {
134                    stats.barriers += 1;
135                }
136            }
137        }
138
139        stats.bytes_copied = memcpy_bytes;
140        stats.bytes_set = memset_bytes;
141
142        Ok(stats)
143    }
144
145    /// Validates the plan without executing it.
146    ///
147    /// Checks that:
148    /// * All `EventWait`s have a preceding `EventRecord` with the same ID.
149    /// * No duplicate event records exist (warns; does not error).
150    ///
151    /// Returns the number of validation issues found.
152    pub fn validate(&self) -> GraphResult<usize> {
153        let mut recorded: HashSet<usize> = HashSet::new();
154        let mut issues = 0usize;
155        for step in &self.plan.steps {
156            match step {
157                PlanStep::EventRecord { event_id, .. } if !recorded.insert(*event_id) => {
158                    // Duplicate record is unusual but not fatal.
159                    issues += 1;
160                }
161                PlanStep::EventRecord { .. } => {}
162                PlanStep::EventWait { event_id, stream } if !recorded.contains(event_id) => {
163                    return Err(GraphError::InvalidPlan(format!(
164                        "EventWait({event_id}) on stream {stream} has no prior EventRecord"
165                    )));
166                }
167                PlanStep::EventWait { .. } => {}
168                _ => {}
169            }
170        }
171        Ok(issues)
172    }
173}
174
175// ---------------------------------------------------------------------------
176// Tests
177// ---------------------------------------------------------------------------
178
179#[cfg(test)]
180mod tests {
181    use super::*;
182    use crate::builder::GraphBuilder;
183    use crate::executor::plan::ExecutionPlan;
184    use crate::graph::ComputeGraph;
185    use crate::node::MemcpyDir;
186
187    fn simple_chain_graph() -> ComputeGraph {
188        let mut b = GraphBuilder::new().with_auto_infer_edges(false);
189        let up = b.add_memcpy("up", MemcpyDir::HostToDevice, 2048);
190        let k = b.add_kernel("k", 4, 256, 0).fusible(false).finish();
191        let dn = b.add_memcpy("dn", MemcpyDir::DeviceToHost, 2048);
192        b.chain(&[up, k, dn]);
193        b.build().unwrap()
194    }
195
196    fn build_and_execute(graph: &ComputeGraph) -> GraphResult<ExecutionStats> {
197        let plan = ExecutionPlan::build(graph, 4)?;
198        SequentialExecutor::new(&plan).run()
199    }
200
201    #[test]
202    fn seq_simple_chain_runs() {
203        let g = simple_chain_graph();
204        let stats = build_and_execute(&g).unwrap();
205        assert_eq!(stats.kernels_launched, 1);
206        assert_eq!(stats.bytes_copied, 2048 * 2);
207    }
208
209    #[test]
210    fn seq_memset_counted() {
211        let mut b = GraphBuilder::new().with_auto_infer_edges(false);
212        b.add_memset("zero", 8192, 0x00);
213        let g = b.build().unwrap();
214        let stats = build_and_execute(&g).unwrap();
215        assert_eq!(stats.bytes_set, 8192);
216    }
217
218    #[test]
219    fn seq_barrier_counted() {
220        let mut b = GraphBuilder::new().with_auto_infer_edges(false);
221        b.add_barrier("sync");
222        let g = b.build().unwrap();
223        let stats = build_and_execute(&g).unwrap();
224        assert_eq!(stats.barriers, 1);
225    }
226
227    #[test]
228    fn seq_host_callback_counted() {
229        let mut b = GraphBuilder::new().with_auto_infer_edges(false);
230        b.add_host_callback("checkpoint");
231        let g = b.build().unwrap();
232        let stats = build_and_execute(&g).unwrap();
233        assert_eq!(stats.host_callbacks, 1);
234    }
235
236    #[test]
237    fn seq_valid_event_record_wait() {
238        // Manually build a plan with a record before a wait.
239        let steps = vec![
240            PlanStep::EventRecord {
241                event_id: 0,
242                stream: StreamId(0),
243            },
244            PlanStep::EventWait {
245                event_id: 0,
246                stream: StreamId(1),
247            },
248        ];
249        let plan = ExecutionPlan {
250            steps,
251            num_streams: 2,
252            pool_bytes: 0,
253            kernel_count_original: 0,
254            kernel_count_fused: 0,
255            event_count: 1,
256        };
257        let stats = SequentialExecutor::new(&plan).run().unwrap();
258        assert_eq!(stats.events_recorded, 1);
259        assert_eq!(stats.events_waited, 1);
260    }
261
262    #[test]
263    fn seq_event_wait_without_record_fails() {
264        let steps = vec![PlanStep::EventWait {
265            event_id: 99,
266            stream: StreamId(0),
267        }];
268        let plan = ExecutionPlan {
269            steps,
270            num_streams: 1,
271            pool_bytes: 0,
272            kernel_count_original: 0,
273            kernel_count_fused: 0,
274            event_count: 1,
275        };
276        let result = SequentialExecutor::new(&plan).run();
277        assert!(matches!(result, Err(GraphError::InvalidPlan(_))));
278    }
279
280    #[test]
281    fn seq_validate_ok() {
282        let steps = vec![
283            PlanStep::EventRecord {
284                event_id: 0,
285                stream: StreamId(0),
286            },
287            PlanStep::EventWait {
288                event_id: 0,
289                stream: StreamId(1),
290            },
291        ];
292        let plan = ExecutionPlan {
293            steps,
294            num_streams: 2,
295            pool_bytes: 0,
296            kernel_count_original: 0,
297            kernel_count_fused: 0,
298            event_count: 1,
299        };
300        let issues = SequentialExecutor::new(&plan).validate().unwrap();
301        assert_eq!(issues, 0);
302    }
303
304    #[test]
305    fn seq_validate_missing_record_fails() {
306        let steps = vec![PlanStep::EventWait {
307            event_id: 5,
308            stream: StreamId(0),
309        }];
310        let plan = ExecutionPlan {
311            steps,
312            num_streams: 1,
313            pool_bytes: 0,
314            kernel_count_original: 0,
315            kernel_count_fused: 0,
316            event_count: 1,
317        };
318        assert!(matches!(
319            SequentialExecutor::new(&plan).validate(),
320            Err(GraphError::InvalidPlan(_))
321        ));
322    }
323
324    #[test]
325    fn seq_stream0_step_counted() {
326        let steps = vec![PlanStep::Barrier {
327            node: crate::node::NodeId(0),
328            stream: StreamId(0),
329        }];
330        let plan = ExecutionPlan {
331            steps,
332            num_streams: 1,
333            pool_bytes: 0,
334            kernel_count_original: 0,
335            kernel_count_fused: 0,
336            event_count: 0,
337        };
338        let stats = SequentialExecutor::new(&plan).run().unwrap();
339        assert_eq!(stats.steps_on_stream0, 1);
340        assert_eq!(stats.steps_on_other_streams, 0);
341    }
342
343    #[test]
344    fn seq_other_stream_step_counted() {
345        let steps = vec![PlanStep::Barrier {
346            node: crate::node::NodeId(0),
347            stream: StreamId(2),
348        }];
349        let plan = ExecutionPlan {
350            steps,
351            num_streams: 3,
352            pool_bytes: 0,
353            kernel_count_original: 0,
354            kernel_count_fused: 0,
355            event_count: 0,
356        };
357        let stats = SequentialExecutor::new(&plan).run().unwrap();
358        assert_eq!(stats.steps_on_other_streams, 1);
359    }
360
361    #[test]
362    fn seq_multiple_kernels_fused_counted() {
363        // Fusible chain → fused to 1 kernel.
364        let mut b = GraphBuilder::new().with_auto_infer_edges(false);
365        let k0 = b.add_kernel("add", 4, 256, 0).fusible(true).finish();
366        let k1 = b.add_kernel("relu", 4, 256, 0).fusible(true).finish();
367        let k2 = b.add_kernel("scale", 4, 256, 0).fusible(true).finish();
368        b.chain(&[k0, k1, k2]);
369        let g = b.build().unwrap();
370        let plan = ExecutionPlan::build(&g, 1).unwrap();
371        let stats = SequentialExecutor::new(&plan).run().unwrap();
372        // After fusion: 1 kernel launch.
373        assert_eq!(stats.kernels_launched, 1);
374    }
375}