use std::sync::Arc;
use fuel_graph::Op;
use fuel_graph::opt::CapabilityPredicate;
use fuel_ir::dispatch::OpKind;
use fuel_ir::{DType, probe::BackendId};
use crate::dispatch::global_bindings;
pub fn cast_fusion_predicate() -> CapabilityPredicate {
Arc::new(|op: &Op, dtypes: &[DType]| -> bool {
let Some(kind) = op_kind(op) else {
return false;
};
let bindings = global_bindings();
for backend in ALL_BACKENDS {
if bindings.lookup(kind, dtypes, *backend).is_ok() {
return true;
}
}
false
})
}
const ALL_BACKENDS: &[BackendId] = &[
BackendId::Cpu,
BackendId::Cuda,
BackendId::Vulkan,
BackendId::Metal,
];
fn op_kind(op: &Op) -> Option<OpKind> {
match op {
Op::Add => Some(OpKind::AddElementwise),
Op::Sub => Some(OpKind::SubElementwise),
Op::Mul => Some(OpKind::MulElementwise),
Op::Div => Some(OpKind::DivElementwise),
Op::Maximum => Some(OpKind::MaximumElementwise),
Op::Minimum => Some(OpKind::MinimumElementwise),
Op::Pow => Some(OpKind::PowElementwise),
Op::Rem => Some(OpKind::RemElementwise),
Op::Relu => Some(OpKind::ReluElementwise),
Op::Neg => Some(OpKind::NegElementwise),
Op::Sqr => Some(OpKind::SqrElementwise),
Op::Sqrt => Some(OpKind::SqrtElementwise),
Op::Recip => Some(OpKind::RecipElementwise),
Op::Abs => Some(OpKind::AbsElementwise),
Op::Tanh => Some(OpKind::TanhElementwise),
Op::Exp => Some(OpKind::ExpElementwise),
Op::Log => Some(OpKind::LogElementwise),
Op::Sin => Some(OpKind::SinElementwise),
Op::Cos => Some(OpKind::CosElementwise),
Op::Sigmoid => Some(OpKind::SigmoidElementwise),
Op::Silu => Some(OpKind::SiluElementwise),
Op::GeluTanh => Some(OpKind::GeluTanhElementwise),
Op::Step => Some(OpKind::StepElementwise),
Op::Floor => Some(OpKind::FloorElementwise),
Op::Ceil => Some(OpKind::CeilElementwise),
Op::Round => Some(OpKind::RoundElementwise),
Op::Sign => Some(OpKind::SignElementwise),
Op::Erf => Some(OpKind::ErfElementwise),
Op::Gelu => Some(OpKind::GeluElementwise),
Op::Rsqrt => Some(OpKind::RsqrtElementwise),
Op::Equal => Some(OpKind::EqualElementwise),
Op::Ne => Some(OpKind::NotEqualElementwise),
Op::Lt => Some(OpKind::LessElementwise),
Op::Le => Some(OpKind::LessEqualElementwise),
Op::Gt => Some(OpKind::GreaterElementwise),
Op::Ge => Some(OpKind::GreaterEqualElementwise),
Op::SumAll | Op::SumDim(_) => Some(OpKind::SumReduce),
Op::MaxAll | Op::MaxDim(_) => Some(OpKind::MaxReduce),
Op::MinAll | Op::MinDim(_) => Some(OpKind::MinReduce),
Op::MeanAll | Op::MeanDim(_) => Some(OpKind::MeanReduce),
Op::ReduceSumTo(_) => Some(OpKind::ReduceSumTo),
Op::ReduceMaxTo(_) => Some(OpKind::ReduceMaxTo),
Op::MatMul => Some(OpKind::MatMul),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
use fuel_graph::NodeHandle;
use fuel_graph::opt::{CastFusionRule, RuleRegistry};
use fuel_graph::topo_order_multi;
use fuel_ir::Shape;
use std::sync::Arc as StdArc;
fn cpu_dev() -> &'static StdArc<dyn fuel_backend_contract::DynBackendDevice> {
use std::sync::OnceLock;
static D: OnceLock<StdArc<dyn fuel_backend_contract::DynBackendDevice>> = OnceLock::new();
D.get_or_init(|| StdArc::new(fuel_cpu_backend::dyn_impl::CpuBackendDevice))
}
#[test]
fn predicate_accepts_registered_op() {
let predicate = cast_fusion_predicate();
let dtypes = [DType::F32, DType::F32];
assert!(predicate(&Op::Neg, &dtypes));
}
#[test]
fn predicate_rejects_unmapped_op() {
let predicate = cast_fusion_predicate();
let dtypes = [DType::F32];
assert!(!predicate(&Op::Const, &dtypes));
}
#[test]
fn end_to_end_cast_neg_collapses_to_neg() {
let _bindings = global_bindings();
let x = NodeHandle::from_f32(vec![1.0_f32; 4], Shape::from_dims(&[4]), cpu_dev()).unwrap();
let xc = x.cast(DType::BF16);
let y = xc.neg();
let graph = y.graph().clone();
let registry =
RuleRegistry::new().with_rule(Box::new(CastFusionRule::new(cast_fusion_predicate())));
let new_roots = registry.optimize_to_fixpoint(&graph, &[y.id()]);
assert_eq!(new_roots.len(), 1);
let new_root = new_roots[0];
let g = graph.read().unwrap();
let reachable = topo_order_multi(&g, &[new_root]);
let cast_count = reachable
.iter()
.filter(|&&n| matches!(g.node(n).op, Op::Cast(_)))
.count();
assert_eq!(
cast_count, 0,
"Cast should be eliminated when NegElementwise is registered for both [F32,F32] and [BF16,BF16]"
);
assert!(matches!(g.node(new_root).op, Op::Neg));
assert_eq!(g.node(new_root).inputs, vec![x.id()]);
}
#[test]
fn live_binding_table_contains_neg_for_both_dtypes() {
let _bindings = global_bindings();
let predicate = cast_fusion_predicate();
assert!(
predicate(&Op::Neg, &[DType::F32, DType::F32]),
"fuel-cpu-backend's standard registration includes Neg[F32]"
);
assert!(
predicate(&Op::Neg, &[DType::BF16, DType::BF16]),
"fuel-cpu-backend's standard registration includes Neg[BF16]"
);
}
}