use std::collections::HashSet;
use fuel_graph::registry::{FusedOpId, FusedOpParams};
use fuel_graph::{Graph, Node, NodeId, Op};
use fuel_ir::backend::BackendCapabilities;
use fuel_ir::{DType, Shape};
use crate::fused::{BackendImpl, CostEstimate};
use crate::kernel::OpParams;
const MAX_DECOMPOSE_DEPTH: u32 = 16;
fn elem_count(shape: &Shape) -> u64 {
shape.dims().iter().map(|&d| d as u64).product::<u64>()
}
pub fn is_fused_cost_sentinel(
cost: fn(&[Shape], &FusedOpParams, &BackendCapabilities) -> CostEstimate,
) -> bool {
std::ptr::eq(
cost as *const (),
crate::fkc::fused_unknown_cost as *const (),
)
}
pub fn fused_layer1_cost(
impl_: &BackendImpl,
id: FusedOpId,
input_shapes: &[Shape],
input_dtypes: &[DType],
params: &FusedOpParams,
caps: &BackendCapabilities,
) -> CostEstimate {
if let Some(expr) = impl_.cost_expr
&& let Ok(est) =
crate::fkc::cost_compile::fused_cost_estimate(expr, input_shapes, input_dtypes, params)
{
return est;
}
if is_fused_cost_sentinel(impl_.cost) {
cost_from_decompose(id, params, input_shapes, input_dtypes, caps)
} else {
(impl_.cost)(input_shapes, params, caps)
}
}
pub fn cost_from_decompose(
id: FusedOpId,
params: &FusedOpParams,
input_shapes: &[Shape],
input_dtypes: &[DType],
caps: &BackendCapabilities,
) -> CostEstimate {
cost_from_decompose_inner(id, params, input_shapes, input_dtypes, caps, 0)
}
fn cost_from_decompose_inner(
id: FusedOpId,
params: &FusedOpParams,
input_shapes: &[Shape],
input_dtypes: &[DType],
caps: &BackendCapabilities,
depth: u32,
) -> CostEstimate {
if input_shapes.is_empty() || input_shapes.len() != input_dtypes.len() {
return CostEstimate::default();
}
let mut g = Graph::new();
let mut leaves: HashSet<NodeId> = HashSet::with_capacity(input_shapes.len());
let mut input_ids = Vec::with_capacity(input_shapes.len());
for (s, d) in input_shapes.iter().zip(input_dtypes.iter()) {
let nid = g.push(Node {
op: Op::Const,
inputs: vec![],
shape: s.clone(),
dtype: *d,
});
leaves.insert(nid);
input_ids.push(nid);
}
let fused = g.push(Node {
op: Op::Fused(id, params.clone()),
inputs: input_ids,
shape: input_shapes[0].clone(),
dtype: input_dtypes[0],
});
let root = if id.is_runtime() {
fuel_graph::runtime_fused::decompose_region(&mut g, fused)
} else {
match fuel_graph::registry::default_registry().entry(id) {
Some(entry) => (entry.decompose)(&mut g, fused, params),
None => return CostEstimate::default(),
}
};
if root == fused || leaves.contains(&root) {
return CostEstimate::default();
}
let mut visited: HashSet<NodeId> = HashSet::new();
let mut max_overhead: u32 = 0;
let flops = fold_subgraph(
&g,
root,
&leaves,
&mut visited,
caps,
&mut max_overhead,
depth,
);
let mut bytes: u64 = 0;
for (s, d) in input_shapes.iter().zip(input_dtypes.iter()) {
bytes = bytes.saturating_add(elem_count(s).saturating_mul(crate::cost::dtype_bytes(*d)));
}
let out = g.node(root);
bytes = bytes
.saturating_add(elem_count(&out.shape).saturating_mul(crate::cost::dtype_bytes(out.dtype)));
CostEstimate {
flops,
bytes_moved: bytes,
kernel_overhead_ns: max_overhead,
}
}
fn fold_subgraph(
graph: &Graph,
node_id: NodeId,
leaves: &HashSet<NodeId>,
visited: &mut HashSet<NodeId>,
caps: &BackendCapabilities,
max_overhead: &mut u32,
depth: u32,
) -> u64 {
if leaves.contains(&node_id) || !visited.insert(node_id) {
return 0;
}
let node = graph.node(node_id);
match &node.op {
Op::Const | Op::Iota { .. } => 0,
Op::Fused(nested_id, nested_params) => {
let mut flops = 0u64;
if depth < MAX_DECOMPOSE_DEPTH {
let in_shapes: Vec<Shape> = node
.inputs
.iter()
.map(|&i| graph.node(i).shape.clone())
.collect();
let in_dtypes: Vec<DType> =
node.inputs.iter().map(|&i| graph.node(i).dtype).collect();
let sub = cost_from_decompose_inner(
*nested_id,
nested_params,
&in_shapes,
&in_dtypes,
caps,
depth + 1,
);
*max_overhead = (*max_overhead).max(sub.kernel_overhead_ns);
flops = flops.saturating_add(sub.flops);
}
for &inp in &node.inputs {
flops = flops.saturating_add(fold_subgraph(
graph,
inp,
leaves,
visited,
caps,
max_overhead,
depth,
));
}
flops
}
_ => {
let mut flops = 0u64;
if let Some(kind) = crate::pipelined::op_to_op_kind(&node.op) {
let mut shapes: Vec<Shape> = node
.inputs
.iter()
.map(|&i| graph.node(i).shape.clone())
.collect();
shapes.push(node.shape.clone());
let mut dtypes: Vec<DType> =
node.inputs.iter().map(|&i| graph.node(i).dtype).collect();
dtypes.push(node.dtype);
let c = crate::cost::default_cost_for_op_kind(kind)(
&shapes,
&dtypes,
&OpParams::None,
caps,
);
flops = c.flops;
*max_overhead = (*max_overhead).max(c.kernel_overhead_ns);
}
for &inp in &node.inputs {
flops = flops.saturating_add(fold_subgraph(
graph,
inp,
leaves,
visited,
caps,
max_overhead,
depth,
));
}
flops
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::fused::{KernelRevisionHash, PrecisionGuarantee};
use crate::kernel::{KernelCaps, KernelRef};
use fuel_graph::jit::{OpAttrs, OpTag, PatternNode};
use fuel_graph::runtime_fused::register_runtime_fused;
use fuel_ir::backend::{BackendCapabilities, SubstrateClass, TransferPath};
use fuel_ir::probe::BackendId;
use fuel_ir::{DeviceLocation, Layout};
use std::collections::HashSet as StdHashSet;
use std::sync::{Arc, RwLock};
fn noop_kernel(
_i: &[Arc<RwLock<fuel_memory::Storage>>],
_o: &mut [Arc<RwLock<fuel_memory::Storage>>],
_l: &[Layout],
_p: &OpParams,
) -> fuel_ir::Result<()> {
Ok(())
}
fn cpu_caps() -> BackendCapabilities {
BackendCapabilities {
backend_id: BackendId::Cpu,
device_location: DeviceLocation::Cpu,
op_dtype_support: StdHashSet::new(),
required_alignment: 1,
access_granularity_bits: 8,
transfer_paths: vec![(DeviceLocation::Cpu, TransferPath::SameDevice)],
storage_substrate: SubstrateClass::HostBytes,
compute_throughput_flops_per_ns: 1.0,
mem_bandwidth_bytes_per_ns: 4.0,
}
}
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 declared_cost(_s: &[Shape], _p: &FusedOpParams, _c: &BackendCapabilities) -> CostEstimate {
CostEstimate {
flops: 777,
bytes_moved: 42,
kernel_overhead_ns: 9,
}
}
fn sentinel_impl() -> BackendImpl {
BackendImpl {
kernel: noop_kernel as KernelRef,
dtypes: &[DType::F32, DType::F32, DType::F32],
cost: crate::fkc::fused_unknown_cost,
precision: PrecisionGuarantee::UNAUDITED,
caps: KernelCaps::empty(),
revision: KernelRevisionHash::UNTRACKED,
cost_expr: None,
}
}
#[test]
fn fused_declared_cost_reaches_layer1_not_sentinel() {
use fuel_graph::registry::{FusedOpParams, FusedOps};
let expr = crate::fkc::cost_compile::intern_cost_expr(
&crate::fkc::cost_expr::compile_field(Some("n")).unwrap(),
)
.unwrap();
let impl_ = BackendImpl {
cost_expr: Some(expr),
..sentinel_impl()
};
let caps = cpu_caps();
let est = fused_layer1_cost(
&impl_,
FusedOps::SOFTMAX_LAST_DIM,
&[Shape::from_dims(&[8])],
&[DType::F32],
&FusedOpParams::SoftmaxLastDim,
&caps,
);
assert_eq!(
est.flops, 8,
"declared fused flops = n = 8; not the sentinel/decompose fallback"
);
}
#[test]
fn sentinel_fused_op_gets_nonzero_composed_cost_from_its_decompose() {
let n: u64 = 8;
let shape = Shape::from_dims(&[n as usize]);
let input_shapes = [shape.clone(), shape.clone()];
let input_dtypes = [DType::F32, DType::F32];
let id = register_runtime_fused("test::cost_from_decompose::relu_add", relu_add_region())
.expect("relu(add) is a registrable region");
let impl_ = sentinel_impl();
let caps = cpu_caps();
let raw = (impl_.cost)(
&input_shapes,
&FusedOpParams::Runtime { scalars: vec![] },
&caps,
);
assert_eq!(
raw,
CostEstimate::default(),
"the fused_unknown_cost sentinel prices at zero — the mis-pricing this fixes",
);
let composed = fused_layer1_cost(
&impl_,
id,
&input_shapes,
&input_dtypes,
&FusedOpParams::Runtime { scalars: vec![] },
&caps,
);
assert!(
composed.flops > 0,
"composed cost must be NONZERO (was {:?})",
composed,
);
assert_eq!(
composed.flops,
2 * n,
"flops = Σ decompose primitives = add(n) + relu(n) = 2n",
);
assert_eq!(
composed.bytes_moved,
3 * n * 4,
"bytes = fused op's own boundary I/O (inputs + output), not Σ intermediates",
);
assert_eq!(
composed.kernel_overhead_ns, 50,
"one launch overhead, not Σ"
);
}
#[test]
fn declared_cost_fused_op_is_untouched_by_the_decompose_default() {
let shape = Shape::from_dims(&[8]);
let input_shapes = [shape.clone(), shape.clone()];
let input_dtypes = [DType::F32, DType::F32];
let id = register_runtime_fused("test::cost_from_decompose::declared", relu_add_region())
.expect("region registers");
let mut impl_ = sentinel_impl();
impl_.cost = declared_cost;
assert!(
!is_fused_cost_sentinel(impl_.cost),
"declared cost is not the sentinel"
);
let got = fused_layer1_cost(
&impl_,
id,
&input_shapes,
&input_dtypes,
&FusedOpParams::Runtime { scalars: vec![] },
&cpu_caps(),
);
assert_eq!(
got,
CostEstimate {
flops: 777,
bytes_moved: 42,
kernel_overhead_ns: 9
},
"a declared cost fn is priced by that fn, unchanged — not composed",
);
}
}