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().unwrap();
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).unwrap();
271 let info = ta.node_info(ids[0]).unwrap();
272 assert_eq!(info.level, 0);
273 assert_eq!(info.asap, 0);
274 assert_eq!(info.slack, 0);
275 assert!(info.on_critical_path);
276 }
277
278 #[test]
279 fn analyse_linear_levels() {
280 let (g, ids) = make_linear(5);
281 let ta = analyse(&g).unwrap();
282 for (i, &id) in ids.iter().enumerate() {
283 assert_eq!(ta.node_info(id).unwrap().level, i, "level mismatch at {i}");
284 }
285 }
286
287 #[test]
288 fn analyse_linear_critical_path_edges() {
289 let (g, _) = make_linear(4);
290 let ta = analyse(&g).unwrap();
291 assert_eq!(ta.critical_path_edges, 3);
292 }
293
294 #[test]
295 fn analyse_diamond_all_on_critical_path() {
296 let mut b = GraphBuilder::new().with_auto_infer_edges(false);
297 let a = b.add_barrier("a");
298 let b1 = b.add_barrier("b");
299 let c = b.add_barrier("c");
300 let d = b.add_barrier("d");
301 b.dep(a, b1).dep(a, c).dep(b1, d).dep(c, d);
302 let g = b.build().unwrap();
303 let ta = analyse(&g).unwrap();
304 assert_eq!(ta.critical_path_edges, 2);
306 assert!(ta.node_info(a).unwrap().on_critical_path);
308 assert!(ta.node_info(d).unwrap().on_critical_path);
309 }
310
311 #[test]
312 fn analyse_level_widths() {
313 let (g, _) = make_linear(3);
314 let ta = analyse(&g).unwrap();
315 let widths = ta.level_widths();
317 assert_eq!(widths, vec![1, 1, 1]);
318 }
319
320 #[test]
321 fn analyse_fork_join_widths() {
322 let mut b = GraphBuilder::new().with_auto_infer_edges(false);
323 let src = b.add_barrier("src");
324 let a = b.add_barrier("a");
325 let c = b.add_barrier("c");
326 let d = b.add_barrier("d");
327 let sink = b.add_barrier("sink");
328 b.fan_out(src, &[a, c, d]);
329 b.fan_in(&[a, c, d], sink);
330 let g = b.build().unwrap();
331 let ta = analyse(&g).unwrap();
332 let widths = ta.level_widths();
333 assert_eq!(widths[0], 1);
335 assert_eq!(widths[1], 3);
336 assert_eq!(widths[2], 1);
337 }
338
339 #[test]
340 fn analyse_priority_order_zero_slack_first() {
341 let mut b = GraphBuilder::new().with_auto_infer_edges(false);
342 let a = b.add_barrier("a");
344 let bnode = b.add_barrier("b");
345 let c = b.add_barrier("c");
346 b.dep(a, bnode);
347 let g = b.build().unwrap();
348 let ta = analyse(&g).unwrap();
349 let prio = ta.priority_order();
350 let pos_a = prio.iter().position(|&x| x == a).unwrap();
352 let pos_c = prio.iter().position(|&x| x == c).unwrap();
353 let slack_a = ta.node_info(a).unwrap().slack;
354 let slack_c = ta.node_info(c).unwrap().slack;
355 if slack_a < slack_c {
356 assert!(
357 pos_a < pos_c,
358 "zero-slack node must precede high-slack node"
359 );
360 }
361 let _ = bnode;
362 }
363
364 #[test]
365 fn analyse_max_level() {
366 let (g, _) = make_linear(7);
367 let ta = analyse(&g).unwrap();
368 assert_eq!(ta.max_level(), 6);
369 }
370
371 #[test]
372 fn analyse_cost_weighted_asap() {
373 let mut b = GraphBuilder::new().with_auto_infer_edges(false);
376 let a = b.add_raw(
377 crate::node::GraphNode::new(crate::node::NodeId(0), crate::node::NodeKind::Barrier)
378 .with_cost(1)
379 .with_name("a"),
380 );
381 let bnode = b.add_raw(
382 crate::node::GraphNode::new(crate::node::NodeId(0), crate::node::NodeKind::Barrier)
383 .with_cost(10)
384 .with_name("b"),
385 );
386 let c = b.add_raw(
387 crate::node::GraphNode::new(crate::node::NodeId(0), crate::node::NodeKind::Barrier)
388 .with_cost(1)
389 .with_name("c"),
390 );
391 b.dep(a, bnode).dep(bnode, c);
392 let g = b.build().unwrap();
393 let ta = analyse(&g).unwrap();
394 assert_eq!(ta.node_info(a).unwrap().asap, 0);
395 assert_eq!(ta.node_info(bnode).unwrap().asap, 1);
396 assert_eq!(ta.node_info(c).unwrap().asap, 11);
397 }
398
399 #[test]
400 fn analyse_critical_path_nodes_nonempty() {
401 let (g, _) = make_linear(4);
402 let ta = analyse(&g).unwrap();
403 let cp = ta.critical_path_nodes();
404 assert!(!cp.is_empty());
405 }
406
407 #[test]
408 fn analyse_memcpy_node_in_graph() {
409 let mut b = GraphBuilder::new().with_auto_infer_edges(false);
410 let upload = b.add_memcpy("up", MemcpyDir::HostToDevice, 1024);
411 let compute = b.add_kernel("k", 4, 128, 0).cost(50).finish();
412 let download = b.add_memcpy("dn", MemcpyDir::DeviceToHost, 1024);
413 b.chain(&[upload, compute, download]);
414 let g = b.build().unwrap();
415 let ta = analyse(&g).unwrap();
416 assert_eq!(ta.critical_path_edges, 2);
417 assert!(ta.node_info(compute).unwrap().asap >= 1);
418 }
419}