use crate::registry::{
BackwardKind, FusedOpEntry, FusedOpFamily, FusedOpParams, FusedOps, PatternMatch,
SubgraphPattern, decompose_via_recipe,
};
use crate::{Graph, NodeId};
use fuel_ir::{DType, Shape};
use fuel_kernel_seam_types::{OpAttrs, OpTag, PatternNode};
use std::sync::OnceLock;
pub fn entry() -> FusedOpEntry {
FusedOpEntry {
destructive_input: Some(0),
id: FusedOps::INPLACE_AFFINE,
name: "InplaceAffine",
family: FusedOpFamily::Forward,
pattern: SubgraphPattern::Callable(canonical_pattern),
decompose,
backward: BackwardKind::NotDifferentiable,
shape_rule,
dtype_rule,
output_views: None,
}
}
fn shape_rule(input_shapes: &[Shape], _params: &FusedOpParams) -> Shape {
debug_assert_eq!(
input_shapes.len(),
1,
"InplaceAffine takes 1 input (the mutated tensor)",
);
input_shapes[0].clone()
}
fn dtype_rule(input_dtypes: &[DType], _params: &FusedOpParams) -> DType {
debug_assert_eq!(input_dtypes.len(), 1, "InplaceAffine takes 1 input",);
input_dtypes[0]
}
fn recipe() -> &'static PatternNode {
static RECIPE: OnceLock<PatternNode> = OnceLock::new();
RECIPE.get_or_init(|| {
let op = |op, attrs, operands| PatternNode::Op {
op,
attrs,
operands,
};
let x = || PatternNode::Bind { index: 0 };
let mul_x = op(OpTag::MulScalar, OpAttrs::default(), vec![x()]);
op(OpTag::AddScalar, OpAttrs::default(), vec![mul_x])
})
}
fn scalars(params: &FusedOpParams) -> Option<Vec<f64>> {
match params {
FusedOpParams::InplaceAffine { mul, add } => Some(vec![*add, *mul]),
_ => 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, _root: NodeId) -> Option<PatternMatch> {
None
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{Node, Op};
fn fused_inplace_affine(g: &mut Graph, dims: &[usize], mul: f64, add: f64) -> NodeId {
let x = g.push(Node {
op: Op::Const,
inputs: vec![],
shape: Shape::from_dims(dims),
dtype: DType::F32,
});
g.push(Node {
op: Op::Fused(
FusedOps::INPLACE_AFFINE,
FusedOpParams::InplaceAffine { mul, add },
),
inputs: vec![x],
shape: Shape::from_dims(dims),
dtype: DType::F32,
})
}
#[test]
fn inplace_affine_decompose_lowers_to_mul_then_add_scalar() {
let mut g = Graph::new();
let (mul, add) = (2.0f64, 1.0f64);
let fused = fused_inplace_affine(&mut g, &[4], mul, add);
let x = g.node(fused).inputs[0];
let out_shape = g.node(fused).shape.clone();
let params = FusedOpParams::InplaceAffine { mul, add };
let root = decompose(&mut g, fused, ¶ms);
assert_ne!(
root, fused,
"InplaceAffine now lowers to a functional subgraph"
);
assert!(
matches!(g.node(root).op, Op::AddScalar(v) if v == add),
"root is AddScalar(add); got {:?}",
g.node(root).op,
);
let ms = g.node(root).inputs[0];
assert!(
matches!(g.node(ms).op, Op::MulScalar(v) if v == mul),
"operand is MulScalar(mul); got {:?}",
g.node(ms).op,
);
assert_eq!(
g.node(ms).inputs,
vec![x],
"MulScalar reads the fused node's input 0"
);
assert_eq!(
g.node(root).shape,
out_shape,
"value form preserves the input shape"
);
assert_eq!(g.node(root).dtype, DType::F32);
}
#[test]
fn inplace_affine_destructive_input_survives_migration() {
let op = Op::Fused(
FusedOps::INPLACE_AFFINE,
FusedOpParams::InplaceAffine { mul: 2.0, add: 1.0 },
);
assert_eq!(
op.destructive_input(),
Some(0),
"INPLACE_AFFINE still marks input 0 destructive after the decompose migration",
);
}
#[test]
fn inplace_affine_wrong_params_is_a_fixpoint_not_a_crash() {
let mut g = Graph::new();
let fused = fused_inplace_affine(&mut g, &[4], 2.0, 1.0);
let before = g.len();
let out = decompose(&mut g, fused, &FusedOpParams::Rope);
assert_eq!(out, fused, "wrong params => typed decline => fixpoint");
assert_eq!(g.len(), before, "declined before any emission");
}
}