1use rlx_ir::{Graph, Op};
23use std::collections::{HashMap, HashSet};
24
25#[derive(Clone, Debug)]
27pub struct FlowPattern {
28 pub depth: usize,
30 pub count: usize,
32 pub tree: String,
34 pub sites: Vec<u32>,
36}
37
38impl FlowPattern {
39 pub fn score(&self) -> usize {
41 self.count * (self.tree.matches('(').count() + 1)
42 }
43}
44
45pub fn op_name(op: &Op) -> String {
48 match op {
49 Op::Activation(a) => format!("{a:?}"),
50 Op::Binary(b) => format!("{b:?}"),
51 Op::Compare(c) => format!("Cmp{c:?}"),
52 Op::Input { .. } => "in".into(),
53 Op::Param { .. } => "w".into(),
54 Op::Constant { .. } => "k".into(),
55 other => format!("{:?}", other.kind()),
56 }
57}
58
59fn sig(
60 ops: &[String],
61 inputs: &[Vec<usize>],
62 node: usize,
63 depth: usize,
64 memo: &mut HashMap<(usize, usize), String>,
65) -> String {
66 if let Some(s) = memo.get(&(node, depth)) {
67 return s.clone();
68 }
69 let name = ops[node].clone();
70 let s = if depth == 0 || inputs[node].is_empty() {
71 name
72 } else {
73 let kids: Vec<String> = inputs[node]
74 .iter()
75 .map(|&i| sig(ops, inputs, i, depth - 1, memo))
76 .collect();
77 format!("{name}({})", kids.join(","))
78 };
79 memo.insert((node, depth), s.clone());
80 s
81}
82
83pub fn repeated_flow_patterns_on(
87 ops: &[String],
88 inputs: &[Vec<usize>],
89 min_depth: usize,
90 max_depth: usize,
91 min_count: usize,
92) -> Vec<FlowPattern> {
93 let mut memo: HashMap<(usize, usize), String> = HashMap::new();
94 let mut out: Vec<FlowPattern> = Vec::new();
95 let mut seen_tree: HashSet<String> = HashSet::new();
96
97 for depth in min_depth..=max_depth {
98 let mut groups: HashMap<String, Vec<u32>> = HashMap::new();
99 for node in 0..ops.len() {
100 if inputs[node].is_empty() {
101 continue; }
103 let s = sig(ops, inputs, node, depth, &mut memo);
104 if !s.contains('(') {
105 continue; }
107 groups.entry(s).or_default().push(node as u32);
108 }
109 for (tree, sites) in groups {
110 if sites.len() >= min_count && seen_tree.insert(tree.clone()) {
111 out.push(FlowPattern {
112 depth,
113 count: sites.len(),
114 sites: sites.into_iter().take(6).collect(),
115 tree,
116 });
117 }
118 }
119 }
120
121 out.sort_by(|a, b| b.score().cmp(&a.score()).then(b.count.cmp(&a.count)));
122 out
123}
124
125pub fn repeated_flow_patterns(
128 g: &Graph,
129 min_depth: usize,
130 max_depth: usize,
131 min_count: usize,
132) -> Vec<FlowPattern> {
133 let ops: Vec<String> = g.nodes().iter().map(|n| op_name(&n.op)).collect();
134 let inputs: Vec<Vec<usize>> = g
135 .nodes()
136 .iter()
137 .map(|n| n.inputs.iter().map(|i| i.0 as usize).collect())
138 .collect();
139 repeated_flow_patterns_on(&ops, &inputs, min_depth, max_depth, min_count)
140}
141
142pub fn decomposition_hint(p: &FlowPattern) -> &'static str {
144 let t = &p.tree;
145 let has_act = t.contains("Relu") || t.contains("Silu") || t.contains("Gelu");
146 let has_attn = t.contains("Attention") || t.contains("Softmax") || t.contains("Rope");
147 let has_moe = t.contains("GroupedMatMul") || t.contains("TopK");
148 if has_moe {
149 "→ MoE expert block: fuse grouped-matmul + gating"
150 } else if has_attn {
151 "→ attention block: fuse (FusedAttentionBlock)"
152 } else if t.contains("MatMul(") && t.contains("Add(") && has_act {
153 if t.starts_with("Add(") {
154 "→ residual linear+bias+act: fuse (FusedMatMulBiasAct + residual)"
155 } else {
156 "→ linear+bias+act: fuse into one kernel (FusedMatMulBiasAct)"
157 }
158 } else if t.contains("MatMul(") {
159 "→ recurring matmul cone: share/prepack"
160 } else {
161 "→ recurring sub-DAG: decompose once + reuse"
162 }
163}