Skip to main content

nxpu_opt/
schedule.rs

1//! Operation scheduling pass.
2//!
3//! Implements list scheduling with critical-path priority to determine
4//! an efficient execution order for operations in a function body.
5//! Identifies parallel execution opportunities by grouping independent
6//! operations into time slots.
7
8use std::fmt;
9
10use nxpu_analysis::DataflowGraph;
11use nxpu_ir::Module;
12
13use crate::Pass;
14
15/// A time slot in a schedule, containing one or more operations
16/// that can execute concurrently.
17#[derive(Clone, Debug)]
18pub struct ScheduleSlot {
19    /// The time step (0-indexed).
20    pub time: usize,
21    /// Node IDs of operations scheduled in this slot.
22    pub ops: Vec<usize>,
23}
24
25/// A complete schedule for a function, mapping operations to time slots.
26#[derive(Clone, Debug)]
27pub struct Schedule {
28    /// Ordered time slots.
29    pub slots: Vec<ScheduleSlot>,
30}
31
32impl Schedule {
33    /// Returns the total number of time steps in the schedule.
34    pub fn total_time(&self) -> usize {
35        self.slots.len()
36    }
37
38    /// Returns the total number of scheduled operations.
39    pub fn total_ops(&self) -> usize {
40        self.slots.iter().map(|s| s.ops.len()).sum()
41    }
42
43    /// Returns the maximum parallelism (most ops in a single slot).
44    pub fn max_parallelism(&self) -> usize {
45        self.slots.iter().map(|s| s.ops.len()).max().unwrap_or(0)
46    }
47
48    /// Returns `true` if the schedule respects all dependency edges in the DFG.
49    ///
50    /// For every edge `from -> to`, the time of `from` must be strictly less
51    /// than the time of `to`.
52    pub fn is_valid(&self, dfg: &DataflowGraph) -> bool {
53        // Build a map from node_id to time slot.
54        let mut node_time: Vec<Option<usize>> = vec![None; dfg.node_count()];
55        for slot in &self.slots {
56            for &op in &slot.ops {
57                if op < node_time.len() {
58                    node_time[op] = Some(slot.time);
59                }
60            }
61        }
62
63        // Every edge must have from.time < to.time.
64        for edge in dfg.edges() {
65            let from_time = match node_time.get(edge.from) {
66                Some(Some(t)) => *t,
67                _ => return false,
68            };
69            let to_time = match node_time.get(edge.to) {
70                Some(Some(t)) => *t,
71                _ => return false,
72            };
73            if from_time >= to_time {
74                return false;
75            }
76        }
77
78        true
79    }
80}
81
82impl fmt::Display for Schedule {
83    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
84        writeln!(
85            f,
86            "Schedule ({} slots, {} ops):",
87            self.total_time(),
88            self.total_ops()
89        )?;
90        for slot in &self.slots {
91            writeln!(f, "  t={}: {:?}", slot.time, slot.ops)?;
92        }
93        Ok(())
94    }
95}
96
97/// Perform list scheduling on a dataflow graph.
98///
99/// Uses critical-path-based priority: nodes on or near the critical path
100/// are scheduled first. This is a greedy heuristic that works well for
101/// many practical cases.
102///
103/// # Algorithm
104///
105/// 1. Compute critical path distances for all nodes.
106/// 2. Initialize ready set with nodes that have no predecessors.
107/// 3. At each time step, select the highest-priority ready node(s).
108/// 4. After scheduling a node, add newly ready successors.
109/// 5. Continue until all nodes are scheduled.
110pub fn list_schedule(dfg: &DataflowGraph) -> Schedule {
111    let n = dfg.node_count();
112    if n == 0 {
113        return Schedule { slots: Vec::new() };
114    }
115
116    // Compute critical path priorities (higher distance = higher priority).
117    let cp_result = dfg.critical_path();
118    let priority = &cp_result.node_distances;
119
120    // Build successor and predecessor lists.
121    let mut preds_count = vec![0usize; n];
122    let mut successors: Vec<Vec<usize>> = vec![Vec::new(); n];
123
124    for edge in dfg.edges() {
125        preds_count[edge.to] += 1;
126        successors[edge.from].push(edge.to);
127    }
128
129    // Track remaining predecessor count for each node.
130    let mut remaining_preds = preds_count.clone();
131
132    // Initialize ready set (nodes with no predecessors).
133    let mut ready: Vec<usize> = Vec::new();
134    for (i, &pred_count) in remaining_preds.iter().enumerate() {
135        if pred_count == 0 {
136            ready.push(i);
137        }
138    }
139
140    let mut slots: Vec<ScheduleSlot> = Vec::new();
141    let mut scheduled = vec![false; n];
142    let mut time = 0;
143
144    while !ready.is_empty() {
145        // Sort ready nodes by priority (descending: higher priority first).
146        ready.sort_by(|&a, &b| {
147            priority
148                .get(b)
149                .unwrap_or(&0)
150                .cmp(priority.get(a).unwrap_or(&0))
151                .then_with(|| a.cmp(&b)) // tie-break by id for determinism
152        });
153
154        // All ready nodes can execute in parallel in this time slot.
155        let slot_ops: Vec<usize> = std::mem::take(&mut ready);
156
157        for &op in &slot_ops {
158            scheduled[op] = true;
159        }
160
161        // Find newly ready successors.
162        let mut next_ready: Vec<usize> = Vec::new();
163        for &op in &slot_ops {
164            for &succ in &successors[op] {
165                remaining_preds[succ] -= 1;
166                if remaining_preds[succ] == 0 && !scheduled[succ] && !next_ready.contains(&succ) {
167                    next_ready.push(succ);
168                }
169            }
170        }
171
172        slots.push(ScheduleSlot {
173            time,
174            ops: slot_ops,
175        });
176
177        ready = next_ready;
178        time += 1;
179    }
180
181    Schedule { slots }
182}
183
184/// Convenience function: build DFG and schedule for an IR function.
185pub fn schedule_function(func: &nxpu_ir::Function) -> (DataflowGraph, Schedule) {
186    let dfg = DataflowGraph::build(func);
187    let schedule = list_schedule(&dfg);
188    (dfg, schedule)
189}
190
191/// An optimization pass that computes and logs the schedule for debugging.
192///
193/// This pass does NOT modify the module. It only computes and logs schedules
194/// for all entry points when the `RUST_LOG` level includes debug messages.
195/// It can also be used to attach schedule metadata.
196#[derive(Debug)]
197pub struct SchedulePass;
198
199impl Pass for SchedulePass {
200    fn name(&self) -> &str {
201        "schedule"
202    }
203
204    fn run(&self, module: &mut Module) -> bool {
205        for ep in &module.entry_points {
206            let (dfg, schedule) = schedule_function(&ep.function);
207            let cp = dfg.critical_path();
208            log::debug!(
209                "entry point '{}': {} nodes, {} edges, critical path length {}, schedule: {} slots",
210                ep.name,
211                dfg.node_count(),
212                dfg.edge_count(),
213                cp.critical_path_length,
214                schedule.total_time(),
215            );
216        }
217        // This pass is analysis-only; it does not modify the module.
218        false
219    }
220}
221
222/// Compute schedules for all entry points in a module.
223///
224/// Returns a list of `(entry_point_name, DataflowGraph, Schedule)` triples.
225pub fn compute_schedules(module: &Module) -> Vec<(String, DataflowGraph, Schedule)> {
226    module
227        .entry_points
228        .iter()
229        .map(|ep| {
230            let (dfg, schedule) = schedule_function(&ep.function);
231            (ep.name.clone(), dfg, schedule)
232        })
233        .collect()
234}
235
236/// Format a schedule for human-readable output (used by --emit-schedule).
237pub fn format_schedule(name: &str, dfg: &DataflowGraph, schedule: &Schedule) -> String {
238    let cp = dfg.critical_path();
239    let mut out = String::new();
240
241    out.push_str(&format!("=== Schedule for '{}' ===\n", name));
242    out.push_str(&format!(
243        "Nodes: {}, Edges: {}, Critical path: {}\n",
244        dfg.node_count(),
245        dfg.edge_count(),
246        cp.critical_path_length,
247    ));
248    out.push_str(&format!(
249        "Time slots: {}, Max parallelism: {}\n\n",
250        schedule.total_time(),
251        schedule.max_parallelism(),
252    ));
253
254    for slot in &schedule.slots {
255        out.push_str(&format!("  t={}: ", slot.time));
256        let descs: Vec<String> = slot
257            .ops
258            .iter()
259            .map(|&id| {
260                let node = &dfg.nodes()[id];
261                format!("node[{}] {}", id, node.kind)
262            })
263            .collect();
264        out.push_str(&descs.join(", "));
265        out.push('\n');
266    }
267
268    if !cp.critical_path.is_empty() {
269        out.push_str("\n  Critical path: ");
270        let path_strs: Vec<String> = cp
271            .critical_path
272            .iter()
273            .map(|&id| format!("{}", id))
274            .collect();
275        out.push_str(&path_strs.join(" -> "));
276        out.push('\n');
277    }
278
279    out
280}
281
282#[cfg(test)]
283mod tests {
284    use super::*;
285    use nxpu_ir::{Expression, Function, Literal, Statement};
286
287    fn dummy_gv_handle() -> nxpu_ir::Handle<nxpu_ir::GlobalVariable> {
288        let mut arena = nxpu_ir::Arena::new();
289        arena.append(nxpu_ir::GlobalVariable {
290            name: None,
291            space: nxpu_ir::AddressSpace::Storage {
292                access: nxpu_ir::StorageAccess::LOAD | nxpu_ir::StorageAccess::STORE,
293            },
294            binding: None,
295            ty: {
296                let mut types = nxpu_ir::UniqueArena::new();
297                types.insert(nxpu_ir::Type {
298                    name: None,
299                    inner: nxpu_ir::TypeInner::Scalar(nxpu_ir::Scalar::F32),
300                })
301            },
302            init: None,
303            layout: None,
304        })
305    }
306
307    #[test]
308    fn schedule_empty_function() {
309        let func = Function::new("test");
310        let (dfg, schedule) = schedule_function(&func);
311        assert_eq!(dfg.node_count(), 0);
312        assert_eq!(schedule.total_time(), 0);
313        assert_eq!(schedule.total_ops(), 0);
314        assert_eq!(schedule.max_parallelism(), 0);
315    }
316
317    #[test]
318    fn schedule_single_statement() {
319        let mut func = Function::new("test");
320        let gv = dummy_gv_handle();
321        let ptr = func.expressions.append(Expression::GlobalVariable(gv));
322        let val = func
323            .expressions
324            .append(Expression::Literal(Literal::F32(1.0)));
325        func.body.push(Statement::Store {
326            pointer: ptr,
327            value: val,
328        });
329
330        let (dfg, schedule) = schedule_function(&func);
331        assert_eq!(dfg.node_count(), 1);
332        assert_eq!(schedule.total_time(), 1);
333        assert_eq!(schedule.total_ops(), 1);
334    }
335
336    #[test]
337    fn schedule_dependent_chain() {
338        let mut func = Function::new("test");
339        let gv = dummy_gv_handle();
340        let ptr = func.expressions.append(Expression::GlobalVariable(gv));
341        let val = func
342            .expressions
343            .append(Expression::Literal(Literal::F32(1.0)));
344
345        // Three stores to the same pointer (WAW dependency chain).
346        func.body.push(Statement::Store {
347            pointer: ptr,
348            value: val,
349        });
350        func.body.push(Statement::Store {
351            pointer: ptr,
352            value: val,
353        });
354        func.body.push(Statement::Store {
355            pointer: ptr,
356            value: val,
357        });
358
359        let (dfg, schedule) = schedule_function(&func);
360        assert_eq!(dfg.node_count(), 3);
361        // Must be serialized due to WAW dependencies.
362        assert_eq!(schedule.total_time(), 3);
363        assert!(
364            schedule.is_valid(&dfg),
365            "schedule must respect dependencies"
366        );
367    }
368
369    #[test]
370    fn schedule_independent_ops_parallel() {
371        let mut func = Function::new("test");
372
373        // Two stores to different pointers with different values.
374        let gv0 = dummy_gv_handle();
375        let gv1 = {
376            let mut arena = nxpu_ir::Arena::new();
377            let _ = arena.append(nxpu_ir::GlobalVariable {
378                name: None,
379                space: nxpu_ir::AddressSpace::Storage {
380                    access: nxpu_ir::StorageAccess::STORE,
381                },
382                binding: None,
383                ty: {
384                    let mut types = nxpu_ir::UniqueArena::new();
385                    types.insert(nxpu_ir::Type {
386                        name: None,
387                        inner: nxpu_ir::TypeInner::Scalar(nxpu_ir::Scalar::F32),
388                    })
389                },
390                init: None,
391                layout: None,
392            });
393            arena.append(nxpu_ir::GlobalVariable {
394                name: None,
395                space: nxpu_ir::AddressSpace::Storage {
396                    access: nxpu_ir::StorageAccess::STORE,
397                },
398                binding: None,
399                ty: {
400                    let mut types = nxpu_ir::UniqueArena::new();
401                    types.insert(nxpu_ir::Type {
402                        name: None,
403                        inner: nxpu_ir::TypeInner::Scalar(nxpu_ir::Scalar::F32),
404                    })
405                },
406                init: None,
407                layout: None,
408            })
409        };
410
411        let ptr_a = func.expressions.append(Expression::GlobalVariable(gv0));
412        let val_a = func
413            .expressions
414            .append(Expression::Literal(Literal::F32(1.0)));
415        let ptr_b = func.expressions.append(Expression::GlobalVariable(gv1));
416        let val_b = func
417            .expressions
418            .append(Expression::Literal(Literal::F32(2.0)));
419
420        func.body.push(Statement::Store {
421            pointer: ptr_a,
422            value: val_a,
423        });
424        func.body.push(Statement::Store {
425            pointer: ptr_b,
426            value: val_b,
427        });
428
429        let (dfg, schedule) = schedule_function(&func);
430        assert_eq!(dfg.node_count(), 2);
431        // Independent ops should be in the same time slot.
432        assert!(
433            schedule.max_parallelism() >= 2,
434            "expected parallel execution of independent stores"
435        );
436        assert!(
437            schedule.is_valid(&dfg),
438            "schedule must respect dependencies"
439        );
440    }
441
442    #[test]
443    fn schedule_respects_barrier() {
444        let mut func = Function::new("test");
445        let gv = dummy_gv_handle();
446        let ptr = func.expressions.append(Expression::GlobalVariable(gv));
447        let val = func
448            .expressions
449            .append(Expression::Literal(Literal::F32(1.0)));
450
451        func.body.push(Statement::Store {
452            pointer: ptr,
453            value: val,
454        });
455        func.body
456            .push(Statement::Barrier(nxpu_ir::Barrier::STORAGE));
457        func.body.push(Statement::Store {
458            pointer: ptr,
459            value: val,
460        });
461
462        let (dfg, schedule) = schedule_function(&func);
463        assert!(
464            schedule.is_valid(&dfg),
465            "schedule must respect barrier deps"
466        );
467        // At minimum 2 time slots: store+barrier can't overlap with post-barrier store.
468        assert!(
469            schedule.total_time() >= 2,
470            "barrier should enforce ordering"
471        );
472    }
473
474    #[test]
475    fn schedule_display() {
476        let func = Function::new("test");
477        let (_, schedule) = schedule_function(&func);
478        let s = format!("{schedule}");
479        assert!(s.contains("Schedule"));
480    }
481
482    #[test]
483    fn format_schedule_output() {
484        let mut func = Function::new("test");
485        let gv = dummy_gv_handle();
486        let ptr = func.expressions.append(Expression::GlobalVariable(gv));
487        let val = func
488            .expressions
489            .append(Expression::Literal(Literal::F32(1.0)));
490        func.body.push(Statement::Store {
491            pointer: ptr,
492            value: val,
493        });
494
495        let (dfg, schedule) = schedule_function(&func);
496        let output = format_schedule("test_ep", &dfg, &schedule);
497        assert!(output.contains("test_ep"));
498        assert!(output.contains("Nodes:"));
499        assert!(output.contains("t=0"));
500    }
501
502    #[test]
503    fn schedule_pass_runs_on_module() {
504        let pass = SchedulePass;
505        let mut module = Module::default();
506        // No entry points -- should not crash.
507        let changed = pass.run(&mut module);
508        assert!(!changed, "schedule pass should not modify the module");
509    }
510
511    #[test]
512    fn compute_schedules_empty_module() {
513        let module = Module::default();
514        let schedules = compute_schedules(&module);
515        assert_eq!(schedules.len(), 0);
516    }
517
518    #[test]
519    fn schedule_validity_check() {
520        let mut func = Function::new("test");
521        let gv = dummy_gv_handle();
522        let ptr = func.expressions.append(Expression::GlobalVariable(gv));
523        let val = func
524            .expressions
525            .append(Expression::Literal(Literal::F32(1.0)));
526        func.body.push(Statement::Store {
527            pointer: ptr,
528            value: val,
529        });
530        func.body.push(Statement::Store {
531            pointer: ptr,
532            value: val,
533        });
534
535        let (dfg, schedule) = schedule_function(&func);
536        assert!(schedule.is_valid(&dfg));
537
538        // Create an invalid schedule (both ops at same time with dependency).
539        let invalid = Schedule {
540            slots: vec![ScheduleSlot {
541                time: 0,
542                ops: vec![0, 1],
543            }],
544        };
545        assert!(
546            !invalid.is_valid(&dfg),
547            "schedule with dependent ops at same time should be invalid"
548        );
549    }
550
551    #[test]
552    fn schedule_pass_with_entry_points() {
553        let mut module = Module::default();
554        let mut func = Function::new("ep_test");
555        let gv = dummy_gv_handle();
556        let ptr = func.expressions.append(Expression::GlobalVariable(gv));
557        let val = func
558            .expressions
559            .append(Expression::Literal(Literal::F32(42.0)));
560        func.body.push(Statement::Store {
561            pointer: ptr,
562            value: val,
563        });
564
565        module.entry_points.push(nxpu_ir::EntryPoint {
566            name: "ep_test".into(),
567            workgroup_size: [1, 1, 1],
568            function: func,
569        });
570
571        let pass = SchedulePass;
572        let changed = pass.run(&mut module);
573        assert!(!changed, "schedule pass should not modify the module");
574    }
575
576    #[test]
577    fn compute_schedules_with_entry_points() {
578        let mut module = Module::default();
579
580        let mut func1 = Function::new("ep1");
581        let gv = dummy_gv_handle();
582        let ptr = func1.expressions.append(Expression::GlobalVariable(gv));
583        let val = func1
584            .expressions
585            .append(Expression::Literal(Literal::F32(1.0)));
586        func1.body.push(Statement::Store {
587            pointer: ptr,
588            value: val,
589        });
590
591        let mut func2 = Function::new("ep2");
592        let gv2 = dummy_gv_handle();
593        let ptr2 = func2.expressions.append(Expression::GlobalVariable(gv2));
594        let val2 = func2
595            .expressions
596            .append(Expression::Literal(Literal::F32(2.0)));
597        func2.body.push(Statement::Store {
598            pointer: ptr2,
599            value: val2,
600        });
601        func2.body.push(Statement::Store {
602            pointer: ptr2,
603            value: val2,
604        });
605
606        module.entry_points.push(nxpu_ir::EntryPoint {
607            name: "ep1".into(),
608            workgroup_size: [1, 1, 1],
609            function: func1,
610        });
611        module.entry_points.push(nxpu_ir::EntryPoint {
612            name: "ep2".into(),
613            workgroup_size: [1, 1, 1],
614            function: func2,
615        });
616
617        let schedules = compute_schedules(&module);
618        assert_eq!(schedules.len(), 2);
619        assert_eq!(schedules[0].0, "ep1");
620        assert_eq!(schedules[1].0, "ep2");
621        assert_eq!(schedules[0].2.total_ops(), 1);
622        assert_eq!(schedules[1].2.total_ops(), 2);
623    }
624
625    #[test]
626    fn format_schedule_with_critical_path() {
627        let mut func = Function::new("test");
628        let gv = dummy_gv_handle();
629        let ptr = func.expressions.append(Expression::GlobalVariable(gv));
630        let val = func
631            .expressions
632            .append(Expression::Literal(Literal::F32(1.0)));
633
634        // Create a chain to produce a non-empty critical path
635        func.body.push(Statement::Store {
636            pointer: ptr,
637            value: val,
638        });
639        func.body.push(Statement::Store {
640            pointer: ptr,
641            value: val,
642        });
643        func.body.push(Statement::Store {
644            pointer: ptr,
645            value: val,
646        });
647
648        let (dfg, schedule) = schedule_function(&func);
649        let output = format_schedule("test_chain", &dfg, &schedule);
650        assert!(output.contains("test_chain"));
651        assert!(output.contains("Nodes: 3"));
652        assert!(output.contains("Critical path:"));
653    }
654
655    #[test]
656    fn schedule_display_with_ops() {
657        let mut func = Function::new("test");
658        let gv = dummy_gv_handle();
659        let ptr = func.expressions.append(Expression::GlobalVariable(gv));
660        let val = func
661            .expressions
662            .append(Expression::Literal(Literal::F32(1.0)));
663        func.body.push(Statement::Store {
664            pointer: ptr,
665            value: val,
666        });
667        func.body.push(Statement::Store {
668            pointer: ptr,
669            value: val,
670        });
671
672        let (_, schedule) = schedule_function(&func);
673        let s = format!("{schedule}");
674        assert!(s.contains("Schedule (2 slots, 2 ops):"));
675        assert!(s.contains("t=0"));
676        assert!(s.contains("t=1"));
677    }
678
679    #[test]
680    fn schedule_is_valid_missing_node() {
681        // Build a DFG with 2 nodes, but schedule only contains 1 node.
682        // The missing node should cause is_valid to return false.
683        let mut func = Function::new("test");
684        let gv = dummy_gv_handle();
685        let ptr = func.expressions.append(Expression::GlobalVariable(gv));
686        let val = func
687            .expressions
688            .append(Expression::Literal(Literal::F32(1.0)));
689        func.body.push(Statement::Store {
690            pointer: ptr,
691            value: val,
692        });
693        func.body.push(Statement::Store {
694            pointer: ptr,
695            value: val,
696        });
697
698        let dfg = DataflowGraph::build(&func);
699        // Only schedule node 0, missing node 1
700        let partial = Schedule {
701            slots: vec![ScheduleSlot {
702                time: 0,
703                ops: vec![0],
704            }],
705        };
706        // The edge from 0->1 exists, but node 1 has no time slot -> invalid
707        assert!(!partial.is_valid(&dfg));
708    }
709}