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().expect("test graph builds successfully")
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).expect("sequential execution of valid graph succeeds");
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().expect("test graph builds successfully");
214        let stats = build_and_execute(&g).expect("sequential execution of valid graph succeeds");
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().expect("test graph builds successfully");
223        let stats = build_and_execute(&g).expect("sequential execution of valid graph succeeds");
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().expect("test graph builds successfully");
232        let stats = build_and_execute(&g).expect("sequential execution of valid graph succeeds");
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)
258            .run()
259            .expect("sequential executor runs a valid plan");
260        assert_eq!(stats.events_recorded, 1);
261        assert_eq!(stats.events_waited, 1);
262    }
263
264    #[test]
265    fn seq_event_wait_without_record_fails() {
266        let steps = vec![PlanStep::EventWait {
267            event_id: 99,
268            stream: StreamId(0),
269        }];
270        let plan = ExecutionPlan {
271            steps,
272            num_streams: 1,
273            pool_bytes: 0,
274            kernel_count_original: 0,
275            kernel_count_fused: 0,
276            event_count: 1,
277        };
278        let result = SequentialExecutor::new(&plan).run();
279        assert!(matches!(result, Err(GraphError::InvalidPlan(_))));
280    }
281
282    #[test]
283    fn seq_validate_ok() {
284        let steps = vec![
285            PlanStep::EventRecord {
286                event_id: 0,
287                stream: StreamId(0),
288            },
289            PlanStep::EventWait {
290                event_id: 0,
291                stream: StreamId(1),
292            },
293        ];
294        let plan = ExecutionPlan {
295            steps,
296            num_streams: 2,
297            pool_bytes: 0,
298            kernel_count_original: 0,
299            kernel_count_fused: 0,
300            event_count: 1,
301        };
302        let issues = SequentialExecutor::new(&plan)
303            .validate()
304            .expect("plan validation succeeds on well-formed plan");
305        assert_eq!(issues, 0);
306    }
307
308    #[test]
309    fn seq_validate_missing_record_fails() {
310        let steps = vec![PlanStep::EventWait {
311            event_id: 5,
312            stream: StreamId(0),
313        }];
314        let plan = ExecutionPlan {
315            steps,
316            num_streams: 1,
317            pool_bytes: 0,
318            kernel_count_original: 0,
319            kernel_count_fused: 0,
320            event_count: 1,
321        };
322        assert!(matches!(
323            SequentialExecutor::new(&plan).validate(),
324            Err(GraphError::InvalidPlan(_))
325        ));
326    }
327
328    #[test]
329    fn seq_stream0_step_counted() {
330        let steps = vec![PlanStep::Barrier {
331            node: crate::node::NodeId(0),
332            stream: StreamId(0),
333        }];
334        let plan = ExecutionPlan {
335            steps,
336            num_streams: 1,
337            pool_bytes: 0,
338            kernel_count_original: 0,
339            kernel_count_fused: 0,
340            event_count: 0,
341        };
342        let stats = SequentialExecutor::new(&plan)
343            .run()
344            .expect("sequential executor runs a valid plan");
345        assert_eq!(stats.steps_on_stream0, 1);
346        assert_eq!(stats.steps_on_other_streams, 0);
347    }
348
349    #[test]
350    fn seq_other_stream_step_counted() {
351        let steps = vec![PlanStep::Barrier {
352            node: crate::node::NodeId(0),
353            stream: StreamId(2),
354        }];
355        let plan = ExecutionPlan {
356            steps,
357            num_streams: 3,
358            pool_bytes: 0,
359            kernel_count_original: 0,
360            kernel_count_fused: 0,
361            event_count: 0,
362        };
363        let stats = SequentialExecutor::new(&plan)
364            .run()
365            .expect("sequential executor runs a valid plan");
366        assert_eq!(stats.steps_on_other_streams, 1);
367    }
368
369    #[test]
370    fn seq_multiple_kernels_fused_counted() {
371        // Fusible chain → fused to 1 kernel.
372        let mut b = GraphBuilder::new().with_auto_infer_edges(false);
373        let k0 = b.add_kernel("add", 4, 256, 0).fusible(true).finish();
374        let k1 = b.add_kernel("relu", 4, 256, 0).fusible(true).finish();
375        let k2 = b.add_kernel("scale", 4, 256, 0).fusible(true).finish();
376        b.chain(&[k0, k1, k2]);
377        let g = b.build().expect("test graph builds successfully");
378        let plan = ExecutionPlan::build(&g, 1).expect("execution plan builds from valid graph");
379        let stats = SequentialExecutor::new(&plan)
380            .run()
381            .expect("sequential executor runs a valid plan");
382        // After fusion: 1 kernel launch.
383        assert_eq!(stats.kernels_launched, 1);
384    }
385}