use fuel_graph::registry::{FusedOpId, FusedOpParams};
use fuel_graph::{Graph, Node, NodeId, Op};
use fuel_ir::Result;
use fuel_ir::probe::BackendId;
use crate::kernel::KernelBindingTable;
use crate::runtime_fused_kernels::fused_kernel_available_in;
#[derive(Clone, Debug)]
pub struct RuntimeFusedSpec {
pub runtime_id: FusedOpId,
pub inputs: Vec<NodeId>,
pub primitive_sink: NodeId,
pub reconverge: NodeId,
pub backend: BackendId,
pub scalars: Vec<f64>,
}
pub fn offer_runtime_fused_arm(
graph: &mut Graph,
spec: &RuntimeFusedSpec,
table: &KernelBindingTable,
) -> Result<Option<NodeId>> {
debug_assert!(
spec.runtime_id.is_runtime(),
"offer_runtime_fused_arm on a static id"
);
if !fused_kernel_available_in(table, spec.runtime_id, spec.backend) {
return Ok(None);
}
if spec.inputs.is_empty() {
return Ok(None); }
let (shape, dtype) = {
let n = graph.node(spec.primitive_sink);
(n.shape.clone(), n.dtype)
};
let fused = graph.push(Node {
op: Op::Fused(
spec.runtime_id,
FusedOpParams::Runtime {
scalars: spec.scalars.clone(),
},
),
inputs: spec.inputs.clone(),
shape,
dtype,
});
graph.set_target_backend(fused, spec.backend);
let mut builder = graph.open_branch(spec.inputs[0]);
builder.add_arm(spec.primitive_sink); builder.add_arm(fused); builder.finalize_branches(graph, spec.reconverge)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::runtime_fused_kernels::adopt_runtime_fused;
use fuel_graph::jit::{OpAttrs, OpTag, PatternNode};
use fuel_ir::{DType, Layout, Shape};
use std::sync::{Arc, RwLock as StdRwLock};
fn noop_kernel(
_inputs: &[Arc<StdRwLock<fuel_memory::Storage>>],
_outputs: &mut [Arc<StdRwLock<fuel_memory::Storage>>],
_layouts: &[Layout],
_params: &crate::kernel::OpParams,
) -> fuel_ir::Result<()> {
Ok(())
}
fn relu_add_region() -> PatternNode {
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 },
],
}],
}
}
fn graph_with_region() -> (Graph, Vec<NodeId>, NodeId, NodeId) {
let mut g = Graph::new();
let s = Shape::from_dims(&[4]);
let leaf = |g: &mut Graph| {
g.push(Node {
op: Op::Const,
inputs: vec![],
shape: s.clone(),
dtype: DType::F32,
})
};
let a = leaf(&mut g);
let b = leaf(&mut g);
let add = g.push(Node {
op: Op::Add,
inputs: vec![a, b],
shape: s.clone(),
dtype: DType::F32,
});
let relu = g.push(Node {
op: Op::Relu,
inputs: vec![add],
shape: s.clone(),
dtype: DType::F32,
});
let neg = g.push(Node {
op: Op::Neg,
inputs: vec![relu],
shape: s.clone(),
dtype: DType::F32,
});
(g, vec![a, b], relu, neg)
}
#[test]
fn offers_a_gated_branch_when_a_kernel_is_bound() {
let rid = adopt_runtime_fused(
"test::arm::relu_add",
relu_add_region(),
noop_kernel as crate::kernel::KernelRef,
vec![DType::F32, DType::F32, DType::F32],
BackendId::Cpu,
)
.expect("registrable region");
let (mut g, inputs, relu, neg) = graph_with_region();
let spec = RuntimeFusedSpec {
runtime_id: rid,
inputs,
primitive_sink: relu,
reconverge: neg,
backend: BackendId::Cpu,
scalars: vec![],
};
let branch = offer_runtime_fused_arm(&mut g, &spec, &crate::dispatch::global_bindings())
.expect("valid branch")
.expect("emitted");
assert!(
matches!(g.node(branch).op, Op::Branch { .. }),
"an Op::Branch decision point"
);
}
#[test]
fn declines_when_no_kernel_for_the_backend() {
let rid = adopt_runtime_fused(
"test::arm::relu_add::cpu_only",
relu_add_region(),
noop_kernel as crate::kernel::KernelRef,
vec![DType::F32, DType::F32, DType::F32],
BackendId::Cpu,
)
.expect("registrable region");
let (mut g, inputs, relu, neg) = graph_with_region();
let spec = RuntimeFusedSpec {
runtime_id: rid,
inputs,
primitive_sink: relu,
reconverge: neg,
backend: BackendId::Cuda,
scalars: vec![],
};
assert!(
offer_runtime_fused_arm(&mut g, &spec, &crate::dispatch::global_bindings())
.expect("no error")
.is_none(),
"no kernel for Cuda ⇒ no fused arm (region stays primitive)",
);
}
}