1use std::fmt;
9
10use nxpu_analysis::DataflowGraph;
11use nxpu_ir::Module;
12
13use crate::Pass;
14
15#[derive(Clone, Debug)]
18pub struct ScheduleSlot {
19 pub time: usize,
21 pub ops: Vec<usize>,
23}
24
25#[derive(Clone, Debug)]
27pub struct Schedule {
28 pub slots: Vec<ScheduleSlot>,
30}
31
32impl Schedule {
33 pub fn total_time(&self) -> usize {
35 self.slots.len()
36 }
37
38 pub fn total_ops(&self) -> usize {
40 self.slots.iter().map(|s| s.ops.len()).sum()
41 }
42
43 pub fn max_parallelism(&self) -> usize {
45 self.slots.iter().map(|s| s.ops.len()).max().unwrap_or(0)
46 }
47
48 pub fn is_valid(&self, dfg: &DataflowGraph) -> bool {
53 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 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
97pub 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 let cp_result = dfg.critical_path();
118 let priority = &cp_result.node_distances;
119
120 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 let mut remaining_preds = preds_count.clone();
131
132 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 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)) });
153
154 let slot_ops: Vec<usize> = std::mem::take(&mut ready);
156
157 for &op in &slot_ops {
158 scheduled[op] = true;
159 }
160
161 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
184pub 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#[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 false
219 }
220}
221
222pub 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
236pub 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 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 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 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 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 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 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 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 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 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 let partial = Schedule {
701 slots: vec![ScheduleSlot {
702 time: 0,
703 ops: vec![0],
704 }],
705 };
706 assert!(!partial.is_valid(&dfg));
708 }
709}