use std::sync::Arc;
use fuel_graph::{Graph, NodeId, PickedRoute, branches_in_topo_order};
use fuel_ir::dispatch::{OpKind, SizeClass};
use fuel_ir::probe::BackendId;
use fuel_ir::{DType, DeviceLocation, Shape};
use crate::fused::{CostEstimate, PrecisionGuarantee};
use crate::kernel::{KernelBindingTable, KernelCaps, OpParams};
use crate::pipelined::{build_lookup_dtypes, op_to_op_kind};
use crate::plan::default_device_for;
use crate::ranker::{
AlternativeSet, BackendRuntimeLookup, Candidate, DecisionContext, RuntimeSelector,
enumerate_candidates,
};
pub const LOOKAHEAD_K: usize = 3;
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct TelemetryFingerprint {
buckets: Vec<(BackendId, u8)>,
}
impl TelemetryFingerprint {
fn sample(backends: &[BackendId], lookup: Option<&BackendRuntimeLookup>) -> Self {
let mut buckets: Vec<(BackendId, u8)> = Vec::new();
if let Some(lookup) = lookup {
let mut seen: Vec<BackendId> = Vec::new();
for &b in backends {
if seen.contains(&b) {
continue;
}
seen.push(b);
if let Some(handle) = lookup(b, default_device_for(b))
&& let Some(free) = handle.available_bytes()
{
buckets.push((b, free_bytes_bucket(free)));
}
}
}
buckets.sort_by_key(|&(b, _)| b as u8);
Self { buckets }
}
}
fn free_bytes_bucket(free: u64) -> u8 {
if free == 0 {
return 0;
}
let log2 = 63 - free.leading_zeros(); ((log2 / 4) + 1).min(u8::MAX as u32) as u8
}
#[derive(Clone, Debug, Default)]
pub struct RouteCache {
cached: Option<(TelemetryFingerprint, PickedRoute)>,
pub hits: u64,
pub resolves: u64,
}
impl RouteCache {
pub fn new() -> Self {
Self::default()
}
pub fn resolve(
&mut self,
graph: &Graph,
roots: &[NodeId],
bindings: &KernelBindingTable,
selector: &dyn RuntimeSelector,
lookup: Option<&BackendRuntimeLookup>,
) -> PickedRoute {
let backends = route_backends(graph, roots);
let fingerprint = TelemetryFingerprint::sample(&backends, lookup);
if let Some((cached_fp, cached_route)) = &self.cached
&& *cached_fp == fingerprint
{
self.hits += 1;
return cached_route.clone();
}
let route = pick_route(graph, roots, bindings, selector, lookup);
self.resolves += 1;
self.cached = Some((fingerprint, route.clone()));
route
}
}
fn route_backends(graph: &Graph, roots: &[NodeId]) -> Vec<BackendId> {
let mut backends: Vec<BackendId> = Vec::new();
for branch in branches_in_topo_order(graph, roots) {
for &arm in &graph.node(branch).inputs {
if let Some(b) = graph.target_backend(arm)
&& !backends.contains(&b)
{
backends.push(b);
}
}
}
backends
}
pub fn pick_route(
graph: &Graph,
roots: &[NodeId],
bindings: &KernelBindingTable,
selector: &dyn RuntimeSelector,
lookup: Option<&BackendRuntimeLookup>,
) -> PickedRoute {
let _ = lookup;
let mut picked = PickedRoute::new();
let branches = branches_in_topo_order(graph, roots);
for branch in branches {
let chosen = resolve_branch(graph, branch, bindings, selector);
if chosen != 0 {
picked.insert(branch, chosen);
}
}
picked
}
pub fn resolve_branch(
graph: &Graph,
branch: NodeId,
bindings: &KernelBindingTable,
selector: &dyn RuntimeSelector,
) -> usize {
let arms = graph.node(branch).inputs.clone();
if arms.len() < 2 {
return 0;
}
pick_arm(graph, &arms, bindings, selector)
}
fn pick_arm(
graph: &Graph,
arms: &[NodeId],
bindings: &KernelBindingTable,
selector: &dyn RuntimeSelector,
) -> usize {
let fork = arms[0];
let fork_set = enumerate_fork_set(graph, fork, arms, bindings);
let mut arm_set = AlternativeSet::empty();
for &arm in arms {
let backend = graph.target_backend(arm).unwrap_or(BackendId::Cpu);
let device = default_device_for(backend);
let candidate = candidate_for_placement(&fork_set, backend, device)
.cloned()
.unwrap_or_else(|| synthetic_candidate(backend, device));
arm_set.push(candidate);
}
if let Some(ctx) = fork_set.context() {
arm_set.set_context(*ctx);
}
match selector.select(&arm_set) {
Some(picked) => arm_index_of(&arm_set, picked),
None => 0,
}
}
fn enumerate_fork_set(
graph: &Graph,
fork: NodeId,
arms: &[NodeId],
bindings: &KernelBindingTable,
) -> AlternativeSet {
let node = graph.node(fork);
let Some(op_kind) = op_to_op_kind(&node.op) else {
return AlternativeSet::empty();
};
let dtypes = build_lookup_dtypes(graph, node);
let placements: Vec<(BackendId, DeviceLocation)> = arms
.iter()
.map(|&arm| {
let backend = graph.target_backend(arm).unwrap_or(BackendId::Cpu);
(backend, default_device_for(backend))
})
.collect();
let mut set = enumerate_candidates(op_kind, &dtypes, &placements, &OpParams::None, bindings);
if let Some(ctx) = fork_decision_context(graph, node, op_kind, &dtypes) {
set.set_context(ctx);
}
set
}
fn fork_decision_context(
graph: &Graph,
node: &fuel_graph::Node,
op_kind: OpKind,
dtypes: &[DType],
) -> Option<DecisionContext> {
let &principal_dtype = dtypes.first()?;
let input_shapes: Vec<Shape> = node
.inputs
.iter()
.map(|&input_id| graph.node(input_id).shape.clone())
.collect();
let size_class = SizeClass::for_op(op_kind, &input_shapes);
Some(DecisionContext {
op: op_kind,
principal_dtype,
size_class,
})
}
fn candidate_for_placement(
set: &AlternativeSet,
backend: BackendId,
device: DeviceLocation,
) -> Option<&Candidate> {
set.alternatives()
.iter()
.find(|c| c.backend == backend && c.device == device)
.or_else(|| {
set.alternatives().iter().find(|c| c.backend == backend)
})
}
fn arm_index_of(set: &AlternativeSet, picked: &Candidate) -> usize {
set.alternatives()
.iter()
.position(|c| c.backend == picked.backend && c.device == picked.device)
.unwrap_or(0)
}
fn synthetic_candidate(backend: BackendId, device: DeviceLocation) -> Candidate {
Candidate {
kernel: noop_kernel,
caps: KernelCaps::empty(),
backend,
device,
precision: PrecisionGuarantee::PRIMITIVE_DETERMINISTIC_CPU,
static_cost: CostEstimate {
flops: 0,
bytes_moved: 0,
kernel_overhead_ns: 0,
},
inbound_transfer_ns: 0,
op_params: OpParams::None,
coupling: Vec::new(),
kernel_source: "route-picker-synthetic",
kernel_revision_hash: 0,
}
}
fn noop_kernel(
_i: &[Arc<std::sync::RwLock<fuel_memory::Storage>>],
_o: &mut [Arc<std::sync::RwLock<fuel_memory::Storage>>],
_l: &[fuel_ir::Layout],
_p: &OpParams,
) -> fuel_ir::Result<()> {
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ranker::{BackendRuntimeHandle, ChainedSelector, WinnerSelector};
use fuel_backend_contract::backend::BackendRuntime;
use fuel_graph::{Node, Op};
use fuel_ir::{DType, Shape};
fn f32_node(g: &mut Graph, op: Op, inputs: Vec<NodeId>) -> NodeId {
g.push(Node {
op,
inputs,
shape: Shape::from_dims(&[2]),
dtype: DType::F32,
})
}
fn diamond(
arm0_backend: BackendId,
arm1_backend: BackendId,
) -> (Graph, NodeId, NodeId, NodeId, NodeId) {
let mut g = Graph::new();
let pre = f32_node(&mut g, Op::Const, vec![]);
let diverge = f32_node(&mut g, Op::Relu, vec![pre]);
let arm0 = f32_node(&mut g, Op::Silu, vec![diverge]);
let arm1 = f32_node(&mut g, Op::GeluTanh, vec![diverge]);
g.set_target_backend(arm0, arm0_backend);
g.set_target_backend(arm1, arm1_backend);
let reconverge = f32_node(&mut g, Op::Relu, vec![arm0]);
let mut b = g.open_branch(diverge);
b.add_arm(arm0);
b.add_arm(arm1);
let branch = b
.finalize_branches(&mut g, reconverge)
.expect("well-formed 2-arm branch")
.expect("2 arms survive");
let post = f32_node(&mut g, Op::Tanh, vec![reconverge]);
(g, branch, arm0, arm1, post)
}
fn diamond_bindings() -> KernelBindingTable {
let mut t = KernelBindingTable::new();
for backend in [BackendId::Cuda, BackendId::Cpu] {
t.register(
OpKind::SiluElementwise,
&[DType::F32, DType::F32],
backend,
noop_kernel,
);
}
t
}
struct MockRuntime {
available: Option<u64>,
total: Option<u64>,
}
impl BackendRuntime for MockRuntime {
fn available_bytes(&self) -> Option<u64> {
self.available
}
fn total_bytes(&self) -> Option<u64> {
self.total
}
}
fn lookup_for(entries: Vec<(BackendId, Option<u64>, Option<u64>)>) -> BackendRuntimeLookup {
Arc::new(move |b, _d| {
entries
.iter()
.find(|(eb, _, _)| *eb == b)
.map(|&(_, a, t)| {
Box::new(MockRuntime {
available: a,
total: t,
}) as BackendRuntimeHandle
})
})
}
#[test]
fn no_telemetry_picks_arm0_empty_route() {
let (g, _branch, _fork, _arm1, post) = diamond(BackendId::Cuda, BackendId::Cpu);
let bindings = diamond_bindings();
let winner = WinnerSelector;
let route = pick_route(&g, &[post], &bindings, &winner, None);
assert!(
route.is_empty(),
"no telemetry ⇒ arm-0 everywhere ⇒ empty route; got {route:?}",
);
let chained = ChainedSelector::with_default_estimator(None, None);
let route = pick_route(&g, &[post], &bindings, &chained, None);
assert!(
route.is_empty(),
"ChainedSelector with no signals ⇒ arm-0 everywhere; got {route:?}",
);
}
#[test]
fn vram_pressure_picks_host_ram_arm() {
let (g, branch, _fork, _arm1, post) = diamond(BackendId::Cuda, BackendId::Cpu);
let bindings = diamond_bindings();
let lookup = lookup_for(vec![
(BackendId::Cuda, Some(1), Some(10_000)),
(BackendId::Cpu, Some(8_000), Some(10_000)),
]);
let chained = ChainedSelector::with_default_estimator(None, Some(lookup.clone()));
let route = pick_route(&g, &[post], &bindings, &chained, Some(&lookup));
assert_eq!(
route.get(&branch).copied(),
Some(1),
"under VRAM pressure the picker takes the host-RAM (CPU) arm; \
route={route:?}",
);
}
#[test]
fn two_coupled_branches_resolve_consistently() {
let mut g = Graph::new();
let pre = f32_node(&mut g, Op::Const, vec![]);
let div1 = f32_node(&mut g, Op::Relu, vec![pre]);
let a0_1 = f32_node(&mut g, Op::Silu, vec![div1]);
let a1_1 = f32_node(&mut g, Op::GeluTanh, vec![div1]);
g.set_target_backend(a0_1, BackendId::Cuda);
g.set_target_backend(a1_1, BackendId::Cpu);
let recon1 = f32_node(&mut g, Op::Relu, vec![a0_1]);
let mut b1 = g.open_branch(div1);
b1.add_arm(a0_1);
b1.add_arm(a1_1);
let branch1 = b1
.finalize_branches(&mut g, recon1)
.expect("branch1 valid")
.expect("2 arms");
let div2 = f32_node(&mut g, Op::Tanh, vec![recon1]);
let a0_2 = f32_node(&mut g, Op::Silu, vec![div2]);
let a1_2 = f32_node(&mut g, Op::GeluTanh, vec![div2]);
g.set_target_backend(a0_2, BackendId::Cuda);
g.set_target_backend(a1_2, BackendId::Cpu);
let recon2 = f32_node(&mut g, Op::Relu, vec![a0_2]);
let mut b2 = g.open_branch(div2);
b2.add_arm(a0_2);
b2.add_arm(a1_2);
let branch2 = b2
.finalize_branches(&mut g, recon2)
.expect("branch2 valid")
.expect("2 arms");
let post = f32_node(&mut g, Op::Tanh, vec![recon2]);
let order = branches_in_topo_order(&g, &[post]);
assert_eq!(
order,
vec![branch1, branch2],
"coupled branches resolve upstream-first in topo order",
);
let bindings = diamond_bindings();
let lookup = lookup_for(vec![
(BackendId::Cuda, Some(1), Some(10_000)),
(BackendId::Cpu, Some(8_000), Some(10_000)),
]);
let chained = ChainedSelector::with_default_estimator(None, Some(lookup.clone()));
let route = pick_route(&g, &[post], &bindings, &chained, Some(&lookup));
assert_eq!(route.get(&branch1).copied(), Some(1), "branch1 → CPU arm");
assert_eq!(route.get(&branch2).copied(), Some(1), "branch2 → CPU arm");
}
#[test]
fn route_cached_and_reresolved_on_telemetry_delta() {
let (g, branch, _fork, _arm1, post) = diamond(BackendId::Cuda, BackendId::Cpu);
let bindings = diamond_bindings();
let comfy = lookup_for(vec![
(BackendId::Cuda, Some(8_000), Some(10_000)),
(BackendId::Cpu, Some(8_000), Some(10_000)),
]);
let chained_comfy = ChainedSelector::with_default_estimator(None, Some(comfy.clone()));
let mut cache = RouteCache::new();
let r1 = cache.resolve(&g, &[post], &bindings, &chained_comfy, Some(&comfy));
assert!(r1.is_empty(), "comfortable ⇒ arm-0 (winner) route");
assert_eq!(cache.resolves, 1, "first resolve populates the cache");
assert_eq!(cache.hits, 0);
let r2 = cache.resolve(&g, &[post], &bindings, &chained_comfy, Some(&comfy));
assert_eq!(r2, r1, "stable telemetry reuses the cached route");
assert_eq!(cache.resolves, 1, "no re-resolve on a stable fingerprint");
assert_eq!(cache.hits, 1, "stable telemetry is a cache hit");
let pressured = lookup_for(vec![
(BackendId::Cuda, Some(1), Some(10_000)),
(BackendId::Cpu, Some(8_000), Some(10_000)),
]);
let chained_pressured =
ChainedSelector::with_default_estimator(None, Some(pressured.clone()));
let r3 = cache.resolve(&g, &[post], &bindings, &chained_pressured, Some(&pressured));
assert_eq!(
cache.resolves, 2,
"a meaningful telemetry delta re-resolves"
);
assert_eq!(cache.hits, 1, "the delta is not counted as a hit");
assert_eq!(
r3.get(&branch).copied(),
Some(1),
"after the pressure transition the route picks the CPU arm",
);
}
#[test]
fn branchless_graph_is_a_noop() {
let mut g = Graph::new();
let a = f32_node(&mut g, Op::Const, vec![]);
let b = f32_node(&mut g, Op::Relu, vec![a]);
let c = f32_node(&mut g, Op::Tanh, vec![b]);
let bindings = KernelBindingTable::new();
let winner = WinnerSelector;
let route = pick_route(&g, &[c], &bindings, &winner, None);
assert!(route.is_empty(), "no branches ⇒ empty route (picker no-op)");
}
#[test]
fn enumerate_fork_set_pulls_registered_candidates() {
let (g, _branch, fork, arm1, _post) = diamond(BackendId::Cuda, BackendId::Cpu);
let bindings = diamond_bindings();
let set = enumerate_fork_set(&g, fork, &[fork, arm1], &bindings);
assert_eq!(
set.alternatives().len(),
2,
"Silu registered at both arm backends ⇒ one candidate per placement",
);
assert!(
set.alternatives()
.iter()
.any(|c| c.backend == BackendId::Cuda)
);
assert!(
set.alternatives()
.iter()
.any(|c| c.backend == BackendId::Cpu)
);
let ctx = set.context().expect("fork DecisionContext stamped");
assert_eq!(ctx.op, OpKind::SiluElementwise);
assert_eq!(ctx.principal_dtype, DType::F32);
}
#[test]
fn synthetic_fallback_when_fork_op_unregistered() {
let (g, branch, _fork, _arm1, post) = diamond(BackendId::Cuda, BackendId::Cpu);
let empty = KernelBindingTable::new();
let lookup = lookup_for(vec![
(BackendId::Cuda, Some(1), Some(10_000)),
(BackendId::Cpu, Some(8_000), Some(10_000)),
]);
let chained = ChainedSelector::with_default_estimator(None, Some(lookup.clone()));
let route = pick_route(&g, &[post], &empty, &chained, Some(&lookup));
assert_eq!(
route.get(&branch).copied(),
Some(1),
"no binding ⇒ synthetic placement candidates ⇒ guard still picks CPU",
);
}
fn diamond_bindings_cuda_vulkan() -> KernelBindingTable {
let mut t = KernelBindingTable::new();
for backend in [BackendId::Cuda, BackendId::Vulkan] {
t.register(
OpKind::SiluElementwise,
&[DType::F32, DType::F32],
backend,
noop_kernel,
);
}
t
}
#[test]
fn load_pressure_picks_unloaded_arm() {
let (g, branch, _arm0, arm1, post) = diamond(BackendId::Cuda, BackendId::Vulkan);
let bindings = diamond_bindings_cuda_vulkan();
let lookup = crate::ranker::mock_combined_lookup(vec![
(BackendId::Cuda, Some(10_000), Some(10_000), Some(4), 1),
(BackendId::Vulkan, Some(10_000), Some(10_000), Some(0), 1),
]);
let chained = ChainedSelector::with_default_estimator(None, Some(lookup.clone()));
let route = pick_route(&g, &[post], &bindings, &chained, Some(&lookup));
assert_eq!(
route.get(&branch).copied(),
Some(1),
"under simulated CUDA saturation the picker takes the UNLOADED \
Vulkan arm (arm-1); route={route:?}",
);
assert_eq!(
g.target_backend(arm1),
Some(BackendId::Vulkan),
"the picked arm's target_backend is the unloaded device",
);
}
#[test]
fn no_load_picks_arm0_empty_route() {
let (g, _branch, _arm0, _arm1, post) = diamond(BackendId::Cuda, BackendId::Vulkan);
let bindings = diamond_bindings_cuda_vulkan();
let lookup = crate::ranker::mock_combined_lookup(vec![
(BackendId::Cuda, Some(10_000), Some(10_000), Some(0), 1),
(BackendId::Vulkan, Some(10_000), Some(10_000), Some(0), 1),
]);
let chained = ChainedSelector::with_default_estimator(None, Some(lookup.clone()));
let route = pick_route(&g, &[post], &bindings, &chained, Some(&lookup));
assert!(
route.is_empty(),
"no load anywhere ⇒ arm-0 everywhere ⇒ empty route; got {route:?}",
);
}
}