use rlx_ir::{Graph, Op};
use std::collections::{HashMap, HashSet};
#[derive(Clone, Debug)]
pub struct FlowPattern {
pub depth: usize,
pub count: usize,
pub tree: String,
pub sites: Vec<u32>,
}
impl FlowPattern {
pub fn score(&self) -> usize {
self.count * (self.tree.matches('(').count() + 1)
}
}
pub fn op_name(op: &Op) -> String {
match op {
Op::Activation(a) => format!("{a:?}"),
Op::Binary(b) => format!("{b:?}"),
Op::Compare(c) => format!("Cmp{c:?}"),
Op::Input { .. } => "in".into(),
Op::Param { .. } => "w".into(),
Op::Constant { .. } => "k".into(),
other => format!("{:?}", other.kind()),
}
}
fn sig(
ops: &[String],
inputs: &[Vec<usize>],
node: usize,
depth: usize,
memo: &mut HashMap<(usize, usize), String>,
) -> String {
if let Some(s) = memo.get(&(node, depth)) {
return s.clone();
}
let name = ops[node].clone();
let s = if depth == 0 || inputs[node].is_empty() {
name
} else {
let kids: Vec<String> = inputs[node]
.iter()
.map(|&i| sig(ops, inputs, i, depth - 1, memo))
.collect();
format!("{name}({})", kids.join(","))
};
memo.insert((node, depth), s.clone());
s
}
pub fn repeated_flow_patterns_on(
ops: &[String],
inputs: &[Vec<usize>],
min_depth: usize,
max_depth: usize,
min_count: usize,
) -> Vec<FlowPattern> {
let mut memo: HashMap<(usize, usize), String> = HashMap::new();
let mut out: Vec<FlowPattern> = Vec::new();
let mut seen_tree: HashSet<String> = HashSet::new();
for depth in min_depth..=max_depth {
let mut groups: HashMap<String, Vec<u32>> = HashMap::new();
for node in 0..ops.len() {
if inputs[node].is_empty() {
continue; }
let s = sig(ops, inputs, node, depth, &mut memo);
if !s.contains('(') {
continue; }
groups.entry(s).or_default().push(node as u32);
}
for (tree, sites) in groups {
if sites.len() >= min_count && seen_tree.insert(tree.clone()) {
out.push(FlowPattern {
depth,
count: sites.len(),
sites: sites.into_iter().take(6).collect(),
tree,
});
}
}
}
out.sort_by(|a, b| b.score().cmp(&a.score()).then(b.count.cmp(&a.count)));
out
}
pub fn repeated_flow_patterns(
g: &Graph,
min_depth: usize,
max_depth: usize,
min_count: usize,
) -> Vec<FlowPattern> {
let ops: Vec<String> = g.nodes().iter().map(|n| op_name(&n.op)).collect();
let inputs: Vec<Vec<usize>> = g
.nodes()
.iter()
.map(|n| n.inputs.iter().map(|i| i.0 as usize).collect())
.collect();
repeated_flow_patterns_on(&ops, &inputs, min_depth, max_depth, min_count)
}
pub fn decomposition_hint(p: &FlowPattern) -> &'static str {
let t = &p.tree;
let has_act = t.contains("Relu") || t.contains("Silu") || t.contains("Gelu");
let has_attn = t.contains("Attention") || t.contains("Softmax") || t.contains("Rope");
let has_moe = t.contains("GroupedMatMul") || t.contains("TopK");
if has_moe {
"→ MoE expert block: fuse grouped-matmul + gating"
} else if has_attn {
"→ attention block: fuse (FusedAttentionBlock)"
} else if t.contains("MatMul(") && t.contains("Add(") && has_act {
if t.starts_with("Add(") {
"→ residual linear+bias+act: fuse (FusedMatMulBiasAct + residual)"
} else {
"→ linear+bias+act: fuse into one kernel (FusedMatMulBiasAct)"
}
} else if t.contains("MatMul(") {
"→ recurring matmul cone: share/prepack"
} else {
"→ recurring sub-DAG: decompose once + reuse"
}
}