1use std::collections::{HashMap, HashSet};
29
30use crate::analysis::{dominance_analyse, topo_analyse};
31use crate::error::{GraphError, GraphResult};
32use crate::graph::ComputeGraph;
33use crate::node::{KernelConfig, NodeId, NodeKind};
34
35#[derive(Debug, Clone, PartialEq, Eq)]
44pub struct FusionGroup {
45 pub id: usize,
47 pub members: Vec<NodeId>,
49 pub config: KernelConfig,
51 pub tag: String,
53}
54
55impl FusionGroup {
56 #[must_use]
58 pub fn size(&self) -> usize {
59 self.members.len()
60 }
61
62 #[must_use]
64 pub fn is_trivial(&self) -> bool {
65 self.members.len() == 1
66 }
67}
68
69#[derive(Debug, Clone)]
75pub struct FusionPlan {
76 pub groups: Vec<FusionGroup>,
78 pub node_to_group: HashMap<NodeId, usize>,
80}
81
82impl FusionPlan {
83 pub fn fusion_count(&self) -> usize {
85 self.groups.iter().filter(|g| !g.is_trivial()).count()
86 }
87
88 pub fn nodes_saved(&self) -> usize {
92 self.groups
93 .iter()
94 .filter(|g| !g.is_trivial())
95 .map(|g| g.size() - 1)
96 .sum()
97 }
98
99 pub fn group_of(&self, node: NodeId) -> Option<&FusionGroup> {
101 self.node_to_group
102 .get(&node)
103 .and_then(|&idx| self.groups.get(idx))
104 }
105}
106
107fn configs_compatible(a: &KernelConfig, b: &KernelConfig) -> bool {
116 a.total_threads() == b.total_threads()
117}
118
119fn only_fusible_between(
122 graph: &ComputeGraph,
123 a: NodeId,
124 b: NodeId,
125 topo_pos: &HashMap<NodeId, usize>,
126) -> bool {
127 let pos_a = topo_pos[&a];
128 let pos_b = topo_pos[&b];
129 if pos_b <= pos_a + 1 {
130 return true; }
132 let mut visited = HashSet::new();
135 let mut stack = vec![a];
136 while let Some(cur) = stack.pop() {
137 if cur == b {
138 continue;
139 }
140 for &s in graph.successors(cur).unwrap_or(&[]) {
141 if visited.insert(s) {
142 if s == b {
143 continue;
144 }
145 let node = graph.node(s).ok();
146 let is_fusible = node.map(|n| n.kind.is_fusible()).unwrap_or(false);
147 let is_barrier = node
148 .map(|n| matches!(n.kind, NodeKind::Barrier))
149 .unwrap_or(false);
150 let spos = topo_pos.get(&s).copied().unwrap_or(usize::MAX);
151 if spos < pos_b && (is_fusible || is_barrier) {
152 stack.push(s);
153 } else if spos < pos_b && !is_fusible && !is_barrier {
154 return false; }
156 }
157 }
158 }
159 true
160}
161
162pub fn analyse(graph: &ComputeGraph) -> GraphResult<FusionPlan> {
174 if graph.is_empty() {
175 return Err(GraphError::EmptyGraph);
176 }
177
178 let topo = topo_analyse(graph)?;
179 let dt = dominance_analyse(graph)?;
180
181 let topo_pos: HashMap<NodeId, usize> = topo
182 .order
183 .iter()
184 .enumerate()
185 .map(|(p, &id)| (id, p))
186 .collect();
187
188 let mut assigned: HashMap<NodeId, usize> = HashMap::new();
190 let mut groups: Vec<FusionGroup> = Vec::new();
191
192 for &node_id in &topo.order {
194 if assigned.contains_key(&node_id) {
195 continue;
196 }
197
198 let node = graph.node(node_id)?;
199
200 let (is_fusible, base_config) = match &node.kind {
202 NodeKind::KernelLaunch {
203 fusible, config, ..
204 } => (*fusible, *config),
205 _ => {
206 let gid = groups.len();
208 groups.push(FusionGroup {
209 id: gid,
210 members: vec![node_id],
211 config: KernelConfig::linear(1, 1, 0),
212 tag: format!("non_kernel_{}", node.kind.tag()),
213 });
214 assigned.insert(node_id, gid);
215 continue;
216 }
217 };
218
219 if !is_fusible {
220 let gid = groups.len();
221 groups.push(FusionGroup {
222 id: gid,
223 members: vec![node_id],
224 config: base_config,
225 tag: format!("non_fusible_{}", node.display_name()),
226 });
227 assigned.insert(node_id, gid);
228 continue;
229 }
230
231 let gid = groups.len();
233 let mut members = vec![node_id];
234 assigned.insert(node_id, gid);
235
236 let mut frontier = graph.successors(node_id)?.to_vec();
238 while let Some(succ_id) = frontier.first().copied() {
239 frontier.remove(0);
240 if assigned.contains_key(&succ_id) {
241 continue;
242 }
243 let succ = graph.node(succ_id)?;
244 let (succ_fusible, succ_config) = match &succ.kind {
245 NodeKind::KernelLaunch {
246 fusible, config, ..
247 } => (*fusible, *config),
248 _ => continue,
249 };
250 if !succ_fusible {
251 continue;
252 }
253 if !configs_compatible(&base_config, &succ_config) {
256 continue;
257 }
258 let last_member = *members.last().unwrap();
260 if !dt.dominates(last_member, succ_id) {
261 continue;
262 }
263 if !only_fusible_between(graph, last_member, succ_id, &topo_pos) {
265 continue;
266 }
267 members.push(succ_id);
269 assigned.insert(succ_id, gid);
270 for &next in graph.successors(succ_id)? {
272 if !assigned.contains_key(&next) {
273 frontier.push(next);
274 }
275 }
276 }
277
278 let tag = if members.len() > 1 {
279 format!(
280 "fused_{}..{}",
281 graph.node(members[0])?.display_name(),
282 graph.node(*members.last().unwrap())?.display_name()
283 )
284 } else {
285 format!("solo_{}", graph.node(node_id)?.display_name())
286 };
287
288 groups.push(FusionGroup {
289 id: gid,
290 members,
291 config: base_config,
292 tag,
293 });
294 }
295
296 let node_to_group: HashMap<NodeId, usize> = assigned;
298
299 Ok(FusionPlan {
300 groups,
301 node_to_group,
302 })
303}
304
305#[cfg(test)]
310mod tests {
311 use super::*;
312 use crate::builder::GraphBuilder;
313 use crate::node::MemcpyDir;
314
315 fn fusible_kernel(b: &mut GraphBuilder, name: &str) -> NodeId {
316 b.add_kernel(name, 4, 256, 0).fusible(true).finish()
317 }
318
319 fn non_fusible_kernel(b: &mut GraphBuilder, name: &str) -> NodeId {
320 b.add_kernel(name, 4, 256, 0).fusible(false).finish()
321 }
322
323 #[test]
324 fn fusion_empty_graph() {
325 let g = ComputeGraph::new();
326 assert!(matches!(analyse(&g), Err(GraphError::EmptyGraph)));
327 }
328
329 #[test]
330 fn fusion_single_fusible_kernel_trivial_group() {
331 let mut b = GraphBuilder::new().with_auto_infer_edges(false);
332 let k = fusible_kernel(&mut b, "add");
333 let g = b.build().unwrap();
334 let plan = analyse(&g).unwrap();
335 assert_eq!(plan.groups.len(), 1);
336 assert!(plan.groups[0].is_trivial());
337 assert_eq!(plan.group_of(k).unwrap().members, vec![k]);
338 }
339
340 #[test]
341 fn fusion_chain_of_fusible_kernels_merged() {
342 let mut b = GraphBuilder::new().with_auto_infer_edges(false);
343 let k0 = fusible_kernel(&mut b, "k0");
344 let k1 = fusible_kernel(&mut b, "k1");
345 let k2 = fusible_kernel(&mut b, "k2");
346 b.chain(&[k0, k1, k2]);
347 let g = b.build().unwrap();
348 let plan = analyse(&g).unwrap();
349 assert_eq!(plan.fusion_count(), 1);
351 let group = plan.group_of(k0).unwrap();
352 assert_eq!(group.size(), 3);
353 assert!(group.members.contains(&k0));
354 assert!(group.members.contains(&k1));
355 assert!(group.members.contains(&k2));
356 }
357
358 #[test]
359 fn fusion_non_fusible_breaks_chain() {
360 let mut b = GraphBuilder::new().with_auto_infer_edges(false);
362 let k0 = fusible_kernel(&mut b, "k0");
363 let k1 = non_fusible_kernel(&mut b, "k1");
364 let k2 = fusible_kernel(&mut b, "k2");
365 b.chain(&[k0, k1, k2]);
366 let g = b.build().unwrap();
367 let plan = analyse(&g).unwrap();
368 let g0 = plan.group_of(k0).unwrap().id;
370 let g2 = plan.group_of(k2).unwrap().id;
371 assert_ne!(g0, g2);
372 }
373
374 #[test]
375 fn fusion_memcpy_not_fused() {
376 let mut b = GraphBuilder::new().with_auto_infer_edges(false);
377 let upload = b.add_memcpy("up", MemcpyDir::HostToDevice, 1024);
378 let k = fusible_kernel(&mut b, "k");
379 let download = b.add_memcpy("dn", MemcpyDir::DeviceToHost, 1024);
380 b.chain(&[upload, k, download]);
381 let g = b.build().unwrap();
382 let plan = analyse(&g).unwrap();
383 let gup = plan.group_of(upload).unwrap();
385 let gdn = plan.group_of(download).unwrap();
386 assert!(gup.is_trivial());
387 assert!(gdn.is_trivial());
388 }
389
390 #[test]
391 fn fusion_incompatible_configs_not_fused() {
392 let mut b = GraphBuilder::new().with_auto_infer_edges(false);
395 let k0 = b.add_kernel("k0", 4, 256, 0).fusible(true).finish();
396 let k1 = b.add_kernel("k1", 8, 256, 0).fusible(true).finish();
397 b.dep(k0, k1);
398 let g = b.build().unwrap();
399 let plan = analyse(&g).unwrap();
400 let gk0 = plan.group_of(k0).unwrap().id;
401 let gk1 = plan.group_of(k1).unwrap().id;
402 assert_ne!(gk0, gk1);
403 }
404
405 #[test]
406 fn fusion_nodes_saved_count() {
407 let mut b = GraphBuilder::new().with_auto_infer_edges(false);
408 let k0 = fusible_kernel(&mut b, "k0");
409 let k1 = fusible_kernel(&mut b, "k1");
410 let k2 = fusible_kernel(&mut b, "k2");
411 b.chain(&[k0, k1, k2]);
412 let g = b.build().unwrap();
413 let plan = analyse(&g).unwrap();
414 assert_eq!(plan.nodes_saved(), 2);
416 }
417
418 #[test]
419 fn fusion_plan_covers_all_nodes() {
420 let mut b = GraphBuilder::new().with_auto_infer_edges(false);
421 let k0 = fusible_kernel(&mut b, "k0");
422 let k1 = non_fusible_kernel(&mut b, "k1");
423 let upload = b.add_memcpy("up", MemcpyDir::HostToDevice, 512);
424 b.chain(&[upload, k0, k1]);
425 let g = b.build().unwrap();
426 let plan = analyse(&g).unwrap();
427 let total: usize = plan.groups.iter().map(|g| g.size()).sum();
429 assert_eq!(total, 3);
430 assert!(plan.node_to_group.contains_key(&k0));
432 assert!(plan.node_to_group.contains_key(&k1));
433 assert!(plan.node_to_group.contains_key(&upload));
434 }
435
436 #[test]
437 fn fusion_parallel_branches_not_fused() {
438 let mut b = GraphBuilder::new().with_auto_infer_edges(false);
440 let src = b.add_barrier("src");
441 let k0 = fusible_kernel(&mut b, "k0");
442 let k1 = fusible_kernel(&mut b, "k1");
443 b.fan_out(src, &[k0, k1]);
444 let g = b.build().unwrap();
445 let plan = analyse(&g).unwrap();
446 let gk0 = plan.group_of(k0).unwrap().id;
448 let gk1 = plan.group_of(k1).unwrap().id;
449 assert_ne!(gk0, gk1);
450 }
451
452 #[test]
453 fn fusion_group_tag_contains_names() {
454 let mut b = GraphBuilder::new().with_auto_infer_edges(false);
455 let k0 = fusible_kernel(&mut b, "relu");
456 let k1 = fusible_kernel(&mut b, "scale");
457 b.dep(k0, k1);
458 let g = b.build().unwrap();
459 let plan = analyse(&g).unwrap();
460 let group = plan.group_of(k0).unwrap();
461 assert!(!group.tag.is_empty());
462 }
463
464 #[test]
465 fn fusion_empty_fusible_graph_one_group() {
466 let mut b = GraphBuilder::new().with_auto_infer_edges(false);
467 let k = fusible_kernel(&mut b, "solo");
468 let g = b.build().unwrap();
469 let plan = analyse(&g).unwrap();
470 assert_eq!(plan.fusion_count(), 0); assert_eq!(plan.nodes_saved(), 0);
472 assert_eq!(plan.group_of(k).unwrap().size(), 1);
473 }
474}