oxicuda_graph/analysis/
topo.rs1use std::collections::VecDeque;
15
16use crate::error::{GraphError, GraphResult};
17use crate::graph::ComputeGraph;
18use crate::node::NodeId;
19
20#[derive(Debug, Clone, PartialEq, Eq)]
26pub struct NodeInfo {
27 pub level: usize,
29 pub asap: u64,
31 pub alap: u64,
33 pub slack: u64,
35 pub on_critical_path: bool,
37}
38
39impl NodeInfo {
40 #[must_use]
43 pub fn mobility(&self) -> u64 {
44 self.slack
45 }
46}
47
48#[derive(Debug, Clone)]
54pub struct TopoAnalysis {
55 pub order: Vec<NodeId>,
57 info: Vec<NodeInfo>,
59 pub critical_path_cost: u64,
61 pub critical_path_edges: usize,
63}
64
65impl TopoAnalysis {
66 #[must_use]
70 pub fn node_info(&self, id: NodeId) -> Option<&NodeInfo> {
71 self.info.get(id.0 as usize)
72 }
73
74 pub fn critical_path_nodes(&self) -> Vec<NodeId> {
76 self.order
77 .iter()
78 .filter(|&&id| {
79 self.info
80 .get(id.0 as usize)
81 .map(|i| i.on_critical_path)
82 .unwrap_or(false)
83 })
84 .copied()
85 .collect()
86 }
87
88 pub fn priority_order(&self) -> Vec<NodeId> {
90 let mut nodes = self.order.clone();
91 nodes.sort_by_key(|&id| {
92 self.info
93 .get(id.0 as usize)
94 .map(|i| i.slack)
95 .unwrap_or(u64::MAX)
96 });
97 nodes
98 }
99
100 #[must_use]
102 pub fn max_level(&self) -> usize {
103 self.info.iter().map(|i| i.level).max().unwrap_or(0)
104 }
105
106 pub fn level_widths(&self) -> Vec<usize> {
108 let max = self.max_level();
109 let mut widths = vec![0usize; max + 1];
110 for info in &self.info {
111 widths[info.level] += 1;
112 }
113 widths
114 }
115}
116
117pub fn analyse(graph: &ComputeGraph) -> GraphResult<TopoAnalysis> {
129 if graph.is_empty() {
130 return Err(GraphError::EmptyGraph);
131 }
132
133 let order = graph.topological_order()?;
134 let n = graph.node_count();
135
136 let mut asap = vec![0u64; n];
139 for &id in &order {
140 let cost_before = asap[id.0 as usize];
141 for &succ in graph.successors(id)? {
142 let new_asap = cost_before + graph.node(id)?.cost_hint;
143 if new_asap > asap[succ.0 as usize] {
144 asap[succ.0 as usize] = new_asap;
145 }
146 }
147 }
148
149 let schedule_len: u64 = (0..n)
151 .map(|i| asap[i] + graph.nodes()[i].cost_hint)
152 .max()
153 .unwrap_or(0);
154
155 let mut alap = vec![schedule_len; n];
158 for (i, node) in graph.nodes().iter().enumerate() {
160 if graph.successors(node.id)?.is_empty() {
161 alap[i] = schedule_len - node.cost_hint;
162 }
163 }
164 for &id in order.iter().rev() {
166 let succs = graph.successors(id)?;
167 if succs.is_empty() {
168 continue;
169 }
170 let cost_v = graph.node(id)?.cost_hint;
172 let min_succ_alap = succs
173 .iter()
174 .map(|&s| alap[s.0 as usize])
175 .min()
176 .unwrap_or(schedule_len);
177 let v_alap = min_succ_alap.saturating_sub(cost_v);
178 if v_alap < alap[id.0 as usize] {
179 alap[id.0 as usize] = v_alap;
180 }
181 }
182
183 let mut info = Vec::with_capacity(n);
185 for i in 0..n {
186 let slack = alap[i].saturating_sub(asap[i]);
187 info.push(NodeInfo {
188 level: 0, asap: asap[i],
190 alap: alap[i],
191 slack,
192 on_critical_path: slack == 0,
193 });
194 }
195
196 let mut level = vec![0usize; n];
198 let mut in_degree: Vec<u32> = graph
199 .nodes()
200 .iter()
201 .map(|nd| {
202 graph
203 .predecessors(nd.id)
204 .map(|p| p.len() as u32)
205 .unwrap_or(0)
206 })
207 .collect();
208 let mut queue: VecDeque<NodeId> = (0..n)
209 .filter(|&i| in_degree[i] == 0)
210 .map(|i| NodeId(i as u32))
211 .collect();
212 while let Some(id) = queue.pop_front() {
213 let lv = level[id.0 as usize];
214 for &succ in graph.successors(id)? {
215 let nl = lv + 1;
216 if nl > level[succ.0 as usize] {
217 level[succ.0 as usize] = nl;
218 }
219 let d = &mut in_degree[succ.0 as usize];
220 *d -= 1;
221 if *d == 0 {
222 queue.push_back(succ);
223 }
224 }
225 }
226 for (i, inf) in info.iter_mut().enumerate() {
227 inf.level = level[i];
228 }
229
230 let critical_path_edges = *level.iter().max().unwrap_or(&0);
231 let critical_path_cost = schedule_len;
232
233 Ok(TopoAnalysis {
234 order,
235 info,
236 critical_path_cost,
237 critical_path_edges,
238 })
239}
240
241#[cfg(test)]
246mod tests {
247 use super::*;
248 use crate::builder::GraphBuilder;
249 use crate::node::MemcpyDir;
250
251 fn make_linear(n: usize) -> (ComputeGraph, Vec<NodeId>) {
252 let mut b = GraphBuilder::new().with_auto_infer_edges(false);
253 let ids: Vec<NodeId> = (0..n).map(|_| b.add_barrier("x")).collect();
254 for w in ids.windows(2) {
255 b.dep(w[0], w[1]);
256 }
257 let g = b.build().expect("test graph builds successfully");
258 (g, ids)
259 }
260
261 #[test]
262 fn analyse_empty_returns_error() {
263 let g = ComputeGraph::new();
264 assert!(matches!(analyse(&g), Err(GraphError::EmptyGraph)));
265 }
266
267 #[test]
268 fn analyse_single_node() {
269 let (g, ids) = make_linear(1);
270 let ta = analyse(&g).expect("topological analysis succeeds on valid graph");
271 let info = ta
272 .node_info(ids[0])
273 .expect("node registered in topological analysis");
274 assert_eq!(info.level, 0);
275 assert_eq!(info.asap, 0);
276 assert_eq!(info.slack, 0);
277 assert!(info.on_critical_path);
278 }
279
280 #[test]
281 fn analyse_linear_levels() {
282 let (g, ids) = make_linear(5);
283 let ta = analyse(&g).expect("topological analysis succeeds on valid graph");
284 for (i, &id) in ids.iter().enumerate() {
285 assert_eq!(
286 ta.node_info(id)
287 .expect("node registered in topological analysis")
288 .level,
289 i,
290 "level mismatch at {i}"
291 );
292 }
293 }
294
295 #[test]
296 fn analyse_linear_critical_path_edges() {
297 let (g, _) = make_linear(4);
298 let ta = analyse(&g).expect("topological analysis succeeds on valid graph");
299 assert_eq!(ta.critical_path_edges, 3);
300 }
301
302 #[test]
303 fn analyse_diamond_all_on_critical_path() {
304 let mut b = GraphBuilder::new().with_auto_infer_edges(false);
305 let a = b.add_barrier("a");
306 let b1 = b.add_barrier("b");
307 let c = b.add_barrier("c");
308 let d = b.add_barrier("d");
309 b.dep(a, b1).dep(a, c).dep(b1, d).dep(c, d);
310 let g = b.build().expect("test graph builds successfully");
311 let ta = analyse(&g).expect("topological analysis succeeds on valid graph");
312 assert_eq!(ta.critical_path_edges, 2);
314 assert!(
316 ta.node_info(a)
317 .expect("node a registered in topological analysis")
318 .on_critical_path
319 );
320 assert!(
321 ta.node_info(d)
322 .expect("node d registered in topological analysis")
323 .on_critical_path
324 );
325 }
326
327 #[test]
328 fn analyse_level_widths() {
329 let (g, _) = make_linear(3);
330 let ta = analyse(&g).expect("topological analysis succeeds on valid graph");
331 let widths = ta.level_widths();
333 assert_eq!(widths, vec![1, 1, 1]);
334 }
335
336 #[test]
337 fn analyse_fork_join_widths() {
338 let mut b = GraphBuilder::new().with_auto_infer_edges(false);
339 let src = b.add_barrier("src");
340 let a = b.add_barrier("a");
341 let c = b.add_barrier("c");
342 let d = b.add_barrier("d");
343 let sink = b.add_barrier("sink");
344 b.fan_out(src, &[a, c, d]);
345 b.fan_in(&[a, c, d], sink);
346 let g = b.build().expect("test graph builds successfully");
347 let ta = analyse(&g).expect("topological analysis succeeds on valid graph");
348 let widths = ta.level_widths();
349 assert_eq!(widths[0], 1);
351 assert_eq!(widths[1], 3);
352 assert_eq!(widths[2], 1);
353 }
354
355 #[test]
356 fn analyse_priority_order_zero_slack_first() {
357 let mut b = GraphBuilder::new().with_auto_infer_edges(false);
358 let a = b.add_barrier("a");
360 let bnode = b.add_barrier("b");
361 let c = b.add_barrier("c");
362 b.dep(a, bnode);
363 let g = b.build().expect("test graph builds successfully");
364 let ta = analyse(&g).expect("topological analysis succeeds on valid graph");
365 let prio = ta.priority_order();
366 let pos_a = prio
368 .iter()
369 .position(|&x| x == a)
370 .expect("node a in priority order");
371 let pos_c = prio
372 .iter()
373 .position(|&x| x == c)
374 .expect("node c in priority order");
375 let slack_a = ta
376 .node_info(a)
377 .expect("node a registered in topological analysis")
378 .slack;
379 let slack_c = ta
380 .node_info(c)
381 .expect("node c registered in topological analysis")
382 .slack;
383 if slack_a < slack_c {
384 assert!(
385 pos_a < pos_c,
386 "zero-slack node must precede high-slack node"
387 );
388 }
389 let _ = bnode;
390 }
391
392 #[test]
393 fn analyse_max_level() {
394 let (g, _) = make_linear(7);
395 let ta = analyse(&g).expect("topological analysis succeeds on valid graph");
396 assert_eq!(ta.max_level(), 6);
397 }
398
399 #[test]
400 fn analyse_cost_weighted_asap() {
401 let mut b = GraphBuilder::new().with_auto_infer_edges(false);
404 let a = b.add_raw(
405 crate::node::GraphNode::new(crate::node::NodeId(0), crate::node::NodeKind::Barrier)
406 .with_cost(1)
407 .with_name("a"),
408 );
409 let bnode = b.add_raw(
410 crate::node::GraphNode::new(crate::node::NodeId(0), crate::node::NodeKind::Barrier)
411 .with_cost(10)
412 .with_name("b"),
413 );
414 let c = b.add_raw(
415 crate::node::GraphNode::new(crate::node::NodeId(0), crate::node::NodeKind::Barrier)
416 .with_cost(1)
417 .with_name("c"),
418 );
419 b.dep(a, bnode).dep(bnode, c);
420 let g = b.build().expect("test graph builds successfully");
421 let ta = analyse(&g).expect("topological analysis succeeds on valid graph");
422 assert_eq!(
423 ta.node_info(a)
424 .expect("node a registered in topological analysis")
425 .asap,
426 0
427 );
428 assert_eq!(
429 ta.node_info(bnode)
430 .expect("node bnode registered in topological analysis")
431 .asap,
432 1
433 );
434 assert_eq!(
435 ta.node_info(c)
436 .expect("node c registered in topological analysis")
437 .asap,
438 11
439 );
440 }
441
442 #[test]
443 fn analyse_critical_path_nodes_nonempty() {
444 let (g, _) = make_linear(4);
445 let ta = analyse(&g).expect("topological analysis succeeds on valid graph");
446 let cp = ta.critical_path_nodes();
447 assert!(!cp.is_empty());
448 }
449
450 #[test]
451 fn analyse_memcpy_node_in_graph() {
452 let mut b = GraphBuilder::new().with_auto_infer_edges(false);
453 let upload = b.add_memcpy("up", MemcpyDir::HostToDevice, 1024);
454 let compute = b.add_kernel("k", 4, 128, 0).cost(50).finish();
455 let download = b.add_memcpy("dn", MemcpyDir::DeviceToHost, 1024);
456 b.chain(&[upload, compute, download]);
457 let g = b.build().expect("test graph builds successfully");
458 let ta = analyse(&g).expect("topological analysis succeeds on valid graph");
459 assert_eq!(ta.critical_path_edges, 2);
460 assert!(
461 ta.node_info(compute)
462 .expect("compute node registered in topological analysis")
463 .asap
464 >= 1
465 );
466 }
467}