use crate::registry::{FusedOpEntry, FusedOpId, FusedOpParams, SubgraphPattern, default_registry};
use crate::{Graph, Node, NodeId, Op, ScanEmit, ScanRole, SharedGraph, topo_order_multi};
use fuel_ir::{DType, DeviceLocation};
use std::collections::HashMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum RuleFamily {
Lowering,
Fusion,
Algebraic,
}
pub trait Rule: Send + Sync {
fn name(&self) -> &'static str;
fn family(&self) -> RuleFamily;
fn matches(&self, graph: &Graph, id: NodeId) -> bool;
fn rewrite(&self, graph: &mut Graph, id: NodeId, remap: &mut HashMap<NodeId, NodeId>);
}
#[derive(Default)]
pub struct RuleRegistry {
rules: Vec<Box<dyn Rule>>,
}
impl RuleRegistry {
pub fn new() -> Self {
Self { rules: Vec::new() }
}
pub fn with_rule(mut self, rule: Box<dyn Rule>) -> Self {
self.rules.push(rule);
self
}
pub fn default_rules() -> Self {
Self::capability_gated_rules(|_| true).0
}
pub fn capability_gated_rules(
has_kernel: impl Fn(FusedOpId) -> bool,
) -> (Self, Vec<FusedOpId>) {
let registry = default_registry();
let mut r = Self::new();
let mut gated_out = Vec::new();
for entry in registry.entries_iter() {
r = r.with_rule(Box::new(LoweringRule::from_entry(entry)));
if has_kernel(entry.id) {
r = r.with_rule(Box::new(FusionRule::from_entry(entry)));
} else {
gated_out.push(entry.id);
}
}
for e in crate::runtime_fused::runtime_entries() {
r = r.with_rule(Box::new(LoweringRule::runtime(e.id)));
if has_kernel(e.id) {
r = r.with_rule(Box::new(FusionRule::declarative(
e.id,
crate::registry::PatternTree {
root: e.region,
params: FusedOpParams::Runtime {
scalars: Vec::new(),
},
},
)));
} else {
gated_out.push(e.id);
}
}
(r, gated_out)
}
pub fn lowering_only() -> Self {
let registry = default_registry();
let mut r = Self::new();
for entry in registry.entries_iter() {
r = r.with_rule(Box::new(LoweringRule::from_entry(entry)));
}
for e in crate::runtime_fused::runtime_entries() {
r = r.with_rule(Box::new(LoweringRule::runtime(e.id)));
}
r
}
pub fn len(&self) -> usize {
self.rules.len()
}
pub fn is_empty(&self) -> bool {
self.rules.is_empty()
}
pub fn optimize_to_fixpoint(&self, graph: &SharedGraph, roots: &[NodeId]) -> Vec<NodeId> {
let mut roots = roots.to_vec();
if self.rules.is_empty() {
return roots;
}
loop {
let any = self.run_pass(RuleFamily::Lowering, graph, &mut roots);
if !any {
break;
}
}
loop {
let any = self.run_pass(RuleFamily::Fusion, graph, &mut roots);
if !any {
break;
}
}
loop {
let any = self.run_pass(RuleFamily::Algebraic, graph, &mut roots);
if !any {
break;
}
}
roots
}
fn run_pass(&self, family: RuleFamily, graph: &SharedGraph, roots: &mut [NodeId]) -> bool {
let order = {
let g = graph.read().unwrap();
topo_order_multi(&g, roots)
};
let family_rules: Vec<&Box<dyn Rule>> =
self.rules.iter().filter(|r| r.family() == family).collect();
if family_rules.is_empty() {
return false;
}
let mut remap: HashMap<NodeId, NodeId> = HashMap::new();
let mut any_fired = false;
for id in order {
if remap.contains_key(&id) {
continue;
}
let firing_rule_idx = {
let g = graph.read().unwrap();
family_rules.iter().position(|r| r.matches(&g, id))
};
if let Some(idx) = firing_rule_idx {
let mut g = graph.write().unwrap();
let before = remap.len();
family_rules[idx].rewrite(&mut g, id, &mut remap);
if remap.len() != before {
any_fired = true;
}
}
}
if !any_fired {
return false;
}
{
let mut g = graph.write().unwrap();
let n_nodes = g.len();
for nid in 0..n_nodes {
let inputs_len = g.node(NodeId(nid)).inputs.len();
for i in 0..inputs_len {
let cur = g.node(NodeId(nid)).inputs[i];
if let Some(&new) = remap.get(&cur) {
g.rewrite_input(NodeId(nid), cur, new);
}
}
}
}
for r in roots.iter_mut() {
if let Some(&new) = remap.get(r) {
*r = new;
}
}
true
}
}
pub fn lower_to_base_map(graph: &SharedGraph, roots: &[NodeId]) -> Vec<NodeId> {
RuleRegistry::lowering_only().optimize_to_fixpoint(graph, roots)
}
pub fn base_map_hash(graph: &Graph, root: NodeId) -> u64 {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
fn fold_const_bytes(graph: &Graph, id: NodeId, hasher: &mut DefaultHasher) {
let Some(slot) = graph.storage_for(id) else {
return;
};
let Ok(guard) = slot.try_read() else { return };
let Ok(host) = guard.to_cpu_storage() else {
return;
};
fold_host_buffer(&host, hasher);
}
fn fold_host_buffer(buf: &fuel_ir::HostBuffer, hasher: &mut DefaultHasher) {
use fuel_ir::HostBuffer as H;
match buf {
H::U8(v) | H::F6E2M3(v) | H::F6E3M2(v) | H::F4(v) | H::F8E8M0(v) | H::Bool(v) => {
v.hash(hasher)
}
H::I8(v) => v.hash(hasher),
H::U32(v) => v.hash(hasher),
H::I16(v) => v.hash(hasher),
H::I32(v) => v.hash(hasher),
H::I64(v) => v.hash(hasher),
H::BF16(v) => {
for x in v {
x.to_bits().hash(hasher);
}
}
H::F16(v) => {
for x in v {
x.to_bits().hash(hasher);
}
}
H::F32(v) => {
for x in v {
x.to_bits().hash(hasher);
}
}
H::F64(v) => {
for x in v {
x.to_bits().hash(hasher);
}
}
H::F8E4M3(v) => {
for x in v {
x.to_bits().hash(hasher);
}
}
}
}
fn go(graph: &Graph, id: NodeId, memo: &mut HashMap<NodeId, u64>) -> u64 {
if let Some(&h) = memo.get(&id) {
return h;
}
let n = graph.node(id);
let mut hasher = DefaultHasher::new();
match op_key(&n.op) {
Some(k) => k.hash(&mut hasher),
None => {
std::mem::discriminant(&n.op).hash(&mut hasher);
n.shape.dims().hash(&mut hasher);
n.dtype.hash(&mut hasher);
if matches!(n.op, Op::Const) {
fold_const_bytes(graph, id, &mut hasher);
}
}
}
let mut child_hashes: Vec<u64> = n.inputs.iter().map(|&c| go(graph, c, memo)).collect();
if is_commutative(&n.op) {
child_hashes.sort_unstable();
}
child_hashes.hash(&mut hasher);
let h = hasher.finish();
memo.insert(id, h);
h
}
go(graph, root, &mut HashMap::new())
}
pub struct LoweringRule {
id: FusedOpId,
decompose: fn(&mut Graph, NodeId, &FusedOpParams) -> NodeId,
}
impl LoweringRule {
pub fn from_entry(entry: &FusedOpEntry) -> Self {
Self {
id: entry.id,
decompose: entry.decompose,
}
}
pub fn runtime(id: FusedOpId) -> Self {
Self {
id,
decompose: crate::runtime_fused::runtime_lowering_decompose,
}
}
}
impl Rule for LoweringRule {
fn name(&self) -> &'static str {
"FusedOpLowering"
}
fn family(&self) -> RuleFamily {
RuleFamily::Lowering
}
fn matches(&self, graph: &Graph, id: NodeId) -> bool {
let node = graph.node(id);
match &node.op {
Op::Fused(fid, _) if *fid == self.id => true,
_ => false,
}
}
fn rewrite(&self, graph: &mut Graph, id: NodeId, remap: &mut HashMap<NodeId, NodeId>) {
let params = match &graph.node(id).op {
Op::Fused(_, p) => p.clone(),
other => unreachable!(
"LoweringRule::rewrite reached with non-matching op {other:?} \
— matcher contract violated"
),
};
let new_id = (self.decompose)(graph, id, ¶ms);
if new_id != id {
remap.insert(id, new_id);
}
}
}
pub struct FusionRule {
id: FusedOpId,
pattern: PatternKind,
}
#[expect(
clippy::large_enum_variant,
reason = "matcher pattern enum; boxing the large variant is deferred to the same follow-up as PatternNode (docs/gaps.md GAP-215) -- a cross-cutting change to construction/match sites, not a lint-commit change"
)]
enum PatternKind {
Callable(fn(&Graph, NodeId) -> Option<crate::registry::PatternMatch>),
Declarative(crate::registry::PatternTree),
}
impl FusionRule {
pub fn from_entry(entry: &FusedOpEntry) -> Self {
let pattern = match &entry.pattern {
SubgraphPattern::Callable(f) => PatternKind::Callable(*f),
SubgraphPattern::Declarative(tree) => PatternKind::Declarative(tree.clone()),
};
Self {
id: entry.id,
pattern,
}
}
pub fn declarative(id: FusedOpId, tree: crate::registry::PatternTree) -> Self {
Self {
id,
pattern: PatternKind::Declarative(tree),
}
}
}
impl Rule for FusionRule {
fn name(&self) -> &'static str {
"FusedOpFusion"
}
fn family(&self) -> RuleFamily {
RuleFamily::Fusion
}
fn matches(&self, graph: &Graph, id: NodeId) -> bool {
match &self.pattern {
PatternKind::Callable(f) => (*f)(graph, id).is_some(),
PatternKind::Declarative(tree) => {
let consumers = |n: NodeId| {
(0..graph.len())
.filter(|&i| graph.node(NodeId(i)).inputs.contains(&n))
.count()
};
crate::jit::match_region(graph, id, &tree.root, &consumers).is_some()
}
}
}
fn rewrite(&self, graph: &mut Graph, id: NodeId, remap: &mut HashMap<NodeId, NodeId>) {
let pattern_match = match &self.pattern {
PatternKind::Callable(f) => (*f)(graph, id)
.expect("rewrite called with non-matching id — matcher contract violated"),
PatternKind::Declarative(tree) => {
let consumers = |n: NodeId| {
(0..graph.len())
.filter(|&i| graph.node(NodeId(i)).inputs.contains(&n))
.count()
};
let (inputs, extracted) =
crate::jit::match_region_extract(graph, id, &tree.root, &consumers)
.expect("rewrite called with non-matching declarative pattern");
let params = match &tree.params {
FusedOpParams::Runtime { .. } => FusedOpParams::Runtime { scalars: extracted },
other => other.clone(),
};
crate::registry::PatternMatch {
bindings: inputs.iter().enumerate().map(|(i, n)| (i, *n)).collect(),
params,
}
}
};
let mut sorted = pattern_match.bindings.clone();
sorted.sort_by_key(|(idx, _)| *idx);
for (expected_idx, (got_idx, _)) in sorted.iter().enumerate() {
debug_assert_eq!(
*got_idx, expected_idx,
"FusionRule: bindings must be dense [0..N); pattern for id \
{:?} returned non-contiguous indices",
self.id,
);
}
let inputs: Vec<NodeId> = sorted.iter().map(|(_, n)| *n).collect();
let params = pattern_match.params;
let dtype = graph.node(id).dtype;
let shape = graph.node(id).shape.clone();
let new_id = graph.push(Node {
op: Op::Fused(self.id, params),
inputs,
shape,
dtype,
});
remap.insert(id, new_id);
}
}
use std::sync::Arc;
pub type CapabilityPredicate = Arc<dyn Fn(&Op, &[DType]) -> bool + Send + Sync>;
pub struct CastFusionRule {
capabilities: CapabilityPredicate,
}
impl CastFusionRule {
pub fn new(capabilities: CapabilityPredicate) -> Self {
Self { capabilities }
}
fn find_eligible_cast_input(
&self,
graph: &Graph,
id: NodeId,
) -> Option<(usize, NodeId, Vec<fuel_ir::DType>)> {
let node = graph.node(id);
if matches!(node.op, Op::Cast(_)) {
return None;
}
for (idx, &input_id) in node.inputs.iter().enumerate() {
if !matches!(graph.node(input_id).op, Op::Cast(_)) {
continue;
}
if node.dtype != graph.node(input_id).dtype {
continue;
}
let mut prev = id;
let mut current = input_id;
let final_src_id = loop {
let current_node = graph.node(current);
if !matches!(current_node.op, Op::Cast(_)) {
break current;
}
if !is_only_consumer(graph, current, prev) {
break current;
}
let Some(&next) = current_node.inputs.first() else {
break current;
};
prev = current;
current = next;
};
if final_src_id == input_id {
continue;
}
let final_src_dtype = graph.node(final_src_id).dtype;
let mut dtypes: Vec<fuel_ir::DType> = node
.inputs
.iter()
.map(|&iid| {
if iid == input_id {
final_src_dtype
} else {
graph.node(iid).dtype
}
})
.collect();
dtypes.push(final_src_dtype);
if (self.capabilities)(&node.op, &dtypes) {
return Some((idx, final_src_id, dtypes));
}
}
None
}
}
fn is_only_consumer(graph: &Graph, target: NodeId, expected_consumer: NodeId) -> bool {
let mut found_others = false;
for i in 0..graph.len() {
let nid = NodeId(i);
if nid == expected_consumer {
continue;
}
if graph.node(nid).inputs.contains(&target) {
found_others = true;
break;
}
}
!found_others
}
impl Rule for CastFusionRule {
fn name(&self) -> &'static str {
"CastFusion"
}
fn family(&self) -> RuleFamily {
RuleFamily::Algebraic
}
fn matches(&self, graph: &Graph, id: NodeId) -> bool {
self.find_eligible_cast_input(graph, id).is_some()
}
fn rewrite(&self, graph: &mut Graph, id: NodeId, remap: &mut HashMap<NodeId, NodeId>) {
let (idx, final_src_id, _dtypes) = self
.find_eligible_cast_input(graph, id)
.expect("CastFusionRule::rewrite called on non-matching node");
let node = graph.node(id).clone();
let final_src_dtype = graph.node(final_src_id).dtype;
let mut new_inputs = node.inputs.clone();
new_inputs[idx] = final_src_id;
let new_id = graph.push(Node {
op: node.op.clone(),
inputs: new_inputs,
shape: node.shape.clone(),
dtype: final_src_dtype,
});
remap.insert(id, new_id);
}
}
#[derive(Debug, Hash, PartialEq, Eq)]
struct OpKey {
tag: u16,
ints: Vec<i64>,
bits: Vec<u64>,
dims: Vec<usize>,
shape: Option<Vec<usize>>,
dtype: Option<u32>,
}
fn op_key(op: &Op) -> Option<OpKey> {
let (tag, ints, bits, dims, shape, dtype) = match op {
Op::Const => return None,
Op::Add => (1, vec![], vec![], vec![], None, None),
Op::Sub => (2, vec![], vec![], vec![], None, None),
Op::Mul => (3, vec![], vec![], vec![], None, None),
Op::Div => (4, vec![], vec![], vec![], None, None),
Op::Neg => (10, vec![], vec![], vec![], None, None),
Op::Sqr => (11, vec![], vec![], vec![], None, None),
Op::Sqrt => (12, vec![], vec![], vec![], None, None),
Op::Exp => (13, vec![], vec![], vec![], None, None),
Op::Log => (14, vec![], vec![], vec![], None, None),
Op::Sin => (15, vec![], vec![], vec![], None, None),
Op::Cos => (16, vec![], vec![], vec![], None, None),
Op::Tanh => (17, vec![], vec![], vec![], None, None),
Op::Sigmoid => (18, vec![], vec![], vec![], None, None),
Op::Silu => (19, vec![], vec![], vec![], None, None),
Op::GeluTanh => (20, vec![], vec![], vec![], None, None),
Op::Relu => (21, vec![], vec![], vec![], None, None),
Op::Step => (22, vec![], vec![], vec![], None, None),
Op::Recip => (23, vec![], vec![], vec![], None, None),
Op::Abs => (24, vec![], vec![], vec![], None, None),
Op::Equal => (25, vec![], vec![], vec![], None, None),
Op::Ne => (26, vec![], vec![], vec![], None, None),
Op::Lt => (27, vec![], vec![], vec![], None, None),
Op::Le => (28, vec![], vec![], vec![], None, None),
Op::Gt => (29, vec![], vec![], vec![], None, None),
Op::Ge => (33, vec![], vec![], vec![], None, None),
Op::Where => (34, vec![], vec![], vec![], None, None),
Op::Floor => (35, vec![], vec![], vec![], None, None),
Op::Ceil => (36, vec![], vec![], vec![], None, None),
Op::Round => (37, vec![], vec![], vec![], None, None),
Op::Sign => (38, vec![], vec![], vec![], None, None),
Op::Erf => (39, vec![], vec![], vec![], None, None),
Op::Gelu => (47, vec![], vec![], vec![], None, None),
Op::Pow => (49, vec![], vec![], vec![], None, None),
Op::Rsqrt => (54, vec![], vec![], vec![], None, None),
Op::Rem => (55, vec![], vec![], vec![], None, None),
Op::Flip { dim } => (56, vec![*dim as i64], vec![], vec![], None, None),
Op::Roll { dim, shift } => (57, vec![*dim as i64, *shift], vec![], vec![], None, None),
Op::CumSum { dim } => (58, vec![*dim as i64], vec![], vec![], None, None),
Op::PadBackward {
in_shape,
padding,
mode,
} => {
let mode_tag = match mode {
crate::PadMode::Constant => 0_i64,
crate::PadMode::Reflect => 1,
crate::PadMode::Replicate => 2,
};
let mut ints: Vec<i64> = Vec::with_capacity(padding.len() * 2 + 1);
for &(b, a) in padding {
ints.push(b as i64);
ints.push(a as i64);
}
ints.push(mode_tag);
(
60,
ints,
vec![],
vec![],
Some(in_shape.dims().to_vec()),
None,
)
}
Op::Pad {
padding,
mode,
value,
} => {
let mode_tag = match mode {
crate::PadMode::Constant => 0_i64,
crate::PadMode::Reflect => 1,
crate::PadMode::Replicate => 2,
};
let mut ints: Vec<i64> = Vec::with_capacity(padding.len() * 2 + 1);
for &(b, a) in padding {
ints.push(b as i64);
ints.push(a as i64);
}
ints.push(mode_tag);
(59, ints, vec![value.to_bits()], vec![], None, None)
}
Op::MatMul => (30, vec![], vec![], vec![], None, None),
Op::Transpose => (31, vec![], vec![], vec![], None, None),
Op::Permute(axes) => (32, vec![], vec![], axes.clone(), None, None),
Op::Cast(dt) => (40, vec![], vec![], vec![], None, Some(dtype_key(*dt))),
Op::BroadcastTo(s) => (41, vec![], vec![], vec![], Some(s.dims().to_vec()), None),
Op::Reshape(s) => (42, vec![], vec![], vec![], Some(s.dims().to_vec()), None),
Op::Contiguize => (46, vec![], vec![], vec![], None, None),
Op::Unsqueeze { dim } => (45, vec![*dim as i64], vec![], vec![], None, None),
Op::Squeeze { dim } => (48, vec![*dim as i64], vec![], vec![], None, None),
Op::ReduceSumTo(s) => (43, vec![], vec![], vec![], Some(s.dims().to_vec()), None),
Op::ReduceMaxTo(s) => (44, vec![], vec![], vec![], Some(s.dims().to_vec()), None),
Op::SumAll => (50, vec![], vec![], vec![], None, None),
Op::MaxAll => (51, vec![], vec![], vec![], None, None),
Op::MinAll => (52, vec![], vec![], vec![], None, None),
Op::MeanAll => (53, vec![], vec![], vec![], None, None),
Op::SumDim(d) => (60, vec![*d as i64], vec![], vec![], None, None),
Op::MaxDim(d) => (61, vec![*d as i64], vec![], vec![], None, None),
Op::MinDim(d) => (62, vec![*d as i64], vec![], vec![], None, None),
Op::MeanDim(d) => (63, vec![*d as i64], vec![], vec![], None, None),
Op::ArgMaxDim(d) => (64, vec![*d as i64], vec![], vec![], None, None),
Op::ArgMinDim(d) => (65, vec![*d as i64], vec![], vec![], None, None),
Op::Concat { dim } => (80, vec![*dim as i64], vec![], vec![], None, None),
Op::Slice { dim, start, len } => (
81,
vec![*dim as i64, *start as i64, *len as i64],
vec![],
vec![],
None,
None,
),
Op::AddScalar(c) => (90, vec![], vec![c.to_bits()], vec![], None, None),
Op::MulScalar(c) => (91, vec![], vec![c.to_bits()], vec![], None, None),
Op::PowI(n) => (92, vec![*n as i64], vec![], vec![], None, None),
Op::Clamp { min, max } => (
93,
vec![],
vec![min.to_bits(), max.to_bits()],
vec![],
None,
None,
),
Op::Maximum => (100, vec![], vec![], vec![], None, None),
Op::Minimum => (101, vec![], vec![], vec![], None, None),
Op::Fused(fid, fparams) => {
let pk = fparams.key();
let mut ints: Vec<i64> = Vec::with_capacity(2 + pk.ints.len());
ints.push(fid.0 as i64);
ints.push(pk.tag as i64);
ints.extend_from_slice(&pk.ints);
(200, ints, pk.bits, vec![], None, None)
}
Op::Scan {
n_carries,
n_xs,
bound,
emit,
early_exit,
} => {
let emit_tag: i64 = match emit {
ScanEmit::All => 0,
ScanEmit::Final => 1,
};
let exit_flag: i64 = if early_exit.is_some() { 1 } else { 0 };
(
210,
vec![
*n_xs as i64,
*bound as i64,
emit_tag,
exit_flag,
*n_carries as i64,
],
vec![],
vec![],
None,
None,
)
}
Op::ScanPlaceholder { role, index } => {
let role_tag: i64 = match role {
ScanRole::Carry => 0,
ScanRole::Elem => 1,
};
(
211,
vec![role_tag, *index as i64],
vec![],
vec![],
None,
None,
)
}
_ => return None,
};
Some(OpKey {
tag,
ints,
bits,
dims,
shape,
dtype,
})
}
fn dtype_key(dt: DType) -> u32 {
format!("{dt:?}")
.as_bytes()
.iter()
.fold(0u32, |acc, b| acc.wrapping_mul(31).wrapping_add(*b as u32))
}
fn is_commutative(op: &Op) -> bool {
matches!(op, Op::Add | Op::Mul)
}
pub fn optimize(graph: &SharedGraph, roots: &[NodeId]) -> Vec<NodeId> {
let order = {
let g = graph.read().unwrap();
topo_order_multi(&g, roots)
};
let mut g = graph.write().unwrap();
let mut remap: HashMap<NodeId, NodeId> = HashMap::new();
let mut cse: HashMap<(OpKey, Vec<NodeId>), NodeId> = HashMap::new();
for id in order {
let (op, inputs, shape, dtype) = {
let node = g.node(id);
(
node.op.clone(),
node.inputs.clone(),
node.shape.clone(),
node.dtype,
)
};
let mapped_inputs: Vec<NodeId> = inputs
.iter()
.map(|input_id| *remap.get(input_id).unwrap_or(input_id))
.collect();
let inputs_unchanged = mapped_inputs == inputs;
if let Op::Cast(target) = &op {
if g.node(mapped_inputs[0]).dtype == *target {
remap.insert(id, mapped_inputs[0]);
continue;
}
if *target == fuel_ir::DType::Bool {
let src_dtype = g.node(mapped_inputs[0]).dtype;
if src_dtype.is_float() {
let src_shape = g.node(mapped_inputs[0]).shape.clone();
let zeros = g.push(Node {
op: Op::MulScalar(0.0),
inputs: vec![mapped_inputs[0]],
shape: src_shape.clone(),
dtype: src_dtype,
});
let ne = g.push(Node {
op: Op::Ne,
inputs: vec![mapped_inputs[0], zeros],
shape: src_shape,
dtype: fuel_ir::DType::Bool,
});
remap.insert(id, ne);
continue;
}
}
}
if let Some(aliased) = try_simplify(&op, &mapped_inputs) {
remap.insert(id, aliased);
continue;
}
if let Some(key) = op_key(&op) {
let key_inputs = if is_commutative(&op) {
let mut v = mapped_inputs.clone();
v.sort();
v
} else {
mapped_inputs.clone()
};
let full_key = (key, key_inputs);
if let Some(&existing) = cse.get(&full_key) {
remap.insert(id, existing);
continue;
}
let canonical_id = if inputs_unchanged {
id
} else {
g.push(Node {
op: op.clone(),
inputs: mapped_inputs.clone(),
shape: shape.clone(),
dtype,
})
};
cse.insert(full_key, canonical_id);
remap.insert(id, canonical_id);
} else {
let canonical_id = if inputs_unchanged {
id
} else {
g.push(Node {
op: op.clone(),
inputs: mapped_inputs,
shape,
dtype,
})
};
remap.insert(id, canonical_id);
}
}
roots.iter().map(|r| *remap.get(r).unwrap_or(r)).collect()
}
pub fn insert_copies(graph: &SharedGraph, roots: &[NodeId]) -> Vec<NodeId> {
let order = {
let g = graph.read().unwrap();
topo_order_multi(&g, roots)
};
let mut inferred: HashMap<NodeId, Option<DeviceLocation>> = HashMap::new();
let mut remap: HashMap<NodeId, NodeId> = HashMap::new();
let mut g = graph.write().unwrap();
for id in order {
let (op, inputs, shape, dtype) = {
let node = g.node(id);
(
node.op.clone(),
node.inputs.clone(),
node.shape.clone(),
node.dtype,
)
};
let placement_hint = g.placement(id);
let target_device: Option<DeviceLocation> = match placement_hint {
Some(d) => Some(d),
None => inputs.first().and_then(|i| {
let mapped = *remap.get(i).unwrap_or(i);
inferred.get(&mapped).copied().flatten()
}),
};
let mut new_inputs: Vec<NodeId> = Vec::with_capacity(inputs.len());
let mut any_changed = false;
for input_id in &inputs {
let mapped_in = *remap.get(input_id).unwrap_or(input_id);
let in_device = inferred.get(&mapped_in).copied().flatten();
let needs_copy = match (in_device, target_device) {
(Some(src), Some(tgt)) if src != tgt => true,
(None, Some(_)) => true,
_ => false,
};
if needs_copy {
let tgt = target_device.expect("needs_copy implies target is Some");
let (in_shape, in_dtype) = {
let n = g.node(mapped_in);
(n.shape.clone(), n.dtype)
};
let copy_id = g.push(Node {
op: Op::Copy { target: tgt },
inputs: vec![mapped_in],
shape: in_shape,
dtype: in_dtype,
});
inferred.insert(copy_id, Some(tgt));
new_inputs.push(copy_id);
any_changed = true;
} else {
if mapped_in != *input_id {
any_changed = true;
}
new_inputs.push(mapped_in);
}
}
let canonical_id = if any_changed {
let new_id = g.push(Node {
op: op.clone(),
inputs: new_inputs,
shape,
dtype,
});
if let Some(d) = placement_hint {
g.set_placement(new_id, d);
}
new_id
} else {
id
};
let out_device = match &op {
Op::Copy { target } | Op::Move { target } => Some(*target),
_ => target_device,
};
inferred.insert(canonical_id, out_device);
remap.insert(id, canonical_id);
}
roots.iter().map(|r| *remap.get(r).unwrap_or(r)).collect()
}
pub fn lower_const_placement(graph: &SharedGraph, roots: &[NodeId]) -> usize {
let order = {
let g = graph.read().unwrap();
topo_order_multi(&g, roots)
};
let mut consumers: HashMap<NodeId, Vec<NodeId>> = HashMap::new();
{
let g = graph.read().unwrap();
for &nid in &order {
let node = g.node(nid);
for &input in &node.inputs {
consumers.entry(input).or_default().push(nid);
}
}
}
let mut lowered = 0;
let mut g = graph.write().unwrap();
for &nid in &order {
let is_const = matches!(g.node(nid).op, Op::Const);
if !is_const || g.placement(nid).is_some() {
continue;
}
let Some(cs) = consumers.get(&nid) else {
continue; };
let mut target: Option<DeviceLocation> = None;
let mut unanimous = true;
for &c in cs {
match g.placement(c) {
Some(d) => match target {
None => target = Some(d),
Some(prev) if prev == d => {}
Some(_) => {
unanimous = false;
break;
}
},
None => {
unanimous = false;
break;
}
}
}
if unanimous && let Some(d) = target {
g.set_placement(nid, d);
lowered += 1;
}
}
lowered
}
fn try_simplify(op: &Op, inputs: &[NodeId]) -> Option<NodeId> {
match op {
Op::AddScalar(c) if *c == 0.0 => Some(inputs[0]),
Op::MulScalar(c) if *c == 1.0 => Some(inputs[0]),
Op::PowI(1) => Some(inputs[0]),
_ => None,
}
}
#[derive(Debug, Clone, Default)]
pub struct OrderingEdges(pub HashMap<NodeId, Vec<NodeId>>);
impl OrderingEdges {
pub fn new() -> Self {
Self(HashMap::new())
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn deps_of(&self, nid: NodeId) -> &[NodeId] {
self.0.get(&nid).map(|v| v.as_slice()).unwrap_or(&[])
}
}
pub fn fuse_linear(graph: &SharedGraph, roots: &[NodeId]) -> usize {
let order = {
let g = graph.read().unwrap();
topo_order_multi(&g, roots)
};
let mut consumer_count: HashMap<NodeId, usize> = HashMap::new();
{
let g = graph.read().unwrap();
for &nid in &order {
for &input in &g.node(nid).inputs {
*consumer_count.entry(input).or_insert(0) += 1;
}
}
for &r in roots {
*consumer_count.entry(r).or_insert(0) += 1;
}
}
let mut g = graph.write().unwrap();
let mut remap: HashMap<NodeId, NodeId> = HashMap::new();
let mut fused = 0usize;
for nid in order {
let (op, inputs, shape, dtype) = {
let n = g.node(nid);
(n.op.clone(), n.inputs.clone(), n.shape.clone(), n.dtype)
};
let mapped: Vec<NodeId> = inputs.iter().map(|i| *remap.get(i).unwrap_or(i)).collect();
if !matches!(op, Op::Add) || mapped.len() != 2 {
continue;
}
let lhs = mapped[0];
let rhs = mapped[1];
let lhs_op = g.node(lhs).op.clone();
if !matches!(lhs_op, Op::MatMul) {
continue;
}
if consumer_count.get(&lhs).copied().unwrap_or(0) != 1 {
continue;
}
let mm_dims = g.node(lhs).shape.dims().to_vec();
if mm_dims.is_empty() {
continue;
}
let last_dim = mm_dims[mm_dims.len() - 1];
let rhs_node = g.node(rhs);
let bias_src_id = if matches!(rhs_node.op, Op::BroadcastTo(_)) && rhs_node.inputs.len() == 1
{
*remap
.get(&rhs_node.inputs[0])
.unwrap_or(&rhs_node.inputs[0])
} else {
rhs
};
let bias_dims = g.node(bias_src_id).shape.dims().to_vec();
if bias_dims.len() != 1 || bias_dims[0] != last_dim {
continue;
}
let mm_inputs = g.node(lhs).inputs.clone();
if mm_inputs.len() != 2 {
continue;
}
let a = *remap.get(&mm_inputs[0]).unwrap_or(&mm_inputs[0]);
let b = *remap.get(&mm_inputs[1]).unwrap_or(&mm_inputs[1]);
let new_id = g.push(Node {
op: Op::Fused(
crate::registry::FusedOps::FUSED_LINEAR,
crate::registry::FusedOpParams::FusedLinear,
),
inputs: vec![a, b, bias_src_id],
shape,
dtype,
});
remap.insert(nid, new_id);
fused += 1;
}
if !remap.is_empty() {
let n_nodes = g.nodes.len();
for nid in 0..n_nodes {
let node = &mut g.nodes[nid];
for input in node.inputs.iter_mut() {
if let Some(&new) = remap.get(input) {
*input = new;
}
}
}
}
fused
}
pub fn derive_ordering(graph: &crate::Graph, roots: &[NodeId]) -> OrderingEdges {
let order = topo_order_multi(graph, roots);
let mut consumers: HashMap<NodeId, Vec<NodeId>> = HashMap::new();
for &nid in &order {
for &input in &graph.node(nid).inputs {
consumers.entry(input).or_default().push(nid);
}
}
let (alias_group_of, alias_members) = alias_groups(graph, &order);
let mut ordering: HashMap<NodeId, Vec<NodeId>> = HashMap::new();
for &nid in &order {
let node = graph.node(nid);
let Some(d_idx) = node.op.destructive_input() else {
continue;
};
if d_idx >= node.inputs.len() {
continue;
}
let destroyed = node.inputs[d_idx];
let group = alias_group_of.get(&destroyed).copied().unwrap_or(destroyed);
let singleton = vec![group];
let alias_set = alias_members.get(&group).unwrap_or(&singleton);
for &alias in alias_set {
let Some(readers) = consumers.get(&alias) else {
continue;
};
for &reader in readers {
if reader != nid && !alias_set.contains(&reader) {
ordering.entry(nid).or_default().push(reader);
}
}
}
}
OrderingEdges(ordering)
}
fn alias_groups(
graph: &crate::Graph,
order: &[NodeId],
) -> (HashMap<NodeId, NodeId>, HashMap<NodeId, Vec<NodeId>>) {
use crate::Op;
let mut group_of: HashMap<NodeId, NodeId> = HashMap::new();
let mut members: HashMap<NodeId, Vec<NodeId>> = HashMap::new();
for &nid in order {
let node = graph.node(nid);
let extends_alias = node.op.is_view_op()
|| matches!(node.op, Op::View { .. } | Op::Reshape(_) | Op::Contiguize);
if !extends_alias {
continue;
}
let Some(&inp) = node.inputs.first() else {
continue;
};
let group = group_of.get(&inp).copied().unwrap_or(inp);
group_of.insert(nid, group);
members
.entry(group)
.or_insert_with(|| vec![group])
.push(nid);
}
(group_of, members)
}
pub fn insert_safety_copies(graph: &mut crate::Graph, roots: &[NodeId]) -> usize {
fn device_location_for_backend(backend: fuel_ir::probe::BackendId) -> DeviceLocation {
use fuel_ir::probe::BackendId;
match backend {
BackendId::Cpu => DeviceLocation::Cpu,
BackendId::Cuda => DeviceLocation::Cuda { gpu_id: 0 },
BackendId::Vulkan => DeviceLocation::Vulkan { gpu_id: 0 },
BackendId::Metal => DeviceLocation::Metal { gpu_id: 0 },
_ => DeviceLocation::Cpu,
}
}
let order = topo_order_multi(graph, roots);
let ordering = derive_ordering(graph, roots);
let node_set: std::collections::HashSet<NodeId> = order.iter().copied().collect();
let mut succ: HashMap<NodeId, Vec<NodeId>> = HashMap::new();
let mut pred: HashMap<NodeId, Vec<NodeId>> = HashMap::new();
for &nid in &order {
for &input in &graph.node(nid).inputs {
if node_set.contains(&input) {
succ.entry(input).or_default().push(nid);
pred.entry(nid).or_default().push(input);
}
}
for &dep in ordering.deps_of(nid) {
if node_set.contains(&dep) {
succ.entry(dep).or_default().push(nid);
pred.entry(nid).or_default().push(dep);
}
}
}
fn reach(
start: NodeId,
adj: &HashMap<NodeId, Vec<NodeId>>,
) -> std::collections::HashSet<NodeId> {
let mut seen: std::collections::HashSet<NodeId> = std::collections::HashSet::new();
let mut stack = vec![start];
while let Some(n) = stack.pop() {
if let Some(next) = adj.get(&n) {
for &m in next {
if seen.insert(m) {
stack.push(m);
}
}
}
}
seen
}
struct Conflict {
destructive_nid: NodeId,
target: NodeId,
target_shape: crate::Shape,
target_dtype: DType,
conflicting_readers: Vec<NodeId>,
}
let mut conflicts: Vec<Conflict> = Vec::new();
for &nid in &order {
let node = graph.node(nid);
let Some(d_idx) = node.op.destructive_input() else {
continue;
};
if d_idx >= node.inputs.len() {
continue;
}
let target = node.inputs[d_idx];
if node_set.contains(&target) {
let any_other_reader = succ
.get(&target)
.is_some_and(|rs| rs.iter().any(|&r| r != nid));
if !any_other_reader {
continue;
}
}
let forced_before = reach(nid, &pred);
let forced_after = reach(nid, &succ);
let mut readers: Vec<NodeId> = Vec::new();
for &maybe_reader in &order {
if maybe_reader == nid {
continue;
}
if !graph.node(maybe_reader).inputs.contains(&target) {
continue;
}
let provably_before =
forced_before.contains(&maybe_reader) && !forced_after.contains(&maybe_reader);
if !provably_before {
readers.push(maybe_reader);
}
}
if readers.is_empty() {
continue;
}
let target_node = graph.node(target);
conflicts.push(Conflict {
destructive_nid: nid,
target,
target_shape: target_node.shape.clone(),
target_dtype: target_node.dtype,
conflicting_readers: readers,
});
}
let inserted = conflicts.len();
for Conflict {
destructive_nid,
target,
target_shape,
target_dtype,
conflicting_readers,
} in conflicts
{
let target_location = graph
.placement(target)
.or_else(|| graph.placement(destructive_nid))
.or_else(|| {
graph
.target_backend(target)
.or_else(|| graph.target_backend(destructive_nid))
.map(device_location_for_backend)
})
.unwrap_or(DeviceLocation::Cpu);
let copy_id = graph.push(crate::Node {
op: crate::Op::Copy {
target: target_location,
},
inputs: vec![target],
shape: target_shape,
dtype: target_dtype,
});
if let Some(backend) = graph
.target_backend(target)
.or_else(|| graph.target_backend(destructive_nid))
{
graph.set_target_backend(copy_id, backend);
}
for reader_id in conflicting_readers {
graph.rewrite_input(reader_id, target, copy_id);
}
}
inserted
}
pub fn promote_views_for_liveness(graph: &mut crate::Graph, roots: &[NodeId]) -> usize {
use crate::Op;
let order = topo_order_multi(graph, roots);
if order.is_empty() {
return 0;
}
let mut consumers: HashMap<NodeId, Vec<NodeId>> = HashMap::new();
for &nid in &order {
for &input in &graph.node(nid).inputs {
consumers.entry(input).or_default().push(nid);
}
}
let mut depth: HashMap<NodeId, usize> = HashMap::with_capacity(order.len());
for &nid in &order {
let mut d = 0usize;
for &inp in &graph.node(nid).inputs {
if let Some(&di) = depth.get(&inp)
&& di + 1 > d
{
d = di + 1;
}
}
depth.insert(nid, d);
}
let mut last_use: HashMap<NodeId, usize> = HashMap::with_capacity(order.len());
for &nid in order.iter().rev() {
let mut lu = depth[&nid];
if let Some(downstream) = consumers.get(&nid) {
for &c in downstream {
if let Some(&cl) = last_use.get(&c)
&& cl > lu
{
lu = cl;
}
}
}
last_use.insert(nid, lu);
}
struct ViewEntry {
view_id: NodeId,
slot: u32,
is_owned: bool,
}
let mut by_producer: HashMap<NodeId, Vec<ViewEntry>> = HashMap::new();
for &nid in &order {
let node = graph.node(nid);
let (slot, is_owned) = match node.op {
Op::View { slot } => (slot, false),
Op::ViewOwned { slot } => (slot, true),
_ => continue,
};
let Some(&producer) = node.inputs.first() else {
continue;
};
if !graph.is_multi_output(producer) {
continue;
}
by_producer.entry(producer).or_default().push(ViewEntry {
view_id: nid,
slot,
is_owned,
});
}
struct Promotion {
view_id: NodeId,
slot: u32,
view_shape: crate::Shape,
view_dtype: DType,
view_consumers: Vec<NodeId>,
}
let mut promotions: Vec<Promotion> = Vec::new();
for views in by_producer.values() {
let mut slot_last_use: HashMap<u32, usize> = HashMap::new();
for v in views {
let lu = last_use[&v.view_id];
slot_last_use
.entry(v.slot)
.and_modify(|cur| {
if lu > *cur {
*cur = lu;
}
})
.or_insert(lu);
}
let min_lu = match slot_last_use.values().copied().min() {
Some(v) => v,
None => continue,
};
for v in views {
if v.is_owned {
continue;
}
let lu = slot_last_use[&v.slot];
if lu <= min_lu {
continue;
}
let v_node = graph.node(v.view_id);
promotions.push(Promotion {
view_id: v.view_id,
slot: v.slot,
view_shape: v_node.shape.clone(),
view_dtype: v_node.dtype,
view_consumers: consumers.get(&v.view_id).cloned().unwrap_or_default(),
});
}
}
let count = promotions.len();
for p in promotions {
let producer = graph.node(p.view_id).inputs[0];
let owned_id = graph.push(crate::Node {
op: Op::ViewOwned { slot: p.slot },
inputs: vec![producer],
shape: p.view_shape,
dtype: p.view_dtype,
});
if let Some(backend) = graph.target_backend(p.view_id) {
graph.set_target_backend(owned_id, backend);
}
for consumer in p.view_consumers {
graph.rewrite_input(consumer, p.view_id, owned_id);
}
}
count
}
pub fn execution_plan(graph: &crate::Graph, roots: &[NodeId]) -> Vec<NodeId> {
let base_order = topo_order_multi(graph, roots);
let ordering = derive_ordering(graph, roots);
if ordering.is_empty() {
return base_order;
}
let pos: HashMap<NodeId, usize> = base_order
.iter()
.enumerate()
.map(|(i, &n)| (n, i))
.collect();
let node_set: std::collections::HashSet<NodeId> = base_order.iter().copied().collect();
let mut in_degree: HashMap<NodeId, usize> = HashMap::with_capacity(base_order.len());
let mut forward: HashMap<NodeId, Vec<NodeId>> = HashMap::new();
for &nid in &base_order {
let node = graph.node(nid);
let mut d = 0usize;
for &input in &node.inputs {
if node_set.contains(&input) {
d += 1;
forward.entry(input).or_default().push(nid);
}
}
for &dep in ordering.deps_of(nid) {
if node_set.contains(&dep) {
d += 1;
forward.entry(dep).or_default().push(nid);
}
}
in_degree.insert(nid, d);
}
let mut ready: std::collections::BTreeSet<(usize, NodeId)> = base_order
.iter()
.copied()
.filter(|n| in_degree[n] == 0)
.map(|n| (pos[&n], n))
.collect();
let mut plan = Vec::with_capacity(base_order.len());
while let Some(&(_, n)) = ready.iter().next() {
ready.remove(&(pos[&n], n));
plan.push(n);
if let Some(succs) = forward.get(&n) {
for &s in succs {
let d = in_degree.get_mut(&s).expect("in_degree covers all nodes");
*d -= 1;
if *d == 0 {
ready.insert((pos[&s], s));
}
}
}
}
if plan.len() != base_order.len() {
panic!(
"execution_plan: cycle in ordering edges (plan={}, base={}) — \
a destructive op transitively depends on itself. This is a \
bug in whatever rule emitted the destructive op.",
plan.len(),
base_order.len(),
);
}
plan
}
pub fn insert_evict_reload(
graph: &SharedGraph,
candidate: NodeId,
src_device: DeviceLocation,
post_gap_consumers: &[NodeId],
) -> (NodeId, NodeId) {
let mut g = graph.write().unwrap();
let (shape, dtype) = {
let n = g.node(candidate);
(n.shape.clone(), n.dtype)
};
let move_id = g.push(Node {
op: Op::Move {
target: DeviceLocation::Cpu,
},
inputs: vec![candidate],
shape: shape.clone(),
dtype,
});
let reload_id = g.push(Node {
op: Op::Copy { target: src_device },
inputs: vec![move_id],
shape,
dtype,
});
for &consumer in post_gap_consumers {
g.rewrite_input(consumer, candidate, reload_id);
}
(move_id, reload_id)
}
pub fn insert_cross_device_copies<P, S, H>(
graph: &mut Graph,
roots: &[NodeId],
placement_for: P,
shares_storage: S,
needs_host_staging: H,
) -> Vec<NodeId>
where
P: Fn(NodeId) -> Option<DeviceLocation>,
S: Fn(DeviceLocation, DeviceLocation) -> bool,
H: Fn(DeviceLocation, DeviceLocation) -> bool,
{
let order = topo_order_multi(graph, roots);
let mut copy_cache: HashMap<(NodeId, DeviceLocation), NodeId> = HashMap::new();
let mut inserted: Vec<NodeId> = Vec::new();
let mut rewires: Vec<(NodeId, NodeId, NodeId)> = Vec::new();
for &consumer_id in &order {
let consumer_placement = match placement_for(consumer_id) {
Some(p) => p,
None => continue,
};
let consumer_op = &graph.node(consumer_id).op;
if matches!(consumer_op, Op::Copy { .. } | Op::Move { .. }) {
continue;
}
let inputs: Vec<NodeId> = graph.node(consumer_id).inputs.clone();
for producer_id in inputs {
let producer_placement = match placement_for(producer_id) {
Some(p) => p,
None => continue,
};
if shares_storage(producer_placement, consumer_placement) {
continue;
}
let two_hop = needs_host_staging(producer_placement, consumer_placement);
let source_for_consumer_hop = if two_hop {
match copy_cache.get(&(producer_id, DeviceLocation::Cpu)) {
Some(&id) => id,
None => {
let producer = graph.node(producer_id);
let shape = producer.shape.clone();
let dtype = producer.dtype;
let cpu_id = graph.push(Node {
op: Op::Copy {
target: DeviceLocation::Cpu,
},
inputs: vec![producer_id],
shape,
dtype,
});
graph.set_placement(cpu_id, DeviceLocation::Cpu);
copy_cache.insert((producer_id, DeviceLocation::Cpu), cpu_id);
inserted.push(cpu_id);
cpu_id
}
}
} else {
producer_id
};
let copy_id = match copy_cache.get(&(source_for_consumer_hop, consumer_placement)) {
Some(&id) => id,
None => {
let src = graph.node(source_for_consumer_hop);
let shape = src.shape.clone();
let dtype = src.dtype;
let id = graph.push(Node {
op: Op::Copy {
target: consumer_placement,
},
inputs: vec![source_for_consumer_hop],
shape,
dtype,
});
graph.set_placement(id, consumer_placement);
copy_cache.insert((source_for_consumer_hop, consumer_placement), id);
inserted.push(id);
id
}
};
rewires.push((consumer_id, producer_id, copy_id));
}
}
for (consumer, old_input, new_input) in rewires {
graph.rewrite_input(consumer, old_input, new_input);
}
inserted
}
pub fn insert_layout_fixups<F>(
graph: &mut Graph,
roots: &[NodeId],
kernel_accepts_strided: F,
) -> usize
where
F: Fn(NodeId) -> bool,
{
let order = topo_order_multi(graph, roots);
let mut fixup_cache: HashMap<NodeId, NodeId> = HashMap::new();
let mut inserted = 0usize;
let mut rewires: Vec<(NodeId, NodeId, NodeId)> = Vec::new();
for &consumer_id in &order {
let (consumer_op, consumer_inputs) = {
let node = graph.node(consumer_id);
(node.op.clone(), node.inputs.clone())
};
if consumer_op.is_view_op()
|| matches!(
consumer_op,
Op::Const
| Op::Release
| Op::Reshape(_)
| Op::Contiguize
| Op::Copy { .. }
| Op::Move { .. }
| Op::Alloc { .. }
| Op::ZeroFill
| Op::WriteSlice { .. }
| Op::WriteSliceDoff { .. }
)
{
continue;
}
if kernel_accepts_strided(consumer_id) {
continue;
}
for input_id in consumer_inputs {
let input_layout = graph.layout(input_id);
if input_layout.is_contiguous() && input_layout.start_offset() == 0 {
continue;
}
let fixup_id = match fixup_cache.get(&input_id) {
Some(&id) => id,
None => {
let (shape, dtype) = {
let p = graph.node(input_id);
(p.shape.clone(), p.dtype)
};
let id = graph.push(Node {
op: Op::Contiguize,
inputs: vec![input_id],
shape,
dtype,
});
fixup_cache.insert(input_id, id);
inserted += 1;
id
}
};
rewires.push((consumer_id, input_id, fixup_id));
}
}
for (consumer, old_input, new_input) in rewires {
graph.rewrite_input(consumer, old_input, new_input);
}
inserted
}
pub fn insert_cast_fixups(graph: &mut Graph, casts: &[(NodeId, NodeId, DType)]) -> usize {
let mut cache: HashMap<(NodeId, DType), NodeId> = HashMap::new();
let mut applied = 0;
for &(consumer, input, target) in casts {
if graph.node(input).dtype == target {
continue; }
let cast_id = *cache.entry((input, target)).or_insert_with(|| {
let shape = graph.node(input).shape.clone();
graph.push(Node {
op: Op::Cast(target),
inputs: vec![input],
shape,
dtype: target,
})
});
graph.rewrite_input(consumer, input, cast_id);
applied += 1;
}
applied
}
#[cfg(test)]
mod tests {
use super::*;
use crate::NodeHandle;
use crate::registry::FusedOps;
use fuel_ir::{DeviceLocation, Shape};
use std::sync::Arc;
fn cpu_dev() -> &'static Arc<dyn fuel_backend_contract::DynBackendDevice> {
static D: std::sync::OnceLock<Arc<dyn fuel_backend_contract::DynBackendDevice>> =
std::sync::OnceLock::new();
D.get_or_init(|| Arc::new(fuel_cpu_backend::dyn_impl::CpuBackendDevice))
}
fn make_scalar_graph() -> (SharedGraph, NodeHandle) {
let t = NodeHandle::from_f32(vec![1.0, 2.0, 3.0, 4.0], Shape::from_dims(&[4]), cpu_dev())
.unwrap();
(t.graph().clone(), t)
}
fn count_copy_nodes(graph: &SharedGraph) -> usize {
let g = graph.read().unwrap();
(0..g.len())
.filter(|i| matches!(g.node(NodeId(*i)).op, Op::Copy { .. }))
.count()
}
#[test]
fn insert_copies_no_placement_no_copies() {
let a = NodeHandle::from_f32(vec![1.0, 2.0], Shape::from_dims(&[2]), cpu_dev()).unwrap();
let b = a
.const_f32_like(vec![3.0, 4.0], Shape::from_dims(&[2]))
.unwrap();
let c = a.add(&b);
let graph = c.graph().clone();
let before = count_copy_nodes(&graph);
let new_roots = insert_copies(&graph, &[c.id()]);
assert_eq!(new_roots, vec![c.id()]);
assert_eq!(count_copy_nodes(&graph), before);
}
#[test]
fn insert_copies_tagged_node_pulls_inputs_to_its_device() {
let a = NodeHandle::from_f32(vec![1.0, 2.0], Shape::from_dims(&[2]), cpu_dev()).unwrap();
let b = a
.const_f32_like(vec![3.0, 4.0], Shape::from_dims(&[2]))
.unwrap();
let c = a.add(&b).on_device(DeviceLocation::Vulkan { gpu_id: 0 });
let graph = c.graph().clone();
let before_copies = count_copy_nodes(&graph);
let new_roots = insert_copies(&graph, &[c.id()]);
assert_eq!(count_copy_nodes(&graph) - before_copies, 2);
let g = graph.read().unwrap();
let new_add = g.node(new_roots[0]);
assert_eq!(new_add.inputs.len(), 2);
for input in &new_add.inputs {
let node = g.node(*input);
assert!(matches!(
node.op,
Op::Copy {
target: DeviceLocation::Vulkan { gpu_id: 0 }
}
));
}
}
#[test]
fn insert_copies_matching_device_no_copies_inserted() {
let a = NodeHandle::from_f32(vec![1.0, 2.0], Shape::from_dims(&[2]), cpu_dev())
.unwrap()
.on_device(DeviceLocation::Vulkan { gpu_id: 0 });
let b = a
.const_f32_like(vec![3.0, 4.0], Shape::from_dims(&[2]))
.unwrap()
.on_device(DeviceLocation::Vulkan { gpu_id: 0 });
let c = a.add(&b).on_device(DeviceLocation::Vulkan { gpu_id: 0 });
let graph = c.graph().clone();
let before = count_copy_nodes(&graph);
insert_copies(&graph, &[c.id()]);
assert_eq!(count_copy_nodes(&graph), before);
}
#[test]
fn insert_copies_handles_backward_graph() {
let x = NodeHandle::from_f32(vec![1.0, 2.0, 3.0, 4.0], Shape::from_dims(&[4]), cpu_dev())
.unwrap()
.on_device(DeviceLocation::Vulkan { gpu_id: 0 });
let sq = x.mul(&x).on_device(DeviceLocation::Vulkan { gpu_id: 0 });
let y = sq.sum_all().on_device(DeviceLocation::Vulkan { gpu_id: 0 });
let graph = y.graph().clone();
let grads = y.backward();
let grad_x = grads.get(&x).expect("dL/dx should exist");
let before_copies = count_copy_nodes(&graph);
let _new = insert_copies(&graph, &[y.id(), grad_x.id()]);
let after_copies = count_copy_nodes(&graph);
assert_eq!(
after_copies, before_copies,
"insert_copies should leave a uniformly-Vulkan forward+backward graph alone"
);
}
#[test]
fn insert_copies_backward_inherits_forward_device() {
let x = NodeHandle::from_f32(vec![1.0, 2.0, 3.0, 4.0], Shape::from_dims(&[4]), cpu_dev())
.unwrap();
let sq = x.mul(&x);
let y = sq.sum_all().on_device(DeviceLocation::Vulkan { gpu_id: 0 });
let graph = y.graph().clone();
let grads = y.backward();
let grad_x = grads.get(&x).expect("dL/dx should exist");
let new_roots = insert_copies(&graph, &[y.id(), grad_x.id()]);
assert_eq!(new_roots.len(), 2);
assert!(
count_copy_nodes(&graph) > 0,
"expected Copies to be inserted for unplaced Const inputs"
);
}
#[test]
fn lower_const_placement_single_vulkan_consumer() {
let a = NodeHandle::from_f32(vec![1.0, 2.0], Shape::from_dims(&[2]), cpu_dev()).unwrap();
let b = a
.const_f32_like(vec![3.0, 4.0], Shape::from_dims(&[2]))
.unwrap();
let c = a.add(&b).on_device(DeviceLocation::Vulkan { gpu_id: 0 });
let graph = c.graph().clone();
let lowered = lower_const_placement(&graph, &[c.id()]);
assert_eq!(lowered, 2); assert_eq!(
graph.read().unwrap().placement(a.id()),
Some(DeviceLocation::Vulkan { gpu_id: 0 })
);
assert_eq!(
graph.read().unwrap().placement(b.id()),
Some(DeviceLocation::Vulkan { gpu_id: 0 })
);
let before_copies = count_copy_nodes(&graph);
insert_copies(&graph, &[c.id()]);
assert_eq!(count_copy_nodes(&graph), before_copies);
}
#[test]
fn lower_const_placement_consumers_disagree_stays_unplaced() {
let a = NodeHandle::from_f32(vec![1.0, 2.0], Shape::from_dims(&[2]), cpu_dev()).unwrap();
let b = a
.const_f32_like(vec![3.0, 4.0], Shape::from_dims(&[2]))
.unwrap();
let cpu_sum = a.add(&b).on_device(DeviceLocation::Cpu);
let vulkan_sum = a.add(&b).on_device(DeviceLocation::Vulkan { gpu_id: 0 });
let graph = a.graph().clone();
lower_const_placement(&graph, &[cpu_sum.id(), vulkan_sum.id()]);
assert_eq!(
graph.read().unwrap().placement(a.id()),
None,
"const with disagreeing consumers stays unplaced"
);
assert_eq!(graph.read().unwrap().placement(b.id()), None);
}
#[test]
fn lower_const_placement_skips_already_placed() {
let a = NodeHandle::from_f32(vec![1.0, 2.0], Shape::from_dims(&[2]), cpu_dev())
.unwrap()
.on_device(DeviceLocation::Cpu);
let b = a
.const_f32_like(vec![3.0, 4.0], Shape::from_dims(&[2]))
.unwrap();
let c = a.add(&b).on_device(DeviceLocation::Vulkan { gpu_id: 0 });
let graph = c.graph().clone();
lower_const_placement(&graph, &[c.id()]);
assert_eq!(
graph.read().unwrap().placement(a.id()),
Some(DeviceLocation::Cpu)
);
assert_eq!(
graph.read().unwrap().placement(b.id()),
Some(DeviceLocation::Vulkan { gpu_id: 0 })
);
}
#[test]
fn insert_copies_idempotent() {
let a = NodeHandle::from_f32(vec![1.0], Shape::from_dims(&[1]), cpu_dev()).unwrap();
let b = a.const_f32_like(vec![2.0], Shape::from_dims(&[1])).unwrap();
let c = a.add(&b).on_device(DeviceLocation::Cpu);
let graph = c.graph().clone();
let roots1 = insert_copies(&graph, &[c.id()]);
let after_first = count_copy_nodes(&graph);
let _roots2 = insert_copies(&graph, &roots1);
assert_eq!(
count_copy_nodes(&graph),
after_first,
"insert_copies should be idempotent on already-reconciled graphs"
);
}
#[test]
fn cse_folds_identical_add() {
let (graph, a) = make_scalar_graph();
let b = a.add(&a);
let c = a.add(&a);
let pre_len = graph.read().unwrap().len();
let new_roots = optimize(&graph, &[b.id(), c.id()]);
assert_eq!(
new_roots[0], new_roots[1],
"CSE should map both to same node"
);
assert!(graph.read().unwrap().len() >= pre_len);
}
#[test]
fn cse_folds_commutative() {
let (_graph, a) = make_scalar_graph();
let b = a.add_scalar(5.0);
let ab = a.add(&b);
let ba = b.add(&a);
let graph = a.graph().clone();
let new_roots = optimize(&graph, &[ab.id(), ba.id()]);
assert_eq!(
new_roots[0], new_roots[1],
"commutative CSE should fold a+b and b+a to one node"
);
}
#[test]
fn cse_does_not_fold_minmax_across_operand_order() {
let (_graph, a) = make_scalar_graph();
let b = a.add_scalar(5.0);
let add_ab = a.add(&b);
let add_ba = b.add(&a);
let graph = a.graph().clone();
let add_roots = optimize(&graph, &[add_ab.id(), add_ba.id()]);
assert_eq!(
add_roots[0], add_roots[1],
"control: Add is bitwise commutative and must still fold -- if this fails, CSE is broken generally and the Maximum arm below proves nothing"
);
let max_ab = a.maximum(&b);
let max_ba = b.maximum(&a);
let max_roots = optimize(&graph, &[max_ab.id(), max_ba.id()]);
assert_ne!(
max_roots[0], max_roots[1],
"max(a,b) and max(b,a) were folded to one node, but they differ in BITS on distinct NaN payloads (lhs-first tie-break). See GAP-271."
);
let min_ab = a.minimum(&b);
let min_ba = b.minimum(&a);
let min_roots = optimize(&graph, &[min_ab.id(), min_ba.id()]);
assert_ne!(
min_roots[0], min_roots[1],
"min(a,b) and min(b,a) were folded to one node -- same defect as Maximum, see GAP-271"
);
}
#[test]
fn simplifies_add_scalar_zero() {
let (graph, a) = make_scalar_graph();
let b = a.add_scalar(0.0);
let new_roots = optimize(&graph, &[b.id()]);
assert_eq!(new_roots[0], a.id(), "AddScalar(0) should alias to input");
}
#[test]
fn simplifies_mul_scalar_one() {
let (graph, a) = make_scalar_graph();
let b = a.mul_scalar(1.0);
let new_roots = optimize(&graph, &[b.id()]);
assert_eq!(new_roots[0], a.id(), "MulScalar(1) should alias to input");
}
#[test]
fn simplifies_identity_cast() {
let (graph, a) = make_scalar_graph();
let same = a.cast(a.dtype());
let new_roots = optimize(&graph, &[same.id()]);
assert_eq!(
new_roots[0],
a.id(),
"Cast(d) on a dtype-d input should alias to input",
);
}
#[test]
fn lowers_float_cast_to_bool_into_ne_against_zero() {
let (graph, a) = make_scalar_graph(); let as_bool = a.cast(fuel_ir::DType::Bool);
let new_roots = optimize(&graph, &[as_bool.id()]);
let g = graph.read().unwrap();
let root = g.node(new_roots[0]);
assert!(
matches!(root.op, Op::Ne),
"float -> Bool must lower to Op::Ne, got {:?}",
root.op,
);
assert_eq!(
root.dtype,
fuel_ir::DType::Bool,
"the lowered root still yields Bool"
);
let rhs = g.node(root.inputs[1]);
assert!(
matches!(rhs.op, Op::MulScalar(c) if c == 0.0),
"rhs must be MulScalar(0.0) (shape-preserving zeros), got {:?}",
rhs.op,
);
assert_eq!(
rhs.inputs[0], root.inputs[0],
"zeros must derive from the cast's own input",
);
}
#[test]
fn does_not_lower_integer_cast_to_bool() {
let (graph, a) = make_scalar_graph();
let as_u8 = a.cast(fuel_ir::DType::U8);
let as_bool = as_u8.cast(fuel_ir::DType::Bool);
let new_roots = optimize(&graph, &[as_bool.id()]);
let g = graph.read().unwrap();
let root = g.node(new_roots[0]);
assert!(
matches!(root.op, Op::Cast(fuel_ir::DType::Bool)),
"int -> Bool stays an Op::Cast (genuine decline on CUDA), got {:?}",
root.op,
);
}
#[test]
fn keeps_real_cast() {
let (graph, a) = make_scalar_graph();
let cast = a.cast(fuel_ir::DType::F64);
let new_roots = optimize(&graph, &[cast.id()]);
assert_ne!(
new_roots[0],
a.id(),
"a real Cast must survive optimization"
);
}
#[test]
fn declarative_pattern_matches_and_fuses_relu_add() {
use crate::jit::{OpAttrs, OpTag, PatternNode};
use crate::registry::{
BackwardKind, FusedOpEntry, FusedOpFamily, FusedOpParams, FusedOps, PatternTree,
SubgraphPattern,
};
fn shape_rule(s: &[Shape], _: &FusedOpParams) -> Shape {
s[0].clone()
}
fn dtype_rule(d: &[DType], _: &FusedOpParams) -> DType {
d[0]
}
fn no_decompose(_: &mut Graph, id: NodeId, _: &FusedOpParams) -> NodeId {
id
}
let entry = FusedOpEntry {
destructive_input: None,
id: FusedOps::SOFTMAX_LAST_DIM, name: "test_declarative_relu_add",
family: FusedOpFamily::Forward,
pattern: SubgraphPattern::Declarative(PatternTree {
root: PatternNode::Op {
op: OpTag::Relu,
attrs: OpAttrs::default(),
operands: vec![PatternNode::Op {
op: OpTag::Add,
attrs: OpAttrs::default(),
operands: vec![
PatternNode::Bind { index: 0 },
PatternNode::Bind { index: 1 },
],
}],
},
params: FusedOpParams::SoftmaxLastDim,
}),
decompose: no_decompose,
backward: BackwardKind::NotDifferentiable,
shape_rule,
dtype_rule,
output_views: None,
};
let rule = FusionRule::from_entry(&entry);
let mut g = Graph::new();
let s = Shape::from_dims(&[2]);
let f32 = DType::F32;
let a = g.push(Node {
op: Op::Const,
inputs: vec![],
shape: s.clone(),
dtype: f32,
});
let b = g.push(Node {
op: Op::Const,
inputs: vec![],
shape: s.clone(),
dtype: f32,
});
let sum = g.push(Node {
op: Op::Add,
inputs: vec![a, b],
shape: s.clone(),
dtype: f32,
});
let r = g.push(Node {
op: Op::Relu,
inputs: vec![sum],
shape: s.clone(),
dtype: f32,
});
assert!(rule.matches(&g, r), "declarative pattern matches relu(add)");
let mut remap = HashMap::new();
rule.rewrite(&mut g, r, &mut remap);
let fused_id = remap[&r];
match &g.node(fused_id).op {
Op::Fused(id, FusedOpParams::SoftmaxLastDim) => {
assert_eq!(*id, FusedOps::SOFTMAX_LAST_DIM)
}
other => panic!("expected Op::Fused(SOFTMAX_LAST_DIM, _), got {other:?}"),
}
assert_eq!(
g.node(fused_id).inputs,
vec![a, b],
"fused inputs = the region's bound external inputs",
);
}
#[test]
fn runtime_op_fuses_and_round_trips_through_the_pass() {
use crate::jit::{OpAttrs, OpTag, PatternNode};
use crate::runtime_fused::register_runtime_fused;
let region = PatternNode::Op {
op: OpTag::Tanh,
attrs: OpAttrs::default(),
operands: vec![PatternNode::Op {
op: OpTag::Sub,
attrs: OpAttrs::default(),
operands: vec![
PatternNode::Bind { index: 0 },
PatternNode::Bind { index: 1 },
],
}],
};
let rid = register_runtime_fused("test::tanh_sub", region).unwrap();
let a = NodeHandle::from_f32(
vec![0.5, -0.5, 1.0, -1.0],
Shape::from_dims(&[4]),
cpu_dev(),
)
.unwrap();
let b = a
.const_f32_like(vec![0.1, 0.2, 0.3, 0.4], Shape::from_dims(&[4]))
.unwrap();
let y = a.sub(&b).tanh();
let graph = y.graph().clone();
let fused = RuleRegistry::default_rules().optimize_to_fixpoint(&graph, &[y.id()]);
{
let g = graph.read().unwrap();
match &g.node(fused[0]).op {
Op::Fused(fid, FusedOpParams::Runtime { scalars }) => {
assert_eq!(*fid, rid, "fused to our runtime op");
assert!(fid.is_runtime());
assert!(scalars.is_empty(), "parameterless region");
}
other => panic!("expected runtime Op::Fused, got {other:?}"),
}
}
let lowered = RuleRegistry::lowering_only().optimize_to_fixpoint(&graph, &fused);
let g = graph.read().unwrap();
assert!(
matches!(g.node(lowered[0]).op, Op::Tanh),
"re-emitted sink is Tanh"
);
let sub_id = g.node(lowered[0]).inputs[0];
assert!(matches!(g.node(sub_id).op, Op::Sub), "Tanh's input is Sub");
assert_eq!(
g.node(sub_id).inputs.len(),
2,
"Sub over the two bound inputs"
);
}
#[test]
fn capability_gate_blocks_fusion_without_a_kernel() {
use crate::jit::{OpAttrs, OpTag, PatternNode};
use crate::runtime_fused::register_runtime_fused;
let region = PatternNode::Op {
op: OpTag::Sigmoid,
attrs: OpAttrs::default(),
operands: vec![PatternNode::Op {
op: OpTag::Div,
attrs: OpAttrs::default(),
operands: vec![
PatternNode::Bind { index: 0 },
PatternNode::Bind { index: 1 },
],
}],
};
let rid = register_runtime_fused("test::sigmoid_div", region).unwrap();
let a = NodeHandle::from_f32(vec![1.0, 2.0, 3.0, 4.0], Shape::from_dims(&[4]), cpu_dev())
.unwrap();
let b = a
.const_f32_like(vec![2.0, 2.0, 2.0, 2.0], Shape::from_dims(&[4]))
.unwrap();
let y = a.div(&b).sigmoid();
let graph = y.graph().clone();
let (rules, gated_out) = RuleRegistry::capability_gated_rules(|id| id != rid);
assert!(
gated_out.contains(&rid),
"kernel-absent op is reported as a miss candidate"
);
let roots = rules.optimize_to_fixpoint(&graph, &[y.id()]);
{
let g = graph.read().unwrap();
assert!(
matches!(g.node(roots[0]).op, Op::Sigmoid),
"kernel-absent runtime op stays primitive (Sigmoid sink), never fuses",
);
}
let (rules2, _) = RuleRegistry::capability_gated_rules(|_| true);
let roots2 = rules2.optimize_to_fixpoint(&graph, &[roots[0]]);
let g = graph.read().unwrap();
assert!(
matches!(g.node(roots2[0]).op, Op::Fused(fid, _) if fid == rid),
"with a kernel, the runtime op fuses",
);
}
#[test]
fn cse_does_not_fold_distinct_ops() {
let (graph, a) = make_scalar_graph();
let sum = a.add(&a);
let prod = a.mul(&a);
let new_roots = optimize(&graph, &[sum.id(), prod.id()]);
assert_ne!(new_roots[0], new_roots[1], "Add and Mul must stay distinct");
}
#[test]
fn cse_does_not_fold_distinct_scalars() {
let (graph, a) = make_scalar_graph();
let b = a.add_scalar(1.0);
let c = a.add_scalar(2.0);
let new_roots = optimize(&graph, &[b.id(), c.id()]);
assert_ne!(
new_roots[0], new_roots[1],
"AddScalar with different c must stay distinct"
);
}
#[test]
fn cse_nested_chain_deduplicates() {
let (graph, a) = make_scalar_graph();
let p1 = a.add(&a);
let p2 = a.add(&a); let q1 = p1.add(&a);
let q2 = p2.add(&a); let new_roots = optimize(&graph, &[q1.id(), q2.id()]);
assert_eq!(new_roots[0], new_roots[1], "nested duplicates must fold");
}
#[test]
fn derive_ordering_empty_for_non_destructive_graph() {
let a = NodeHandle::from_f32(vec![1.0, 2.0], Shape::from_dims(&[2]), cpu_dev()).unwrap();
let b = a
.const_f32_like(vec![3.0, 4.0], Shape::from_dims(&[2]))
.unwrap();
let c = a.add(&b);
let ord = derive_ordering(&c.graph().read().unwrap(), &[c.id()]);
assert!(
ord.is_empty(),
"graph without destructive ops → no ordering edges"
);
}
#[test]
fn derive_ordering_release_must_run_after_sibling_readers() {
let a = NodeHandle::from_f32(vec![1.0, 2.0], Shape::from_dims(&[2]), cpu_dev()).unwrap();
let b = a.relu();
let r = a.release();
let ord = derive_ordering(&a.graph().read().unwrap(), &[b.id(), r.id()]);
let deps = ord.deps_of(r.id());
assert_eq!(
deps.len(),
1,
"release should have one ordering dep (the relu)"
);
assert_eq!(deps[0], b.id(), "release must run after relu");
}
#[test]
fn derive_ordering_follows_view_chains_for_destructive_op() {
let x = NodeHandle::from_f32(
vec![1.0_f32, 2.0, 3.0, 4.0],
Shape::from_dims(&[2, 2]),
cpu_dev(),
)
.unwrap();
let v = x.transpose();
let z = v.relu();
let r = x.release();
let ord = derive_ordering(&x.graph().read().unwrap(), &[z.id(), r.id()]);
let deps = ord.deps_of(r.id());
assert!(
deps.contains(&z.id()),
"release(x) must be pinned after relu(transpose(x)) (view-aware alias set); got deps = {deps:?}",
);
}
#[test]
fn derive_ordering_inplace_unary_through_view_chain() {
let x = NodeHandle::from_f32(
vec![-1.0_f32, 2.0, -3.0, 4.0],
Shape::from_dims(&[2, 2]),
cpu_dev(),
)
.unwrap();
let y = x.transpose();
let z = y.relu();
let x_shape = x.shape();
let x_dtype = x.dtype();
let x_id = x.id();
let inplace_id = x.graph().write().unwrap().push(crate::Node {
op: crate::Op::ReluInplace,
inputs: vec![x_id],
shape: x_shape,
dtype: x_dtype,
});
let ord = derive_ordering(&x.graph().read().unwrap(), &[z.id(), inplace_id]);
let deps = ord.deps_of(inplace_id);
assert!(
deps.contains(&z.id()),
"ReluInplace(x) must be pinned after Relu(Transpose(x)) via view-aware alias set; got deps = {deps:?}",
);
}
#[test]
fn derive_ordering_write_slice_round2_plain_slice_reader_pinned() {
let dest =
NodeHandle::from_f32(vec![0.0_f32; 4], Shape::from_dims(&[4]), cpu_dev()).unwrap();
let src1 = dest
.const_f32_like(vec![1.0_f32, 2.0], Shape::from_dims(&[2]))
.unwrap();
let src2 = dest
.const_f32_like(vec![9.0_f32, 8.0], Shape::from_dims(&[2]))
.unwrap();
let x = dest
.write_slice(&src1, vec![(0, 2)])
.expect("write_slice round 1");
let s = x.slice(0, 0, 2);
let k = s.relu();
let y = x
.write_slice(&src2, vec![(0, 2)])
.expect("write_slice round 2");
let ord = derive_ordering(&x.graph().read().unwrap(), &[k.id(), y.id()]);
let deps = ord.deps_of(y.id());
assert!(
deps.contains(&k.id()),
"round-2 write_slice must be pinned after the round-1 slice's \
reader; got deps = {deps:?}",
);
}
#[test]
fn derive_ordering_write_slice_round2_reshape_reader_pinned() {
let dest =
NodeHandle::from_f32(vec![0.0_f32; 4], Shape::from_dims(&[4]), cpu_dev()).unwrap();
let src1 = dest
.const_f32_like(vec![1.0_f32, 2.0], Shape::from_dims(&[2]))
.unwrap();
let src2 = dest
.const_f32_like(vec![9.0_f32, 8.0], Shape::from_dims(&[2]))
.unwrap();
let x = dest
.write_slice(&src1, vec![(0, 2)])
.expect("write_slice round 1");
let s = x.slice(0, 0, 2);
let r = s.reshape(Shape::from_dims(&[2]));
let k = r.relu();
let y = x
.write_slice(&src2, vec![(0, 2)])
.expect("write_slice round 2");
let ord = derive_ordering(&x.graph().read().unwrap(), &[k.id(), y.id()]);
let deps = ord.deps_of(y.id());
assert!(
deps.contains(&k.id()),
"round-2 write_slice must be pinned after the reshape-adopted-arc \
reader (CRITICAL SUSPECT #2 gap — Reshape excluded from \
collect_alias_set's alias-extending predicate); got deps = {deps:?}",
);
}
#[test]
fn insert_safety_copies_residual_connection_breaks_cycle() {
let x = NodeHandle::from_f32(
vec![1.0_f32, -2.0, 3.0, -4.0],
Shape::from_dims(&[4]),
cpu_dev(),
)
.unwrap();
let x_shape = x.shape();
let x_dtype = x.dtype();
let x_id = x.id();
let (y_id, z_id) = {
let mut g = x.graph().write().unwrap();
let y_id = g.push(crate::Node {
op: crate::Op::ReluInplace,
inputs: vec![x_id],
shape: x_shape.clone(),
dtype: x_dtype,
});
let z_id = g.push(crate::Node {
op: crate::Op::Add,
inputs: vec![y_id, x_id],
shape: x_shape,
dtype: x_dtype,
});
(y_id, z_id)
};
let inserted = insert_safety_copies(&mut x.graph().write().unwrap(), &[z_id]);
assert_eq!(inserted, 1, "should insert exactly one safety copy");
let copy_id = {
let g = x.graph().read().unwrap();
let z_node = g.node(z_id);
assert_eq!(z_node.inputs.len(), 2, "Add still has 2 inputs");
assert_eq!(z_node.inputs[0], y_id, "Add's y input unchanged");
assert_ne!(z_node.inputs[1], x_id, "Add's x input was rewired");
let copy_node_id = z_node.inputs[1];
let copy_node = g.node(copy_node_id);
assert!(
matches!(copy_node.op, crate::Op::Copy { .. }),
"rewired input is Op::Copy; got {:?}",
copy_node.op
);
assert_eq!(copy_node.inputs, vec![x_id], "Op::Copy reads x");
copy_node_id
};
let plan = execution_plan(&x.graph().read().unwrap(), &[z_id]);
let pos: HashMap<NodeId, usize> = plan.iter().enumerate().map(|(i, n)| (*n, i)).collect();
let copy_pos = pos[©_id];
let inplace_pos = pos[&y_id];
assert!(
copy_pos < inplace_pos,
"Copy must run before ReluInplace; copy={copy_pos} inplace={inplace_pos}"
);
}
#[test]
fn insert_safety_copies_derives_target_location_from_target_backend_when_unplaced() {
let x = NodeHandle::from_f32(
vec![1.0_f32, -2.0, 3.0, -4.0],
Shape::from_dims(&[4]),
cpu_dev(),
)
.unwrap();
let x_shape = x.shape();
let x_dtype = x.dtype();
let x_id = x.id();
let (_y_id, z_id) = {
let mut g = x.graph().write().unwrap();
let y_id = g.push(crate::Node {
op: crate::Op::ReluInplace,
inputs: vec![x_id],
shape: x_shape.clone(),
dtype: x_dtype,
});
let z_id = g.push(crate::Node {
op: crate::Op::Add,
inputs: vec![y_id, x_id],
shape: x_shape,
dtype: x_dtype,
});
g.set_target_backend(y_id, fuel_ir::probe::BackendId::Cuda);
g.set_target_backend(z_id, fuel_ir::probe::BackendId::Cuda);
(y_id, z_id)
};
assert!(
x.graph().read().unwrap().placement(x_id).is_none(),
"precondition: x has no explicit placement",
);
let inserted = insert_safety_copies(&mut x.graph().write().unwrap(), &[z_id]);
assert_eq!(inserted, 1, "should insert exactly one safety copy");
let g = x.graph().read().unwrap();
let z_node = g.node(z_id);
let copy_id = z_node.inputs[1];
let copy_node = g.node(copy_id);
assert_eq!(
copy_node.op,
crate::Op::Copy {
target: DeviceLocation::Cuda { gpu_id: 0 }
},
"same-device CUDA safety copy must target Cuda (derived from \
target_backend), NOT fall back to Cpu when placement is unset",
);
}
#[test]
fn insert_safety_copies_noop_when_no_destructive_conflicts() {
let x = NodeHandle::from_f32(vec![1.0, 2.0], Shape::from_dims(&[2]), cpu_dev()).unwrap();
let y = x.relu();
let z = y.sum_all();
let inserted = insert_safety_copies(&mut x.graph().write().unwrap(), &[z.id()]);
assert_eq!(inserted, 0);
}
#[test]
fn insert_safety_copies_noop_when_inplace_target_only_consumed_via_alias_path() {
let x =
NodeHandle::from_f32(vec![1.0_f32, -1.0], Shape::from_dims(&[2]), cpu_dev()).unwrap();
let x_shape = x.shape();
let x_dtype = x.dtype();
let x_id = x.id();
let y_id = x.graph().write().unwrap().push(crate::Node {
op: crate::Op::ReluInplace,
inputs: vec![x_id],
shape: x_shape,
dtype: x_dtype,
});
let inserted = insert_safety_copies(&mut x.graph().write().unwrap(), &[y_id]);
assert_eq!(inserted, 0);
}
#[test]
fn insert_safety_copies_no_spurious_copy_for_independent_reader_of_move() {
for flip_roots in [false, true] {
let a = NodeHandle::from_f32(vec![1.0_f32, -2.0], Shape::from_dims(&[2]), cpu_dev())
.unwrap();
let b = a.relu();
let a_shape = a.shape();
let a_dtype = a.dtype();
let a_id = a.id();
let m_id = a.graph().write().unwrap().push(crate::Node {
op: crate::Op::Move {
target: DeviceLocation::Cpu,
},
inputs: vec![a_id],
shape: a_shape,
dtype: a_dtype,
});
let roots = if flip_roots {
[m_id, b.id()]
} else {
[b.id(), m_id]
};
let inserted = insert_safety_copies(&mut a.graph().write().unwrap(), &roots);
assert_eq!(
inserted, 0,
"relu(a) is pinned before move(a) by derive_ordering; \
no copy regardless of roots order (flip_roots={flip_roots})",
);
let g = a.graph().read().unwrap();
assert_eq!(
g.node(b.id()).inputs,
vec![a_id],
"reader must keep reading `a` directly",
);
drop(g);
let plan = execution_plan(&a.graph().read().unwrap(), &roots);
let b_pos = plan.iter().position(|&n| n == b.id()).unwrap();
let m_pos = plan.iter().position(|&n| n == m_id).unwrap();
assert!(b_pos < m_pos, "relu must precede move: {plan:?}");
}
}
#[test]
fn insert_safety_copies_transitive_descendant_reader_gets_copy() {
let x = NodeHandle::from_f32(
vec![1.0_f32, -2.0, 3.0, -4.0],
Shape::from_dims(&[4]),
cpu_dev(),
)
.unwrap();
let x_shape = x.shape();
let x_dtype = x.dtype();
let x_id = x.id();
let (y_id, z_id) = {
let mut g = x.graph().write().unwrap();
let y_id = g.push(crate::Node {
op: crate::Op::ReluInplace,
inputs: vec![x_id],
shape: x_shape.clone(),
dtype: x_dtype,
});
let w_id = g.push(crate::Node {
op: crate::Op::Relu,
inputs: vec![y_id],
shape: x_shape.clone(),
dtype: x_dtype,
});
let z_id = g.push(crate::Node {
op: crate::Op::Add,
inputs: vec![w_id, x_id],
shape: x_shape,
dtype: x_dtype,
});
(y_id, z_id)
};
let inserted = insert_safety_copies(&mut x.graph().write().unwrap(), &[z_id]);
assert_eq!(inserted, 1, "transitive residual cycle needs one copy");
let g = x.graph().read().unwrap();
let z_node = g.node(z_id);
assert_ne!(z_node.inputs[1], x_id, "z's x input must be rewired");
assert!(matches!(
g.node(z_node.inputs[1]).op,
crate::Op::Copy { .. }
));
drop(g);
let plan = execution_plan(&x.graph().read().unwrap(), &[z_id]);
assert!(plan.contains(&y_id), "plan still contains the in-place op");
}
#[test]
fn insert_safety_copies_parallel_view_root_reader_keeps_copy() {
let x = NodeHandle::from_f32(
vec![1.0_f32, 2.0, 3.0, 4.0],
Shape::from_dims(&[2, 2]),
cpu_dev(),
)
.unwrap();
let v = x.transpose();
let x_shape = x.shape();
let x_dtype = x.dtype();
let x_id = x.id();
let m_id = x.graph().write().unwrap().push(crate::Node {
op: crate::Op::Move {
target: DeviceLocation::Cpu,
},
inputs: vec![x_id],
shape: x_shape,
dtype: x_dtype,
});
let inserted = insert_safety_copies(&mut x.graph().write().unwrap(), &[m_id, v.id()]);
assert_eq!(
inserted, 1,
"unpinned view-root reader has no ordering guarantee → copy",
);
let g = x.graph().read().unwrap();
let v_node = g.node(v.id());
assert_ne!(
v_node.inputs[0], x_id,
"view must be rewired to the snapshot"
);
assert!(matches!(
g.node(v_node.inputs[0]).op,
crate::Op::Copy { .. }
));
}
#[test]
fn insert_safety_copies_breaks_cycle_through_other_ordering_edges() {
let t1 =
NodeHandle::from_f32(vec![1.0_f32, 2.0], Shape::from_dims(&[2]), cpu_dev()).unwrap();
let t2 = t1
.const_f32_like(vec![3.0_f32, 4.0], Shape::from_dims(&[2]))
.unwrap();
let shape = t1.shape();
let dtype = t1.dtype();
let (t1_id, t2_id) = (t1.id(), t2.id());
let (x_id, r_id) = {
let mut g = t1.graph().write().unwrap();
let m1 = g.push(crate::Node {
op: crate::Op::Move {
target: DeviceLocation::Cpu,
},
inputs: vec![t1_id],
shape: shape.clone(),
dtype,
});
let m2 = g.push(crate::Node {
op: crate::Op::Move {
target: DeviceLocation::Cpu,
},
inputs: vec![t2_id],
shape: shape.clone(),
dtype,
});
let x = g.push(crate::Node {
op: crate::Op::Add,
inputs: vec![m1, t2_id],
shape: shape.clone(),
dtype,
});
let r = g.push(crate::Node {
op: crate::Op::Add,
inputs: vec![m2, t1_id],
shape,
dtype,
});
(x, r)
};
let inserted = insert_safety_copies(&mut t1.graph().write().unwrap(), &[x_id, r_id]);
assert_eq!(inserted, 2, "both cross-readers sit on the combined cycle");
let plan = execution_plan(&t1.graph().read().unwrap(), &[x_id, r_id]);
assert!(plan.contains(&x_id) && plan.contains(&r_id));
}
#[test]
fn insert_safety_copies_idempotent_after_rewrite() {
let x =
NodeHandle::from_f32(vec![1.0_f32, -2.0], Shape::from_dims(&[2]), cpu_dev()).unwrap();
let x_shape = x.shape();
let x_dtype = x.dtype();
let x_id = x.id();
let z_id = {
let mut g = x.graph().write().unwrap();
let y_id = g.push(crate::Node {
op: crate::Op::ReluInplace,
inputs: vec![x_id],
shape: x_shape.clone(),
dtype: x_dtype,
});
g.push(crate::Node {
op: crate::Op::Add,
inputs: vec![y_id, x_id],
shape: x_shape,
dtype: x_dtype,
})
};
let first = insert_safety_copies(&mut x.graph().write().unwrap(), &[z_id]);
assert_eq!(first, 1);
let second = insert_safety_copies(&mut x.graph().write().unwrap(), &[z_id]);
assert_eq!(second, 0, "second run must be a no-op");
}
#[test]
fn insert_safety_copies_skips_reach_when_target_has_no_other_reader() {
const N: usize = 4000;
let shape = Shape::from_dims(&[4]);
let mut g = crate::Graph::new();
let mut roots: Vec<NodeId> = Vec::with_capacity(N);
for _ in 0..N {
let leaf = g.push(crate::Node {
op: crate::Op::Const,
inputs: vec![],
shape: shape.clone(),
dtype: DType::F32,
});
let relu = g.push(crate::Node {
op: crate::Op::ReluInplace,
inputs: vec![leaf],
shape: shape.clone(),
dtype: DType::F32,
});
roots.push(relu);
}
let start = std::time::Instant::now();
let inserted = insert_safety_copies(&mut g, &roots);
let elapsed = start.elapsed();
assert_eq!(
inserted, 0,
"no leaf has a reader besides its own ReluInplace; nothing to copy"
);
assert!(
elapsed < std::time::Duration::from_millis(500),
"insert_safety_copies({N} independent no-reader chains) took {elapsed:?} — \
the no-other-reader pre-check should make this O(N), not O(N^2)"
);
}
type SafetyCorpusFactory = fn() -> (SharedGraph, Vec<NodeId>);
#[derive(Debug, PartialEq)]
struct SafetyAnalysisResult {
inserted: usize,
node_count: usize,
nodes: Vec<(String, Vec<NodeId>)>,
}
fn run_old_safety_analysis(graph: &SharedGraph, roots: &[NodeId]) -> SafetyAnalysisResult {
let inserted = insert_safety_copies(&mut graph.write().unwrap(), roots);
let g = graph.read().unwrap();
let nodes = (0..g.len())
.map(|i| {
let n = g.node(NodeId(i));
(format!("{:?}", n.op), n.inputs.clone())
})
.collect();
SafetyAnalysisResult {
inserted,
node_count: g.len(),
nodes,
}
}
fn run_new_safety_analysis(graph: &SharedGraph, roots: &[NodeId]) -> SafetyAnalysisResult {
run_old_safety_analysis(graph, roots)
}
fn assert_safety_analysis_matches(name: &str, factory: SafetyCorpusFactory) {
let (old_graph, old_roots) = factory();
let (new_graph, new_roots) = factory();
assert_eq!(
old_roots, new_roots,
"{name}: factory produced different roots across two calls -- corpus factory \
must be deterministic (same pushes, same order) or this harness can't compare"
);
let old = run_old_safety_analysis(&old_graph, &old_roots);
let new = run_new_safety_analysis(&new_graph, &new_roots);
assert_eq!(old, new, "{name}: old vs new safety analysis diverged");
}
fn corpus_no_destructive_conflicts() -> (SharedGraph, Vec<NodeId>) {
let a = NodeHandle::from_f32(vec![1.0, 2.0], Shape::from_dims(&[2]), cpu_dev()).unwrap();
let b = a
.const_f32_like(vec![3.0, 4.0], Shape::from_dims(&[2]))
.unwrap();
let c = a.add(&b);
(c.graph().clone(), vec![c.id()])
}
fn corpus_residual_connection() -> (SharedGraph, Vec<NodeId>) {
let x = NodeHandle::from_f32(
vec![1.0_f32, -2.0, 3.0, -4.0],
Shape::from_dims(&[4]),
cpu_dev(),
)
.unwrap();
let x_shape = x.shape();
let x_dtype = x.dtype();
let x_id = x.id();
let z_id = {
let mut g = x.graph().write().unwrap();
let y_id = g.push(crate::Node {
op: crate::Op::ReluInplace,
inputs: vec![x_id],
shape: x_shape.clone(),
dtype: x_dtype,
});
g.push(crate::Node {
op: crate::Op::Add,
inputs: vec![y_id, x_id],
shape: x_shape,
dtype: x_dtype,
})
};
(x.graph().clone(), vec![z_id])
}
fn corpus_independent_reader_of_move_needs_no_copy() -> (SharedGraph, Vec<NodeId>) {
let a =
NodeHandle::from_f32(vec![1.0_f32, -2.0], Shape::from_dims(&[2]), cpu_dev()).unwrap();
let b = a.relu();
let a_shape = a.shape();
let a_dtype = a.dtype();
let a_id = a.id();
let m_id = a.graph().write().unwrap().push(crate::Node {
op: crate::Op::Move {
target: DeviceLocation::Cpu,
},
inputs: vec![a_id],
shape: a_shape,
dtype: a_dtype,
});
(a.graph().clone(), vec![b.id(), m_id])
}
fn corpus_many_independent_no_reader_chains() -> (SharedGraph, Vec<NodeId>) {
const N: usize = 25;
let shape = Shape::from_dims(&[4]);
let mut g = crate::Graph::new();
let mut roots: Vec<NodeId> = Vec::with_capacity(N);
for _ in 0..N {
let leaf = g.push(crate::Node {
op: crate::Op::Const,
inputs: vec![],
shape: shape.clone(),
dtype: DType::F32,
});
let relu = g.push(crate::Node {
op: crate::Op::ReluInplace,
inputs: vec![leaf],
shape: shape.clone(),
dtype: DType::F32,
});
roots.push(relu);
}
(Arc::new(std::sync::RwLock::new(g)), roots)
}
const SAFETY_CORPUS: &[(&str, SafetyCorpusFactory)] = &[
("no_destructive_conflicts", corpus_no_destructive_conflicts),
("residual_connection", corpus_residual_connection),
(
"independent_reader_of_move_needs_no_copy",
corpus_independent_reader_of_move_needs_no_copy,
),
(
"many_independent_no_reader_chains",
corpus_many_independent_no_reader_chains,
),
];
#[test]
fn differential_safety_analysis_matches_on_corpus() {
for &(name, factory) in SAFETY_CORPUS {
assert_safety_analysis_matches(name, factory);
}
}
#[test]
fn derive_ordering_release_of_multi_reader_input() {
let a = NodeHandle::from_f32(vec![1.0, 2.0], Shape::from_dims(&[2]), cpu_dev()).unwrap();
let b = a.relu();
let c = a.neg();
let r = a.release();
let ord = derive_ordering(&a.graph().read().unwrap(), &[b.id(), c.id(), r.id()]);
let mut deps = ord.deps_of(r.id()).to_vec();
deps.sort_by_key(|n| n.0);
let mut expected = vec![b.id(), c.id()];
expected.sort_by_key(|n| n.0);
assert_eq!(deps, expected);
}
#[test]
fn execution_plan_matches_topo_when_no_destructive_ops() {
let a = NodeHandle::from_f32(vec![1.0, 2.0], Shape::from_dims(&[2]), cpu_dev()).unwrap();
let b = a
.const_f32_like(vec![3.0, 4.0], Shape::from_dims(&[2]))
.unwrap();
let c = a.add(&b);
let graph = c.graph().read().unwrap();
let plan = execution_plan(&graph, &[c.id()]);
let topo = topo_order_multi(&graph, &[c.id()]);
assert_eq!(plan, topo);
}
#[test]
fn execution_plan_pins_release_after_sibling_reader() {
let a = NodeHandle::from_f32(vec![1.0, 2.0], Shape::from_dims(&[2]), cpu_dev()).unwrap();
let b = a.relu();
let r = a.release();
let plan = execution_plan(&a.graph().read().unwrap(), &[b.id(), r.id()]);
let b_pos = plan.iter().position(|&n| n == b.id()).unwrap();
let r_pos = plan.iter().position(|&n| n == r.id()).unwrap();
assert!(
b_pos < r_pos,
"expected relu@{b_pos} to precede release@{r_pos}: {plan:?}"
);
}
#[test]
fn execution_plan_handles_chain_of_destructive_ops() {
let a = NodeHandle::from_f32(vec![1.0, 2.0], Shape::from_dims(&[2]), cpu_dev()).unwrap();
let b = a.relu();
let c = a.neg();
let r = a.release();
let plan = execution_plan(&a.graph().read().unwrap(), &[b.id(), c.id(), r.id()]);
let b_pos = plan.iter().position(|&n| n == b.id()).unwrap();
let c_pos = plan.iter().position(|&n| n == c.id()).unwrap();
let r_pos = plan.iter().position(|&n| n == r.id()).unwrap();
assert!(
b_pos < r_pos,
"relu@{b_pos} must precede release@{r_pos}: {plan:?}"
);
assert!(
c_pos < r_pos,
"neg@{c_pos} must precede release@{r_pos}: {plan:?}"
);
}
#[test]
fn insert_evict_reload_creates_expected_chain() {
let a =
NodeHandle::from_f32(vec![1.0_f32, 2.0], Shape::from_dims(&[2]), cpu_dev()).unwrap();
let b = a.relu();
let c = a.neg();
let (move_id, reload_id) =
insert_evict_reload(a.graph(), a.id(), DeviceLocation::Cpu, &[c.id()]);
let g = a.graph().read().unwrap();
match &g.node(move_id).op {
Op::Move {
target: DeviceLocation::Cpu,
} => {}
other => panic!("expected Op::Move{{Cpu}}, got {other:?}"),
}
assert_eq!(g.node(move_id).inputs, vec![a.id()]);
assert_eq!(
g.node(move_id).op.destructive_input(),
Some(0),
"Move must destroy its source so the device storage frees",
);
assert!(matches!(g.node(reload_id).op, Op::Copy { .. }));
assert_eq!(g.node(reload_id).inputs, vec![move_id]);
assert_eq!(
g.node(c.id()).inputs,
vec![reload_id],
"post-gap consumer should read from reload, not candidate directly"
);
assert_eq!(
g.node(b.id()).inputs,
vec![a.id()],
"pre-gap consumer should still read candidate directly"
);
}
#[test]
fn execution_plan_respects_transitive_data_deps_after_release() {
let a = NodeHandle::from_f32(vec![1.0, 2.0], Shape::from_dims(&[2]), cpu_dev()).unwrap();
let b = a.relu();
let sum = b.sum_all();
let r = a.release();
let plan = execution_plan(&a.graph().read().unwrap(), &[sum.id(), r.id()]);
let b_pos = plan.iter().position(|&n| n == b.id()).unwrap();
let sum_pos = plan.iter().position(|&n| n == sum.id()).unwrap();
let r_pos = plan.iter().position(|&n| n == r.id()).unwrap();
assert!(b_pos < sum_pos);
assert!(b_pos < r_pos);
}
#[test]
fn fuse_linear_collapses_matmul_plus_rank1_bias() {
let a = crate::NodeHandle::from_f32(
vec![1.0_f32, 2.0, 3.0, 4.0, 5.0, 6.0],
crate::Shape::from_dims(&[2, 3]),
cpu_dev(),
)
.unwrap();
let b = a
.const_f32_like(
(0..12).map(|i| (i as f32) * 0.1).collect::<Vec<f32>>(),
crate::Shape::from_dims(&[3, 4]),
)
.unwrap();
let bias = a.const_f32_like(
vec![0.5_f32, -0.5, 1.0, -1.0],
crate::Shape::from_dims(&[4]),
);
let mm = a.matmul(&b);
let bias_b = bias.unwrap().broadcast_to(crate::Shape::from_dims(&[2, 4]));
let out = mm.add(&bias_b);
let n_fused = fuse_linear(out.graph(), &[out.id()]);
assert_eq!(n_fused, 1, "exactly one MatMul→Add(bias[N]) should fuse");
let g = out.graph().read().unwrap();
let any_fused = g.nodes.iter().any(|n| {
matches!(
n.op,
Op::Fused(fid, _) if fid == crate::registry::FusedOps::FUSED_LINEAR,
)
});
assert!(
any_fused,
"graph should contain an Op::Fused(FUSED_LINEAR) node"
);
}
#[test]
fn fuse_linear_skips_when_matmul_has_other_consumers() {
let a = crate::NodeHandle::from_f32(
vec![1.0_f32; 6],
crate::Shape::from_dims(&[2, 3]),
cpu_dev(),
)
.unwrap();
let b = a
.const_f32_like(vec![1.0_f32; 12], crate::Shape::from_dims(&[3, 4]))
.unwrap();
let bias = a
.const_f32_like(vec![1.0_f32; 4], crate::Shape::from_dims(&[4]))
.unwrap();
let mm = a.matmul(&b);
let bias_b = bias.broadcast_to(crate::Shape::from_dims(&[2, 4]));
let with_bias = mm.add(&bias_b);
let also_used = mm.relu(); let n_fused = fuse_linear(a.graph(), &[with_bias.id(), also_used.id()]);
assert_eq!(
n_fused, 0,
"MatMul has 2 consumers — fusion would duplicate work"
);
}
fn count_op_in_reachable<F: Fn(&Op) -> bool>(
graph: &SharedGraph,
roots: &[NodeId],
pred: F,
) -> usize {
let g = graph.read().unwrap();
topo_order_multi(&g, roots)
.into_iter()
.filter(|id| pred(&g.node(*id).op))
.count()
}
#[test]
fn softmax_last_dim_lower_rule_produces_canonical_subgraph() {
let x = NodeHandle::from_f32(vec![1.0_f32; 12], Shape::from_dims(&[2, 3, 2]), cpu_dev())
.unwrap();
let sm = x.softmax_last_dim();
let graph = sm.graph().clone();
let registry = RuleRegistry::lowering_only();
let new_roots = registry.optimize_to_fixpoint(&graph, &[sm.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]);
assert_eq!(
reachable.len(),
10,
"lowered subgraph: 9 ops + 1 Const = 10 reachable nodes"
);
drop(g);
let n_reduce_max =
count_op_in_reachable(&graph, &[new_root], |op| matches!(op, Op::ReduceMaxTo(_)));
let n_max_dim =
count_op_in_reachable(&graph, &[new_root], |op| matches!(op, Op::MaxDim(_)));
let n_sum_dim =
count_op_in_reachable(&graph, &[new_root], |op| matches!(op, Op::SumDim(_)));
let n_unsqueeze =
count_op_in_reachable(&graph, &[new_root], |op| matches!(op, Op::Unsqueeze { .. }));
let n_reshape =
count_op_in_reachable(&graph, &[new_root], |op| matches!(op, Op::Reshape(_)));
let n_broadcast =
count_op_in_reachable(&graph, &[new_root], |op| matches!(op, Op::BroadcastTo(_)));
let n_sub = count_op_in_reachable(&graph, &[new_root], |op| matches!(op, Op::Sub));
let n_exp = count_op_in_reachable(&graph, &[new_root], |op| matches!(op, Op::Exp));
let n_reduce_sum =
count_op_in_reachable(&graph, &[new_root], |op| matches!(op, Op::ReduceSumTo(_)));
let n_div = count_op_in_reachable(&graph, &[new_root], |op| matches!(op, Op::Div));
let n_softmax = count_op_in_reachable(
&graph,
&[new_root],
|op| matches!(op, Op::Fused(fid, _) if *fid == FusedOps::SOFTMAX_LAST_DIM),
);
assert_eq!(
n_max_dim, 1,
"T5 max side = MaxDim(last) + Unsqueeze append"
);
assert_eq!(
n_sum_dim, 1,
"T5 sum side = SumDim(last) + Unsqueeze append"
);
assert_eq!(n_unsqueeze, 2, "one keepdim-restore per reduce");
assert_eq!(
n_reduce_max, 0,
"the legacy ReduceMaxTo spelling is retired from the lowering"
);
assert_eq!(
n_reduce_sum, 0,
"the legacy ReduceSumTo spelling is retired from the lowering"
);
assert_eq!(n_reshape, 0, "keepdim restores via Unsqueeze, not Reshape");
assert_eq!(n_broadcast, 2);
assert_eq!(n_sub, 1);
assert_eq!(
n_exp, 1,
"the shared `e` interior emits ONCE (identity-share), not per consumer"
);
assert_eq!(n_div, 1);
assert_eq!(
n_softmax, 0,
"no SoftmaxLastDim/Fused(SOFTMAX_LAST_DIM) should remain reachable post-lowering"
);
let g = graph.read().unwrap();
for id in topo_order_multi(&g, &[new_root]) {
if let Op::BroadcastTo(_) = g.node(id).op {
assert!(
g.has_explicit_layout(id),
"lowered BroadcastTo at {id:?} missing explicit Layout entry — \
Graph::push auto-populate should have set it",
);
let l = g.layout(id);
assert_eq!(l.shape().dims(), &[2, 3, 2]);
assert_eq!(
l.stride().last().copied(),
Some(0),
"BroadcastTo from [2,3,1] to [2,3,2] should have stride 0 on last dim"
);
}
}
}
#[test]
fn softmax_last_dim_fuse_rule_collapses_canonical_subgraph() {
let x =
NodeHandle::from_f32(vec![1.0_f32; 6], Shape::from_dims(&[2, 3]), cpu_dev()).unwrap();
let m = x.reduce_max_to(Shape::from_dims(&[2, 1]));
let mb = m.broadcast_to(Shape::from_dims(&[2, 3]));
let s = x.sub(&mb);
let e = s.exp();
let d = e.reduce_sum_to(Shape::from_dims(&[2, 1]));
let db = d.broadcast_to(Shape::from_dims(&[2, 3]));
let out = e.div(&db);
let graph = out.graph().clone();
let registry = RuleRegistry::new().with_rule(Box::new(FusionRule::from_entry(
&crate::registry::softmax_last_dim::entry(),
)));
let new_roots = registry.optimize_to_fixpoint(&graph, &[out.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]);
assert_eq!(
reachable.len(),
2,
"fused subgraph: 1 Fused(SOFTMAX_LAST_DIM) + 1 Const = 2 reachable nodes"
);
assert!(
matches!(
g.node(new_root).op,
Op::Fused(fid, _) if fid == FusedOps::SOFTMAX_LAST_DIM
),
"post-migration fusion produces Op::Fused(SOFTMAX_LAST_DIM, _)",
);
assert_eq!(g.node(new_root).inputs, vec![x.id()]);
}
#[test]
fn softmax_last_dim_lower_then_fuse_round_trips() {
let x =
NodeHandle::from_f32(vec![1.0_f32; 6], Shape::from_dims(&[2, 3]), cpu_dev()).unwrap();
let sm = x.softmax_last_dim();
let graph = sm.graph().clone();
let original_root = sm.id();
let registry = RuleRegistry::default_rules();
let new_roots = registry.optimize_to_fixpoint(&graph, &[original_root]);
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]);
assert_eq!(
reachable.len(),
2,
"round-tripped graph: 1 Fused(SOFTMAX_LAST_DIM) + 1 Const = 2 reachable nodes"
);
assert!(
matches!(
g.node(new_root).op,
Op::Fused(fid, _) if fid == FusedOps::SOFTMAX_LAST_DIM
),
"round-tripped node should be Op::Fused(SOFTMAX_LAST_DIM, _)",
);
assert_eq!(
g.node(new_root).inputs,
vec![x.id()],
"round-tripped Fused(SOFTMAX_LAST_DIM) should consume the original input"
);
assert_eq!(g.node(new_root).shape, g.node(original_root).shape);
assert_eq!(g.node(new_root).dtype, g.node(original_root).dtype);
}
#[test]
fn empty_registry_is_noop() {
let x = NodeHandle::from_f32(vec![1.0_f32; 4], Shape::from_dims(&[4]), cpu_dev()).unwrap();
let y = x.add(&x);
let graph = y.graph().clone();
let pre_len = graph.read().unwrap().len();
let new_roots = RuleRegistry::new().optimize_to_fixpoint(&graph, &[y.id()]);
assert_eq!(new_roots, vec![y.id()]);
assert_eq!(
graph.read().unwrap().len(),
pre_len,
"empty registry should add no nodes"
);
}
#[test]
fn lower_rule_does_not_fire_without_softmax() {
let x = NodeHandle::from_f32(vec![1.0_f32; 4], Shape::from_dims(&[4]), cpu_dev()).unwrap();
let y = x.relu();
let graph = y.graph().clone();
let pre_len = graph.read().unwrap().len();
let new_roots = RuleRegistry::lowering_only().optimize_to_fixpoint(&graph, &[y.id()]);
assert_eq!(new_roots, vec![y.id()]);
assert_eq!(graph.read().unwrap().len(), pre_len);
}
#[test]
fn lower_to_base_map_dissolves_a_fused_op() {
use std::sync::RwLock;
let graph = Arc::new(RwLock::new(Graph::new()));
let sink = {
let mut g = graph.write().unwrap();
let shape = Shape::from_dims(&[1, 4, 8]); let x = g.push(Node {
op: Op::Const,
inputs: vec![],
shape: shape.clone(),
dtype: DType::F32,
});
let cshape = Shape::from_dims(&[4, 8]);
let cos = g.push(Node {
op: Op::Const,
inputs: vec![],
shape: cshape.clone(),
dtype: DType::F32,
});
let sin = g.push(Node {
op: Op::Const,
inputs: vec![],
shape: cshape,
dtype: DType::F32,
});
g.push(Node {
op: Op::Fused(FusedOps::ROPE, FusedOpParams::Rope),
inputs: vec![x, cos, sin],
shape,
dtype: DType::F32,
})
};
let roots = lower_to_base_map(&graph, &[sink]);
let g = graph.read().unwrap();
let reachable = topo_order_multi(&g, &roots);
let has_rope = reachable
.iter()
.any(|&n| matches!(g.node(n).op, Op::Fused(fid, _) if fid == FusedOps::ROPE));
assert!(!has_rope, "ROPE must be lowered to its primitive base map");
}
#[test]
fn base_map_hash_commutative_reorder_is_equal() {
let mk = |swap: bool| {
let mut g = Graph::new();
let s = Shape::from_dims(&[4]);
let a = g.push(Node {
op: Op::Const,
inputs: vec![],
shape: s.clone(),
dtype: DType::F32,
});
let b = g.push(Node {
op: Op::Const,
inputs: vec![],
shape: s.clone(),
dtype: DType::F32,
});
let ins = if swap { vec![b, a] } else { vec![a, b] };
let sink = g.push(Node {
op: Op::Add,
inputs: ins,
shape: s,
dtype: DType::F32,
});
(g, sink)
};
let (g0, r0) = mk(false);
let (g1, r1) = mk(true);
assert_eq!(base_map_hash(&g0, r0), base_map_hash(&g1, r1), "a+b == b+a");
}
#[test]
fn base_map_hash_distinct_ops_differ() {
let mk = |op: Op| {
let mut g = Graph::new();
let s = Shape::from_dims(&[4]);
let a = g.push(Node {
op: Op::Const,
inputs: vec![],
shape: s.clone(),
dtype: DType::F32,
});
let b = g.push(Node {
op: Op::Const,
inputs: vec![],
shape: s.clone(),
dtype: DType::F32,
});
let sink = g.push(Node {
op,
inputs: vec![a, b],
shape: s,
dtype: DType::F32,
});
(g, sink)
};
let (ga, ra) = mk(Op::Add);
let (gm, rm) = mk(Op::Mul);
assert_ne!(base_map_hash(&ga, ra), base_map_hash(&gm, rm), "add != mul");
}
#[test]
fn base_map_hash_slice_concat_fragment_is_deterministic() {
let mk = || {
let mut g = Graph::new();
let s = Shape::from_dims(&[4, 8]);
let half = Shape::from_dims(&[4, 4]);
let x = g.push(Node {
op: Op::Const,
inputs: vec![],
shape: s.clone(),
dtype: DType::F32,
});
let lo = g.push(Node {
op: Op::Slice {
dim: 1,
start: 0,
len: 4,
},
inputs: vec![x],
shape: half.clone(),
dtype: DType::F32,
});
let hi = g.push(Node {
op: Op::Slice {
dim: 1,
start: 4,
len: 4,
},
inputs: vec![x],
shape: half,
dtype: DType::F32,
});
let sink = g.push(Node {
op: Op::Concat { dim: 1 },
inputs: vec![hi, lo],
shape: s,
dtype: DType::F32,
});
(g, sink)
};
let (g0, r0) = mk();
let (g1, r1) = mk();
assert_eq!(
base_map_hash(&g0, r0),
base_map_hash(&g1, r1),
"two independently-built Slice->Concat fragments must hash equal"
);
}
#[test]
fn base_map_hash_folds_real_const_bytes() {
let shape = Shape::from_dims(&[4]);
let a = NodeHandle::from_f32(vec![1.0, 2.0, 3.0, 4.0], shape.clone(), cpu_dev()).unwrap();
let b = NodeHandle::from_f32(vec![1.0, 2.0, 3.0, 4.0], shape.clone(), cpu_dev()).unwrap();
let c = NodeHandle::from_f32(vec![9.0, 9.0, 9.0, 9.0], shape, cpu_dev()).unwrap();
let ga = a.graph().read().unwrap();
let gb = b.graph().read().unwrap();
let gc = c.graph().read().unwrap();
assert_eq!(
base_map_hash(&ga, a.id()),
base_map_hash(&gb, b.id()),
"identical const bytes must hash equal across independently-built graphs"
);
assert_ne!(
base_map_hash(&ga, a.id()),
base_map_hash(&gc, c.id()),
"different const bytes (same shape/dtype) must hash differently"
);
}
#[test]
fn scan_base_map_hash_differs_for_different_bodies() {
use crate::{Graph, Node, Op, ScanEmit, ScanRole};
use fuel_ir::{DType, Shape};
let mk = |mul: bool| {
let mut g = Graph::new();
let s = Shape::from_dims(&[1]);
let carry = g.push(Node {
op: Op::Const,
inputs: vec![],
shape: s.clone(),
dtype: DType::F32,
});
let hole = g.push(Node {
op: Op::ScanPlaceholder {
role: ScanRole::Carry,
index: 0,
},
inputs: vec![],
shape: s.clone(),
dtype: DType::F32,
});
let new_carry = g.push(Node {
op: if mul {
Op::MulScalar(2.0)
} else {
Op::AddScalar(1.0)
},
inputs: vec![hole],
shape: s.clone(),
dtype: DType::F32,
});
let scan = g.push(Node {
op: Op::Scan {
n_carries: 1,
n_xs: 0,
bound: 3,
emit: ScanEmit::All,
early_exit: None,
},
inputs: vec![carry, new_carry, new_carry],
shape: s,
dtype: DType::F32,
});
(g, scan)
};
let (ga, ra) = mk(true);
let (gb, rb) = mk(false);
assert_ne!(
base_map_hash(&ga, ra),
base_map_hash(&gb, rb),
"scans with structurally different bodies must hash differently",
);
}
#[test]
fn scan_base_map_hash_equal_for_same_body_cross_graph() {
use crate::{Graph, Node, Op, ScanEmit, ScanRole};
use fuel_ir::{DType, Shape};
let mk = || {
let mut g = Graph::new();
let s = Shape::from_dims(&[1]);
let carry = g.push(Node {
op: Op::Const,
inputs: vec![],
shape: s.clone(),
dtype: DType::F32,
});
let hole = g.push(Node {
op: Op::ScanPlaceholder {
role: ScanRole::Carry,
index: 0,
},
inputs: vec![],
shape: s.clone(),
dtype: DType::F32,
});
let new_carry = g.push(Node {
op: Op::MulScalar(2.0),
inputs: vec![hole],
shape: s.clone(),
dtype: DType::F32,
});
let scan = g.push(Node {
op: Op::Scan {
n_carries: 1,
n_xs: 0,
bound: 3,
emit: ScanEmit::All,
early_exit: None,
},
inputs: vec![carry, new_carry, new_carry],
shape: s,
dtype: DType::F32,
});
(g, scan)
};
let (g0, r0) = mk();
let (g1, r1) = mk();
assert_eq!(
base_map_hash(&g0, r0),
base_map_hash(&g1, r1),
"same body -> equal hash"
);
}
#[test]
fn gap303_n_carries_participates_in_op_key() {
use crate::{Op, ScanEmit, ScanRole};
let mk = |n_carries: usize| {
op_key(&Op::Scan {
n_carries,
n_xs: 2,
bound: 3,
emit: ScanEmit::All,
early_exit: None,
})
};
let one = mk(1);
assert!(one.is_some(), "Op::Scan must produce an op_key, not None");
for n in [2usize, 3, 7] {
assert_ne!(
mk(n),
one,
"n_carries={n} must not key identically to n_carries=1 (all other fields held equal)"
);
}
assert_ne!(mk(2), mk(3), "distinct carry counts must key distinctly");
let c0 = op_key(&Op::ScanPlaceholder {
role: ScanRole::Carry,
index: 0,
});
let c1 = op_key(&Op::ScanPlaceholder {
role: ScanRole::Carry,
index: 1,
});
let e0 = op_key(&Op::ScanPlaceholder {
role: ScanRole::Elem,
index: 0,
});
assert!(c0.is_some(), "ScanPlaceholder must produce an op_key");
assert_ne!(c0, c1, "Carry/0 vs Carry/1 must differ in the body hash");
assert_ne!(c0, e0, "Carry/0 vs Elem/0 must differ in the body hash");
}
#[test]
fn scan_emit_and_placeholder_role_participate_in_op_key() {
use crate::{Op, ScanEmit, ScanRole};
let all = op_key(&Op::Scan {
n_carries: 1,
n_xs: 0,
bound: 3,
emit: ScanEmit::All,
early_exit: None,
});
let fin = op_key(&Op::Scan {
n_carries: 1,
n_xs: 0,
bound: 3,
emit: ScanEmit::Final,
early_exit: None,
});
assert!(
all.is_some() && fin.is_some(),
"Op::Scan must produce an op_key, not None"
);
assert_ne!(all, fin, "emit=All vs emit=Final must differ in op_key");
assert_ne!(
op_key(&Op::Scan {
n_carries: 1,
n_xs: 1,
bound: 3,
emit: ScanEmit::All,
early_exit: None
}),
all
);
assert_ne!(
op_key(&Op::Scan {
n_carries: 1,
n_xs: 0,
bound: 4,
emit: ScanEmit::All,
early_exit: None
}),
all
);
let c0 = op_key(&Op::ScanPlaceholder {
role: ScanRole::Carry,
index: 0,
});
let e0 = op_key(&Op::ScanPlaceholder {
role: ScanRole::Elem,
index: 0,
});
let c1 = op_key(&Op::ScanPlaceholder {
role: ScanRole::Carry,
index: 1,
});
assert!(c0.is_some());
assert_ne!(c0, e0, "Carry vs Elem must differ");
assert_ne!(c0, c1, "index must participate");
}
#[test]
fn flash_attn_vanilla_decomposes_to_sdpa() {
let s = Shape::from_dims(&[1, 1, 1, 1]);
let q = NodeHandle::from_f32(vec![0.0_f32; 1], s.clone(), cpu_dev()).unwrap();
let k = q.const_f32_like(vec![0.0_f32; 1], s.clone()).unwrap();
let v = q.const_f32_like(vec![0.0_f32; 1], s.clone()).unwrap();
let attn = q.flash_attn(&k, &v, None, 1.0_f32, false, None, None, None);
let graph = attn.graph().clone();
let new_roots = RuleRegistry::lowering_only().optimize_to_fixpoint(&graph, &[attn.id()]);
assert_eq!(new_roots.len(), 1);
let root = new_roots[0];
let g = graph.read().unwrap();
assert!(
!matches!(g.node(root).op, Op::Fused(fid, _) if fid == crate::registry::FusedOps::FLASH_ATTN),
"vanilla FlashAttn must decompose, not stay fused",
);
let has_matmul = (0..g.len()).any(|i| matches!(g.node(NodeId(i)).op, Op::MatMul));
assert!(has_matmul, "decomposed FlashAttn must contain a MatMul");
}
#[test]
fn flash_attn_causal_decomposes_with_triu_mask() {
let s = Shape::from_dims(&[1, 1, 2, 1]); let q = NodeHandle::from_f32(vec![0.0_f32; 2], s.clone(), cpu_dev()).unwrap();
let k = q.const_f32_like(vec![0.0_f32; 2], s.clone()).unwrap();
let v = q.const_f32_like(vec![0.0_f32; 2], s.clone()).unwrap();
let attn = q.flash_attn(&k, &v, None, 1.0_f32, true, None, None, None); let graph = attn.graph().clone();
let new_roots = RuleRegistry::lowering_only().optimize_to_fixpoint(&graph, &[attn.id()]);
assert_eq!(new_roots.len(), 1);
let root = new_roots[0];
let g = graph.read().unwrap();
assert!(
!matches!(g.node(root).op, Op::Fused(fid, _) if fid == crate::registry::FusedOps::FLASH_ATTN),
"causal FlashAttn must decompose (Triu mask), not stay fused",
);
let has_triu = (0..g.len()).any(|i| matches!(g.node(NodeId(i)).op, Op::Triu { .. }));
assert!(
has_triu,
"causal decomposition must contain a Triu mask band"
);
}
#[test]
fn fuse_rule_does_not_fire_on_plain_div() {
let a = NodeHandle::from_f32(vec![1.0_f32; 4], Shape::from_dims(&[4]), cpu_dev()).unwrap();
let b = a
.const_f32_like(vec![2.0_f32; 4], Shape::from_dims(&[4]))
.unwrap();
let c = a.div(&b);
let graph = c.graph().clone();
let pre_len = graph.read().unwrap().len();
let new_roots = RuleRegistry::new()
.with_rule(Box::new(FusionRule::from_entry(
&crate::registry::softmax_last_dim::entry(),
)))
.optimize_to_fixpoint(&graph, &[c.id()]);
assert_eq!(new_roots, vec![c.id()]);
assert_eq!(graph.read().unwrap().len(), pre_len);
}
fn allow_all_predicate() -> CapabilityPredicate {
Arc::new(|_op: &Op, _dtypes: &[fuel_ir::DType]| true)
}
fn deny_all_predicate() -> CapabilityPredicate {
Arc::new(|_op: &Op, _dtypes: &[fuel_ir::DType]| false)
}
#[test]
fn cast_fusion_drops_cast_when_consumer_supports_source_dtype() {
let x = NodeHandle::from_f32(vec![1.0_f32; 4], Shape::from_dims(&[4]), cpu_dev()).unwrap();
let xc = x.cast(fuel_ir::DType::BF16);
let y = xc.neg();
let graph = y.graph().clone();
let pre_len = graph.read().unwrap().len();
let registry =
RuleRegistry::new().with_rule(Box::new(CastFusionRule::new(allow_all_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();
assert!(matches!(g.node(new_root).op, Op::Neg));
assert_eq!(g.node(new_root).inputs, vec![x.id()]);
let reachable = topo_order_multi(&g, &[new_root]);
assert!(
!reachable
.iter()
.any(|&n| matches!(g.node(n).op, Op::Cast(_))),
"no Cast node should be reachable post-fusion"
);
assert!(g.len() > pre_len);
}
#[test]
fn cast_fusion_no_fire_when_predicate_denies() {
let x = NodeHandle::from_f32(vec![1.0_f32; 4], Shape::from_dims(&[4]), cpu_dev()).unwrap();
let xc = x.cast(fuel_ir::DType::BF16);
let y = xc.neg();
let graph = y.graph().clone();
let pre_len = graph.read().unwrap().len();
let registry =
RuleRegistry::new().with_rule(Box::new(CastFusionRule::new(deny_all_predicate())));
let new_roots = registry.optimize_to_fixpoint(&graph, &[y.id()]);
assert_eq!(new_roots, vec![y.id()]);
assert_eq!(
graph.read().unwrap().len(),
pre_len,
"predicate-says-no should append no nodes"
);
let g = graph.read().unwrap();
assert!(matches!(g.node(y.id()).op, Op::Neg));
assert_eq!(g.node(y.id()).inputs, vec![xc.id()]);
assert!(matches!(g.node(xc.id()).op, Op::Cast(_)));
}
#[test]
fn cast_fusion_no_fire_when_cast_has_multiple_consumers() {
let x = NodeHandle::from_f32(vec![1.0_f32; 4], Shape::from_dims(&[4]), cpu_dev()).unwrap();
let xc = x.cast(fuel_ir::DType::BF16);
let y1 = xc.neg();
let y2 = xc.relu();
let graph = y1.graph().clone();
let pre_len = graph.read().unwrap().len();
let registry =
RuleRegistry::new().with_rule(Box::new(CastFusionRule::new(allow_all_predicate())));
let new_roots = registry.optimize_to_fixpoint(&graph, &[y1.id(), y2.id()]);
assert_eq!(new_roots, vec![y1.id(), y2.id()]);
assert_eq!(graph.read().unwrap().len(), pre_len);
let g = graph.read().unwrap();
assert!(matches!(g.node(xc.id()).op, Op::Cast(_)));
}
#[test]
fn cast_fusion_rewrites_output_dtype_to_source_dtype() {
let x = NodeHandle::from_f32(vec![1.0_f32; 4], Shape::from_dims(&[4]), cpu_dev()).unwrap();
let xc = x.cast(fuel_ir::DType::BF16);
let y = xc.neg();
assert_eq!(y.dtype(), fuel_ir::DType::BF16);
let graph = y.graph().clone();
let registry =
RuleRegistry::new().with_rule(Box::new(CastFusionRule::new(allow_all_predicate())));
let new_roots = registry.optimize_to_fixpoint(&graph, &[y.id()]);
let new_root = new_roots[0];
let g = graph.read().unwrap();
assert_eq!(g.node(new_root).dtype, fuel_ir::DType::F32);
}
#[test]
fn cast_fusion_multi_input_op_fuses_only_qualifying_edge() {
let a = NodeHandle::from_f32(vec![1.0_f32; 4], Shape::from_dims(&[4]), cpu_dev()).unwrap();
let ac = a.cast(fuel_ir::DType::BF16);
let b = a
.const_bf16_like(vec![half::bf16::from_f32(2.0); 4], Shape::from_dims(&[4]))
.unwrap();
let sum = ac.add(&b);
let graph = sum.graph().clone();
let predicate: CapabilityPredicate =
Arc::new(|op: &Op, _dtypes: &[fuel_ir::DType]| matches!(op, Op::Add));
let registry = RuleRegistry::new().with_rule(Box::new(CastFusionRule::new(predicate)));
let new_roots = registry.optimize_to_fixpoint(&graph, &[sum.id()]);
let new_root = new_roots[0];
let g = graph.read().unwrap();
assert!(matches!(g.node(new_root).op, Op::Add));
assert_eq!(g.node(new_root).inputs, vec![a.id(), b.id()]);
}
#[test]
fn cast_fusion_handles_chained_casts_via_fixpoint() {
let x = NodeHandle::from_f32(vec![1.0_f32; 4], Shape::from_dims(&[4]), cpu_dev()).unwrap();
let x_bf16 = x.cast(fuel_ir::DType::BF16);
let x_f32 = x_bf16.cast(fuel_ir::DType::F32);
let y = x_f32.neg();
let graph = y.graph().clone();
let registry =
RuleRegistry::new().with_rule(Box::new(CastFusionRule::new(allow_all_predicate())));
let new_roots = registry.optimize_to_fixpoint(&graph, &[y.id()]);
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,
"fixpoint should eliminate both casts in the chain"
);
assert!(matches!(g.node(new_root).op, Op::Neg));
assert_eq!(g.node(new_root).inputs, vec![x.id()]);
}
#[test]
fn cast_fusion_does_not_fire_on_cast_node_itself() {
let x = NodeHandle::from_f32(vec![1.0_f32; 4], Shape::from_dims(&[4]), cpu_dev()).unwrap();
let xc = x.cast(fuel_ir::DType::BF16);
let graph = xc.graph().clone();
let pre_len = graph.read().unwrap().len();
let registry =
RuleRegistry::new().with_rule(Box::new(CastFusionRule::new(allow_all_predicate())));
let new_roots = registry.optimize_to_fixpoint(&graph, &[xc.id()]);
assert_eq!(new_roots, vec![xc.id()]);
assert_eq!(graph.read().unwrap().len(), pre_len);
}
use std::collections::HashMap as StdHashMap;
fn add_node(g: &mut Graph, op: Op, inputs: Vec<NodeId>) -> NodeId {
g.push(Node {
op,
inputs,
shape: Shape::from_dims(&[4]),
dtype: DType::F32,
})
}
fn snapshot_placements(g: &Graph) -> StdHashMap<NodeId, DeviceLocation> {
let mut m = StdHashMap::new();
for id in 0..g.len() {
let nid = NodeId(id);
if let Some(loc) = g.placement(nid) {
m.insert(nid, loc);
}
}
m
}
fn build_two_node_graph_with_placements(
a: DeviceLocation,
b: DeviceLocation,
) -> (Graph, NodeId, NodeId) {
let mut g = Graph::new();
let n1 = add_node(&mut g, Op::Const, vec![]);
let n2 = add_node(&mut g, Op::Neg, vec![n1]);
g.set_placement(n1, a);
g.set_placement(n2, b);
(g, n1, n2)
}
fn shares_storage_cpu_devices_only(a: DeviceLocation, b: DeviceLocation) -> bool {
match (a, b) {
(DeviceLocation::Cpu, DeviceLocation::Cpu) => true,
(DeviceLocation::Cuda { gpu_id: x }, DeviceLocation::Cuda { gpu_id: y }) => x == y,
(DeviceLocation::Vulkan { gpu_id: x }, DeviceLocation::Vulkan { gpu_id: y }) => x == y,
(DeviceLocation::Metal { gpu_id: x }, DeviceLocation::Metal { gpu_id: y }) => x == y,
_ => false,
}
}
fn no_host_staging(_a: DeviceLocation, _b: DeviceLocation) -> bool {
false
}
fn host_staging_cross_vendor_gpu(a: DeviceLocation, b: DeviceLocation) -> bool {
use DeviceLocation::*;
match (a, b) {
(Cpu, _) | (_, Cpu) => false,
(Cuda { .. }, Cuda { .. }) => false,
(Vulkan { .. }, Vulkan { .. }) => false,
(Metal { .. }, Metal { .. }) => false,
_ => true,
}
}
#[test]
fn insert_copies_noop_when_all_same_device() {
let (mut g, _n1, n2) =
build_two_node_graph_with_placements(DeviceLocation::Cpu, DeviceLocation::Cpu);
let pre_len = g.len();
let placements = snapshot_placements(&g);
let inserted = insert_cross_device_copies(
&mut g,
&[n2],
|id| placements.get(&id).copied(),
shares_storage_cpu_devices_only,
no_host_staging,
);
assert_eq!(inserted.len(), 0, "same-device edge → no copy needed");
assert_eq!(g.len(), pre_len, "graph unchanged");
}
#[test]
fn insert_copies_cuda_consumer_of_cpu_input() {
let cuda = DeviceLocation::Cuda { gpu_id: 0 };
let (mut g, n1, n2) = build_two_node_graph_with_placements(DeviceLocation::Cpu, cuda);
let pre_len = g.len();
let placements = snapshot_placements(&g);
let inserted = insert_cross_device_copies(
&mut g,
&[n2],
|id| placements.get(&id).copied(),
shares_storage_cpu_devices_only,
no_host_staging,
);
assert_eq!(inserted.len(), 1);
assert_eq!(g.len(), pre_len + 1, "one new Op::Copy node appended");
let consumer_inputs = &g.node(n2).inputs;
assert_eq!(consumer_inputs.len(), 1);
let copy_id = consumer_inputs[0];
assert_ne!(copy_id, n1, "consumer's input was rewired");
let copy_node = g.node(copy_id);
assert!(
matches!(copy_node.op, Op::Copy { target } if target == cuda),
"rewired input is Op::Copy targeting CUDA; got {:?}",
copy_node.op,
);
assert_eq!(copy_node.inputs, vec![n1], "Op::Copy reads from n1");
assert_eq!(g.placement(copy_id), Some(cuda), "Op::Copy lands on CUDA");
}
#[test]
fn insert_copies_dedupes_when_two_consumers_share_input_and_target() {
let cuda = DeviceLocation::Cuda { gpu_id: 0 };
let mut g = Graph::new();
let cpu_src = add_node(&mut g, Op::Const, vec![]);
g.set_placement(cpu_src, DeviceLocation::Cpu);
let cuda_a = add_node(&mut g, Op::Neg, vec![cpu_src]);
let cuda_b = add_node(&mut g, Op::Sqr, vec![cpu_src]);
g.set_placement(cuda_a, cuda);
g.set_placement(cuda_b, cuda);
let pre_len = g.len();
let placements = snapshot_placements(&g);
let inserted = insert_cross_device_copies(
&mut g,
&[cuda_a, cuda_b],
|id| placements.get(&id).copied(),
shares_storage_cpu_devices_only,
no_host_staging,
);
assert_eq!(
inserted.len(),
1,
"CSE — one Op::Copy serves both consumers"
);
assert_eq!(g.len(), pre_len + 1);
let a_input = g.node(cuda_a).inputs[0];
let b_input = g.node(cuda_b).inputs[0];
assert_eq!(a_input, b_input, "both consumers share the rewired input");
}
#[test]
fn insert_copies_does_not_dedupe_across_distinct_targets() {
let cuda = DeviceLocation::Cuda { gpu_id: 0 };
let vulkan = DeviceLocation::Vulkan { gpu_id: 0 };
let mut g = Graph::new();
let cpu_src = add_node(&mut g, Op::Const, vec![]);
g.set_placement(cpu_src, DeviceLocation::Cpu);
let cuda_c = add_node(&mut g, Op::Neg, vec![cpu_src]);
let vk_c = add_node(&mut g, Op::Sqr, vec![cpu_src]);
g.set_placement(cuda_c, cuda);
g.set_placement(vk_c, vulkan);
let placements = snapshot_placements(&g);
let inserted = insert_cross_device_copies(
&mut g,
&[cuda_c, vk_c],
|id| placements.get(&id).copied(),
shares_storage_cpu_devices_only,
no_host_staging,
);
assert_eq!(inserted.len(), 2, "distinct targets → distinct copies");
let cuda_input = g.node(cuda_c).inputs[0];
let vk_input = g.node(vk_c).inputs[0];
assert_ne!(cuda_input, vk_input);
}
#[test]
fn insert_copies_skips_when_placement_absent() {
let mut g = Graph::new();
let n1 = add_node(&mut g, Op::Const, vec![]);
let n2 = add_node(&mut g, Op::Neg, vec![n1]);
g.set_placement(n2, DeviceLocation::Cuda { gpu_id: 0 });
let placements = snapshot_placements(&g);
let inserted = insert_cross_device_copies(
&mut g,
&[n2],
|id| placements.get(&id).copied(),
shares_storage_cpu_devices_only,
no_host_staging,
);
assert_eq!(inserted.len(), 0, "producer with no placement → skip");
}
#[test]
fn insert_copies_skips_transfer_ops_themselves() {
let cuda = DeviceLocation::Cuda { gpu_id: 0 };
let mut g = Graph::new();
let cpu_src = add_node(&mut g, Op::Const, vec![]);
g.set_placement(cpu_src, DeviceLocation::Cpu);
let copy = g.push(Node {
op: Op::Copy { target: cuda },
inputs: vec![cpu_src],
shape: Shape::from_dims(&[4]),
dtype: DType::F32,
});
g.set_placement(copy, cuda);
let pre_len = g.len();
let placements = snapshot_placements(&g);
let inserted = insert_cross_device_copies(
&mut g,
&[copy],
|id| placements.get(&id).copied(),
shares_storage_cpu_devices_only,
no_host_staging,
);
assert_eq!(inserted.len(), 0, "the existing Op::Copy is not re-wrapped");
assert_eq!(g.len(), pre_len);
}
#[test]
fn insert_cross_device_copies_idempotent() {
let cuda = DeviceLocation::Cuda { gpu_id: 0 };
let (mut g, _n1, n2) = build_two_node_graph_with_placements(DeviceLocation::Cpu, cuda);
let placements_before = snapshot_placements(&g);
let first = insert_cross_device_copies(
&mut g,
&[n2],
|id| placements_before.get(&id).copied(),
shares_storage_cpu_devices_only,
no_host_staging,
);
let placements_after = snapshot_placements(&g);
let second = insert_cross_device_copies(
&mut g,
&[n2],
|id| placements_after.get(&id).copied(),
shares_storage_cpu_devices_only,
no_host_staging,
);
assert_eq!(first.len(), 1);
assert_eq!(
second.len(),
0,
"re-run sees inserted-Copy as on-target; no new copies"
);
}
#[test]
fn insert_copies_across_gpu_ids_within_one_backend() {
let cuda0 = DeviceLocation::Cuda { gpu_id: 0 };
let cuda1 = DeviceLocation::Cuda { gpu_id: 1 };
let (mut g, _n1, n2) = build_two_node_graph_with_placements(cuda0, cuda1);
let placements = snapshot_placements(&g);
let inserted = insert_cross_device_copies(
&mut g,
&[n2],
|id| placements.get(&id).copied(),
shares_storage_cpu_devices_only,
no_host_staging,
);
assert_eq!(inserted.len(), 1, "cross-gpu_id is cross-device too");
let copy_id = g.node(n2).inputs[0];
let copy_node = g.node(copy_id);
assert!(matches!(copy_node.op, Op::Copy { target } if target == cuda1));
}
#[test]
fn insert_copies_cross_vendor_gpu_inserts_two_hops_through_cpu() {
let cuda = DeviceLocation::Cuda { gpu_id: 0 };
let vulkan = DeviceLocation::Vulkan { gpu_id: 0 };
let (mut g, vk_src, cuda_consumer) = build_two_node_graph_with_placements(vulkan, cuda);
let pre_len = g.len();
let placements = snapshot_placements(&g);
let inserted = insert_cross_device_copies(
&mut g,
&[cuda_consumer],
|id| placements.get(&id).copied(),
shares_storage_cpu_devices_only,
host_staging_cross_vendor_gpu,
);
assert_eq!(inserted.len(), 2, "cross-vendor GPU↔GPU → two hops");
assert_eq!(g.len(), pre_len + 2, "two new Op::Copy nodes appended");
let hop2 = g.node(cuda_consumer).inputs[0];
let hop2_node = g.node(hop2);
assert!(
matches!(hop2_node.op, Op::Copy { target } if target == cuda),
"consumer-side hop targets CUDA; got {:?}",
hop2_node.op,
);
assert_eq!(
g.placement(hop2),
Some(cuda),
"consumer-side hop lands on CUDA"
);
let hop1 = hop2_node.inputs[0];
let hop1_node = g.node(hop1);
assert!(
matches!(hop1_node.op, Op::Copy { target } if target == DeviceLocation::Cpu),
"CPU-intermediate hop targets Cpu; got {:?}",
hop1_node.op,
);
assert_eq!(
g.placement(hop1),
Some(DeviceLocation::Cpu),
"CPU intermediate is placed on CPU",
);
assert_eq!(
hop1_node.inputs,
vec![vk_src],
"CPU hop reads the Vulkan producer"
);
}
#[test]
fn insert_copies_cross_vendor_two_hop_idempotent() {
let cuda = DeviceLocation::Cuda { gpu_id: 0 };
let vulkan = DeviceLocation::Vulkan { gpu_id: 0 };
let (mut g, _vk_src, cuda_consumer) = build_two_node_graph_with_placements(vulkan, cuda);
let p1 = snapshot_placements(&g);
let first = insert_cross_device_copies(
&mut g,
&[cuda_consumer],
|id| p1.get(&id).copied(),
shares_storage_cpu_devices_only,
host_staging_cross_vendor_gpu,
);
let p2 = snapshot_placements(&g);
let second = insert_cross_device_copies(
&mut g,
&[cuda_consumer],
|id| p2.get(&id).copied(),
shares_storage_cpu_devices_only,
host_staging_cross_vendor_gpu,
);
assert_eq!(first.len(), 2, "two hops on first run");
assert_eq!(second.len(), 0, "re-run adds no new copies");
}
#[test]
fn insert_copies_cross_vendor_two_hop_cse_shares_cpu_intermediate() {
let cuda = DeviceLocation::Cuda { gpu_id: 0 };
let vulkan = DeviceLocation::Vulkan { gpu_id: 0 };
let mut g = Graph::new();
let vk_src = add_node(&mut g, Op::Const, vec![]);
g.set_placement(vk_src, vulkan);
let cuda_a = add_node(&mut g, Op::Neg, vec![vk_src]);
let cuda_b = add_node(&mut g, Op::Sqr, vec![vk_src]);
g.set_placement(cuda_a, cuda);
g.set_placement(cuda_b, cuda);
let placements = snapshot_placements(&g);
let inserted = insert_cross_device_copies(
&mut g,
&[cuda_a, cuda_b],
|id| placements.get(&id).copied(),
shares_storage_cpu_devices_only,
host_staging_cross_vendor_gpu,
);
assert_eq!(
inserted.len(),
2,
"CSE — one CPU intermediate + one CUDA hop"
);
let a_input = g.node(cuda_a).inputs[0];
let b_input = g.node(cuda_b).inputs[0];
assert_eq!(
a_input, b_input,
"both CUDA consumers share the consumer-side hop"
);
let cpu_hop = g.node(a_input).inputs[0];
assert!(matches!(
g.node(cpu_hop).op,
Op::Copy {
target: DeviceLocation::Cpu
}
));
assert_eq!(g.node(cpu_hop).inputs, vec![vk_src]);
}
#[test]
fn insert_copies_cpu_gpu_edge_stays_single_hop_under_cross_vendor_predicate() {
let cuda = DeviceLocation::Cuda { gpu_id: 0 };
let (mut g, n1, n2) = build_two_node_graph_with_placements(DeviceLocation::Cpu, cuda);
let pre_len = g.len();
let placements = snapshot_placements(&g);
let inserted = insert_cross_device_copies(
&mut g,
&[n2],
|id| placements.get(&id).copied(),
shares_storage_cpu_devices_only,
host_staging_cross_vendor_gpu,
);
assert_eq!(inserted.len(), 1, "CPU↔CUDA is a single hop");
assert_eq!(g.len(), pre_len + 1);
let copy_id = g.node(n2).inputs[0];
assert!(matches!(g.node(copy_id).op, Op::Copy { target } if target == cuda));
assert_eq!(g.node(copy_id).inputs, vec![n1]);
}
#[test]
fn insert_copies_takes_external_placement_oracle() {
let cuda = DeviceLocation::Cuda { gpu_id: 0 };
let (mut g, n1, n2) = build_two_node_graph_with_placements(DeviceLocation::Cpu, cuda);
let mut external: StdHashMap<NodeId, DeviceLocation> = StdHashMap::new();
external.insert(n1, DeviceLocation::Cpu);
external.insert(n2, cuda);
let _placements = snapshot_placements(&g);
let inserted = insert_cross_device_copies(
&mut g,
&[n2],
|id| external.get(&id).copied(),
shares_storage_cpu_devices_only,
no_host_staging,
);
assert_eq!(inserted.len(), 1);
}
fn count_contiguize_nodes(graph: &SharedGraph) -> usize {
let g = graph.read().unwrap();
(0..g.len())
.filter(|i| matches!(g.node(NodeId(*i)).op, Op::Contiguize))
.count()
}
#[test]
fn insert_fixups_no_strided_inputs_no_fixups() {
let a = NodeHandle::from_f32(vec![1.0, 2.0], Shape::from_dims(&[2]), cpu_dev()).unwrap();
let b = a
.const_f32_like(vec![3.0, 4.0], Shape::from_dims(&[2]))
.unwrap();
let c = a.add(&b);
let graph = c.graph().clone();
let before = count_contiguize_nodes(&graph);
let inserted = {
let mut g = graph.write().unwrap();
insert_layout_fixups(&mut g, &[c.id()], |_| false)
};
assert_eq!(inserted, 0);
assert_eq!(count_contiguize_nodes(&graph), before);
}
#[test]
fn insert_fixups_strided_input_rejecting_kernel_inserts_contiguize() {
let a = NodeHandle::from_f32(
vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0],
Shape::from_dims(&[2, 3]),
cpu_dev(),
)
.unwrap();
let at = a.transpose();
let b = at
.const_f32_like(vec![0.0; 6], Shape::from_dims(&[3, 2]))
.unwrap();
let c = at.add(&b);
let graph = c.graph().clone();
let before = count_contiguize_nodes(&graph);
let inserted = {
let mut g = graph.write().unwrap();
insert_layout_fixups(&mut g, &[c.id()], |_| false)
};
assert_eq!(inserted, 1);
assert_eq!(count_contiguize_nodes(&graph) - before, 1);
let g = graph.read().unwrap();
let c_node = g.node(c.id());
let new_input = g.node(c_node.inputs[0]);
assert!(
matches!(new_input.op, Op::Contiguize),
"consumer's strided input must be rewired through Op::Contiguize, \
got {:?}",
new_input.op,
);
assert_eq!(new_input.inputs, vec![at.id()]);
}
#[test]
fn insert_fixups_strided_input_accepting_kernel_no_fixup() {
let a = NodeHandle::from_f32(
vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0],
Shape::from_dims(&[2, 3]),
cpu_dev(),
)
.unwrap();
let at = a.transpose();
let b = at
.const_f32_like(vec![0.0; 6], Shape::from_dims(&[3, 2]))
.unwrap();
let c = at.add(&b);
let graph = c.graph().clone();
let before = count_contiguize_nodes(&graph);
let inserted = {
let mut g = graph.write().unwrap();
insert_layout_fixups(&mut g, &[c.id()], |_| true)
};
assert_eq!(inserted, 0);
assert_eq!(count_contiguize_nodes(&graph), before);
}
#[test]
fn insert_fixups_cse_dedupes_shared_strided_input() {
let a = NodeHandle::from_f32(
vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0],
Shape::from_dims(&[2, 3]),
cpu_dev(),
)
.unwrap();
let at = a.transpose();
let b = at
.const_f32_like(vec![0.0; 6], Shape::from_dims(&[3, 2]))
.unwrap();
let c1 = at.add(&b);
let c2 = at.mul(&b);
let graph = c1.graph().clone();
let inserted = {
let mut g = graph.write().unwrap();
insert_layout_fixups(&mut g, &[c1.id(), c2.id()], |_| false)
};
assert_eq!(inserted, 1);
let g = graph.read().unwrap();
let c1_input = g.node(c1.id()).inputs[0];
let c2_input = g.node(c2.id()).inputs[0];
assert_eq!(
c1_input, c2_input,
"CSE: both consumers must share the same Contiguize node",
);
assert!(matches!(g.node(c1_input).op, Op::Contiguize));
}
#[test]
fn insert_fixups_idempotent() {
let a = NodeHandle::from_f32(
vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0],
Shape::from_dims(&[2, 3]),
cpu_dev(),
)
.unwrap();
let at = a.transpose();
let b = at
.const_f32_like(vec![0.0; 6], Shape::from_dims(&[3, 2]))
.unwrap();
let c = at.add(&b);
let graph = c.graph().clone();
let first = {
let mut g = graph.write().unwrap();
insert_layout_fixups(&mut g, &[c.id()], |_| false)
};
assert_eq!(first, 1, "first run inserts one Contiguize");
let second = {
let mut g = graph.write().unwrap();
insert_layout_fixups(&mut g, &[c.id()], |_| false)
};
assert_eq!(second, 0, "second run is a no-op");
}
#[test]
fn insert_fixups_skips_view_op_consumers() {
let a = NodeHandle::from_f32(
vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0],
Shape::from_dims(&[2, 3]),
cpu_dev(),
)
.unwrap();
let at = a.transpose();
let ap = at.permute(&[1, 0]);
let graph = ap.graph().clone();
let inserted = {
let mut g = graph.write().unwrap();
insert_layout_fixups(&mut g, &[ap.id()], |_| false)
};
assert_eq!(
inserted, 0,
"view ops preserve strided layouts; no Contiguize should be inserted",
);
}
}