use rlx_ir::{Graph, NodeId, Op};
use std::collections::{HashMap, HashSet};
#[derive(Debug)]
pub enum Segment {
Thunks { start: usize, end: usize },
MpsGraph(MpsGraphSegment),
}
#[derive(Debug)]
pub struct MpsGraphSegment {
pub nodes: Vec<NodeId>,
pub boundary_inputs: Vec<NodeId>,
pub boundary_outputs: Vec<NodeId>,
}
fn mps_segment_eligible(op: &Op) -> bool {
use rlx_ir::op::Activation;
match op {
Op::Input { .. } | Op::Param { .. } | Op::Constant { .. } => false,
Op::Attention { .. } => false,
Op::MatMul
| Op::FusedMatMulBiasAct { .. }
| Op::Activation(Activation::Gelu)
| Op::Activation(Activation::Silu)
| Op::Binary(_)
| Op::LayerNorm { .. }
| Op::RmsNorm { .. }
| Op::FusedResidualLN { .. }
| Op::FusedResidualRmsNorm { .. }
| Op::Reshape { .. }
| Op::Expand { .. }
| Op::Cast { .. }
| Op::Gather { .. }
| Op::Narrow { .. }
| Op::FusedSwiGLU { .. }
| Op::Concat { .. }
| Op::Rope { .. } => true,
_ => false,
}
}
pub fn segment(graph: &Graph) -> Vec<Segment> {
let nodes = graph.nodes();
let mut segments: Vec<Segment> = Vec::new();
let mut consumers: HashMap<NodeId, Vec<NodeId>> = HashMap::new();
for node in nodes {
for &input in &node.inputs {
consumers.entry(input).or_default().push(node.id);
}
}
let graph_outputs: HashSet<NodeId> = graph.outputs.iter().copied().collect();
let mut pending_nodes: Vec<NodeId> = Vec::new();
let mut idx_in_schedule: usize = 0;
let flush_mps = |pending: &mut Vec<NodeId>, segs: &mut Vec<Segment>| {
if pending.is_empty() {
return;
}
let pending_set: HashSet<NodeId> = pending.iter().copied().collect();
let mut bin_set: HashSet<NodeId> = HashSet::new();
for &id in pending.iter() {
for &input in &graph_node(graph, id).inputs {
if !pending_set.contains(&input) {
bin_set.insert(input);
}
}
}
let mut bout_set: HashSet<NodeId> = HashSet::new();
for &id in pending.iter() {
let used_outside = consumers
.get(&id)
.map(|cs| cs.iter().any(|c| !pending_set.contains(c)))
.unwrap_or(false);
if used_outside || graph_outputs.contains(&id) {
bout_set.insert(id);
}
}
let mut boundary_inputs: Vec<NodeId> = bin_set.into_iter().collect();
boundary_inputs.sort_by_key(|id| id.0);
let mut boundary_outputs: Vec<NodeId> = bout_set.into_iter().collect();
boundary_outputs.sort_by_key(|id| id.0);
segs.push(Segment::MpsGraph(MpsGraphSegment {
nodes: pending.clone(),
boundary_inputs,
boundary_outputs,
}));
pending.clear();
};
for node in nodes {
if matches!(
node.op,
Op::Input { .. } | Op::Param { .. } | Op::Constant { .. }
) {
idx_in_schedule += 1;
continue;
}
if mps_segment_eligible(&node.op) {
pending_nodes.push(node.id);
} else {
flush_mps(&mut pending_nodes, &mut segments);
segments.push(Segment::Thunks {
start: idx_in_schedule,
end: idx_in_schedule + 1,
});
}
idx_in_schedule += 1;
}
flush_mps(&mut pending_nodes, &mut segments);
let mut coalesced: Vec<Segment> = Vec::new();
for seg in segments {
match (coalesced.last_mut(), seg) {
(Some(Segment::Thunks { end, .. }), Segment::Thunks { start: ns, end: ne })
if *end == ns =>
{
*end = ne;
}
(_, s) => coalesced.push(s),
}
}
coalesced
}
#[inline]
fn graph_node(graph: &Graph, id: NodeId) -> &rlx_ir::Node {
graph.node(id)
}
#[cfg(test)]
mod tests {
use super::*;
use rlx_ir::op::{Activation, BinaryOp, MaskKind};
use rlx_ir::{DType, Op, Shape};
fn count_segments(segs: &[Segment]) -> (usize, usize) {
let mut mps = 0;
let mut th = 0;
for s in segs {
match s {
Segment::MpsGraph(_) => mps += 1,
Segment::Thunks { .. } => th += 1,
}
}
(mps, th)
}
#[test]
fn segment_pure_mlp_yields_one_mps_segment() {
let f = DType::F32;
let mut g = Graph::new("mlp");
let x = g.input("x", Shape::new(&[1, 4], f));
let w = g.param("w", Shape::new(&[4, 4], f));
let mm = g.matmul(x, w, Shape::new(&[1, 4], f));
let r = g.activation(Activation::Gelu, mm, Shape::new(&[1, 4], f));
g.set_outputs(vec![r]);
let segs = segment(&g);
let (mps, th) = count_segments(&segs);
assert_eq!(
(mps, th),
(1, 0),
"pure MLP should be 1 MPSGraph segment + 0 thunk segments, got mps={mps} th={th}"
);
}
#[test]
fn segment_around_attention_splits_correctly() {
let f = DType::F32;
let mut g = Graph::new("attn_split");
let x = g.input("x", Shape::new(&[1, 4, 8], f));
let mask = g.input("mask", Shape::new(&[1, 4], f));
let w1 = g.param("w1", Shape::new(&[8, 24], f));
let qkv = g.matmul(x, w1, Shape::new(&[1, 4, 24], f));
let q = g.add_node(
Op::Narrow {
axis: 2,
start: 0,
len: 8,
},
vec![qkv],
Shape::new(&[1, 4, 8], f),
);
let k = g.add_node(
Op::Narrow {
axis: 2,
start: 8,
len: 8,
},
vec![qkv],
Shape::new(&[1, 4, 8], f),
);
let v = g.add_node(
Op::Narrow {
axis: 2,
start: 16,
len: 8,
},
vec![qkv],
Shape::new(&[1, 4, 8], f),
);
let attn = g.add_node(
Op::Attention {
num_heads: 2,
head_dim: 4,
v_head_dim: None,
mask_kind: MaskKind::Custom,
score_scale: None,
attn_logit_softcap: None,
},
vec![q, k, v, mask],
Shape::new(&[1, 4, 8], f),
);
let w2 = g.param("w2", Shape::new(&[8, 8], f));
let out = g.matmul(attn, w2, Shape::new(&[1, 4, 8], f));
g.set_outputs(vec![out]);
let segs = segment(&g);
let (mps, th) = count_segments(&segs);
assert_eq!(
(mps, th),
(2, 1),
"expected 2 MPSGraph segments + 1 thunk segment around attention, got mps={mps} th={th}"
);
}
#[test]
fn segment_attention_boundary_inputs_correct() {
let f = DType::F32;
let mut g = Graph::new("attn_boundary");
let x = g.input("x", Shape::new(&[1, 4, 8], f));
let mask = g.input("mask", Shape::new(&[1, 4], f));
let w1 = g.param("w1", Shape::new(&[8, 24], f));
let qkv = g.matmul(x, w1, Shape::new(&[1, 4, 24], f));
let q = g.add_node(
Op::Narrow {
axis: 2,
start: 0,
len: 8,
},
vec![qkv],
Shape::new(&[1, 4, 8], f),
);
let k = g.add_node(
Op::Narrow {
axis: 2,
start: 8,
len: 8,
},
vec![qkv],
Shape::new(&[1, 4, 8], f),
);
let v = g.add_node(
Op::Narrow {
axis: 2,
start: 16,
len: 8,
},
vec![qkv],
Shape::new(&[1, 4, 8], f),
);
let attn_id = g.add_node(
Op::Attention {
num_heads: 2,
head_dim: 4,
v_head_dim: None,
mask_kind: MaskKind::Custom,
score_scale: None,
attn_logit_softcap: None,
},
vec![q, k, v, mask],
Shape::new(&[1, 4, 8], f),
);
let w2 = g.param("w2", Shape::new(&[8, 8], f));
let _ = g.add_node(
Op::Binary(BinaryOp::Add),
vec![attn_id, w2],
Shape::new(&[1, 4, 8], f),
);
g.set_outputs(vec![attn_id]);
let segs = segment(&g);
let mut found_post = false;
let mut saw_attn_thunk = false;
for s in &segs {
match s {
Segment::Thunks { .. } => saw_attn_thunk = true,
Segment::MpsGraph(seg) if saw_attn_thunk => {
found_post = seg.boundary_inputs.contains(&attn_id);
break;
}
_ => {}
}
}
assert!(
found_post,
"post-attention MPSGraph segment should list attention output as boundary input"
);
}
}