use crate::registry::{
BackwardKind, FusedOpEntry, FusedOpFamily, FusedOpParams, FusedOps, PatternMatch,
SubgraphPattern, decompose_via_recipe,
};
use crate::{Graph, NodeId, Op};
use fuel_ir::{DType, Shape};
use fuel_kernel_seam_types::shape_expr::ShapeExpr;
use fuel_kernel_seam_types::{OpAttrs, OpTag, PatternNode};
use std::collections::HashMap;
use std::sync::OnceLock;
pub fn entry() -> FusedOpEntry {
FusedOpEntry {
destructive_input: None,
id: FusedOps::RMS_NORM_LAST_DIM,
name: "RmsNormLastDim",
family: FusedOpFamily::Norm,
pattern: SubgraphPattern::Callable(canonical_pattern),
decompose,
backward: BackwardKind::Fused(FusedOps::RMS_NORM_LAST_DIM_BACKWARD),
shape_rule: shape_passthrough,
dtype_rule: dtype_passthrough,
output_views: None,
}
}
fn shape_passthrough(input_shapes: &[Shape], _params: &FusedOpParams) -> Shape {
debug_assert_eq!(input_shapes.len(), 1, "RmsNormLastDim takes one input");
input_shapes[0].clone()
}
fn dtype_passthrough(input_dtypes: &[DType], _params: &FusedOpParams) -> DType {
debug_assert_eq!(input_dtypes.len(), 1, "RmsNormLastDim takes one input");
input_dtypes[0]
}
fn recipe() -> &'static PatternNode {
static RECIPE: OnceLock<PatternNode> = OnceLock::new();
RECIPE.get_or_init(|| {
let axis_last = || OpAttrs {
axis_last: true,
..OpAttrs::default()
};
let same_as_x = || OpAttrs {
target_shape_rel: Some(ShapeExpr::SameAs { operand: 0 }),
..OpAttrs::default()
};
let op = |op, attrs, operands| PatternNode::Op {
op,
attrs,
operands,
};
let x = || PatternNode::Bind { index: 0 };
op(
OpTag::Div,
OpAttrs::default(),
vec![
x(),
op(
OpTag::BroadcastTo,
same_as_x(),
vec![op(
OpTag::Sqrt,
OpAttrs::default(),
vec![
op(
OpTag::AddScalar,
OpAttrs::default(),
vec![op(
OpTag::Unsqueeze,
axis_last(),
vec![op(
OpTag::MeanDim,
axis_last(),
vec![op(OpTag::Sqr, OpAttrs::default(), vec![x()])],
)],
)],
),
],
)],
),
],
)
})
}
fn scalars(params: &FusedOpParams) -> Option<Vec<f64>> {
match params {
FusedOpParams::RmsNormLastDim { eps } => Some(vec![*eps]),
_ => None,
}
}
pub fn decompose(graph: &mut Graph, id: NodeId, params: &FusedOpParams) -> NodeId {
decompose_via_recipe(graph, id, recipe(), scalars(params))
}
pub fn canonical_pattern(graph: &Graph, div_id: NodeId) -> Option<PatternMatch> {
legacy_spelled_pattern(graph, div_id).or_else(|| recipe_spelled_pattern(graph, div_id))
}
fn legacy_spelled_pattern(graph: &Graph, div_id: NodeId) -> Option<PatternMatch> {
match_common(graph, div_id, |op| matches!(op, Op::Reshape(_)))
}
fn recipe_spelled_pattern(graph: &Graph, div_id: NodeId) -> Option<PatternMatch> {
match_common(graph, div_id, |op| matches!(op, Op::Unsqueeze { .. }))
}
fn match_common(
graph: &Graph,
div_id: NodeId,
keepdim_ok: impl Fn(&Op) -> bool,
) -> Option<PatternMatch> {
let div = graph.node(div_id);
if !matches!(div.op, Op::Div) {
return None;
}
if div.inputs.len() != 2 {
return None;
}
let x_id = div.inputs[0];
let denom_bcast_id = div.inputs[1];
let denom_bcast = graph.node(denom_bcast_id);
if !matches!(denom_bcast.op, Op::BroadcastTo(_)) {
return None;
}
if denom_bcast.inputs.len() != 1 {
return None;
}
let denom_id = denom_bcast.inputs[0];
let denom = graph.node(denom_id);
if !matches!(denom.op, Op::Sqrt) {
return None;
}
if denom.inputs.len() != 1 {
return None;
}
let denom_sq_id = denom.inputs[0];
let denom_sq = graph.node(denom_sq_id);
let eps = match denom_sq.op {
Op::AddScalar(e) => e,
_ => return None,
};
if denom_sq.inputs.len() != 1 {
return None;
}
let mean_kd_id = denom_sq.inputs[0];
let mean_kd = graph.node(mean_kd_id);
if !keepdim_ok(&mean_kd.op) {
return None;
}
if mean_kd.inputs.len() != 1 {
return None;
}
let mean_id = mean_kd.inputs[0];
let mean = graph.node(mean_id);
let last_axis_via_mean = match mean.op {
Op::MeanDim(d) => d,
_ => return None,
};
if mean.inputs.len() != 1 {
return None;
}
let sq_id = mean.inputs[0];
let sq = graph.node(sq_id);
if !matches!(sq.op, Op::Sqr) {
return None;
}
if sq.inputs.len() != 1 {
return None;
}
if sq.inputs[0] != x_id {
return None;
}
let x_shape = &graph.node(x_id).shape;
if x_shape.rank() == 0 {
return None;
}
let last = x_shape.rank() - 1;
if last_axis_via_mean != last {
return None;
}
let kd_shape = &graph.node(mean_kd_id).shape;
if kd_shape.rank() != x_shape.rank() {
return None;
}
for axis in 0..x_shape.rank() {
let expected = if axis == last {
1
} else {
x_shape.dims()[axis]
};
if kd_shape.dims()[axis] != expected {
return None;
}
}
let intermediates_with_expected_count = [
(sq_id, 1),
(mean_id, 1),
(mean_kd_id, 1),
(denom_sq_id, 1),
(denom_id, 1),
(denom_bcast_id, 1),
];
let consumer_counts = count_consumers(graph);
for (nid, expected) in intermediates_with_expected_count {
if consumer_counts.get(&nid).copied().unwrap_or(0) != expected {
return None;
}
}
Some(PatternMatch {
bindings: vec![(0, x_id)],
params: FusedOpParams::RmsNormLastDim { eps },
})
}
fn count_consumers(graph: &Graph) -> HashMap<NodeId, usize> {
let mut counts: HashMap<NodeId, usize> = HashMap::new();
let n = graph.len();
for nid in 0..n {
let node = graph.node(NodeId(nid));
for &input in &node.inputs {
*counts.entry(input).or_insert(0) += 1;
}
}
counts
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Node;
use fuel_ir::{DType, Shape};
#[test]
fn canonical_pattern_matches_the_recipe_spelling() {
let mut g = Graph::new();
let sh = Shape::from_dims(&[2, 4]);
let x = g.push(Node {
op: Op::Const,
inputs: vec![],
shape: sh.clone(),
dtype: DType::F32,
});
let params = FusedOpParams::RmsNormLastDim { eps: 1e-5 };
let fused = g.push(Node {
op: Op::Fused(FusedOps::RMS_NORM_LAST_DIM, params.clone()),
inputs: vec![x],
shape: sh,
dtype: DType::F32,
});
let root = decompose(&mut g, fused, ¶ms);
assert_ne!(root, fused, "recipe decompose fires");
let m = canonical_pattern(&g, root).expect("the recipe emission must re-fuse");
assert_eq!(m.bindings, vec![(0, x)], "bound external input = x");
match m.params {
FusedOpParams::RmsNormLastDim { eps } => {
assert_eq!(eps, 1e-5, "eps recovered from the AddScalar");
}
other => panic!("wrong params variant: {other:?}"),
}
}
#[test]
fn canonical_pattern_still_matches_the_legacy_spelling() {
let mut g = Graph::new();
let sh = Shape::from_dims(&[2, 4]);
let kd = Shape::from_dims(&[2, 1]);
let f32_node = |op, inputs, shape| Node {
op,
inputs,
shape,
dtype: DType::F32,
};
let x = g.push(f32_node(Op::Const, vec![], sh.clone()));
let sq = g.push(f32_node(Op::Sqr, vec![x], sh.clone()));
let mean = g.push(f32_node(Op::MeanDim(1), vec![sq], kd.clone()));
let mean_kd = g.push(f32_node(Op::Reshape(kd.clone()), vec![mean], kd.clone()));
let denom_sq = g.push(f32_node(Op::AddScalar(1e-5), vec![mean_kd], kd.clone()));
let denom = g.push(f32_node(Op::Sqrt, vec![denom_sq], kd));
let denom_b = g.push(f32_node(
Op::BroadcastTo(sh.clone()),
vec![denom],
sh.clone(),
));
let div = g.push(f32_node(Op::Div, vec![x, denom_b], sh));
let m = canonical_pattern(&g, div).expect("legacy spelling must still match");
assert_eq!(m.bindings, vec![(0, x)]);
assert!(matches!(m.params, FusedOpParams::RmsNormLastDim { eps } if eps == 1e-5));
}
}