use std::sync::Arc;
use fuel_ir::backend::FitStatus;
use super::{
AlternativeSet, BackendRuntimeLookup, Candidate, DecisionContext, JudgeOracle,
OutputBytesEstimator, RuntimeSelector, composite_ns, default_backend_rates,
default_estimate_output_bytes,
};
#[derive(Clone)]
pub struct ChainedSelector {
judge: Option<Arc<dyn JudgeOracle>>,
backend_runtime_lookup: Option<BackendRuntimeLookup>,
estimate_output_bytes: OutputBytesEstimator,
}
impl ChainedSelector {
pub fn new(
judge: Option<Arc<dyn JudgeOracle>>,
backend_runtime_lookup: Option<BackendRuntimeLookup>,
estimate_output_bytes: OutputBytesEstimator,
) -> Self {
Self {
judge,
backend_runtime_lookup,
estimate_output_bytes,
}
}
pub fn with_default_estimator(
judge: Option<Arc<dyn JudgeOracle>>,
backend_runtime_lookup: Option<BackendRuntimeLookup>,
) -> Self {
Self::new(
judge,
backend_runtime_lookup,
Arc::new(default_estimate_output_bytes),
)
}
fn fit_and_load_for(&self, c: &Candidate) -> (FitStatus, u8) {
let Some(lookup) = self.backend_runtime_lookup.as_ref() else {
return (FitStatus::Unknown, super::device_load::LOAD_TIER_IDLE);
};
match lookup(c.backend, c.device) {
Some(runtime) => {
let fit = runtime.would_fit((self.estimate_output_bytes)(c));
let load = super::device_load::load_tier_of_handle(runtime.as_ref());
(fit, load)
}
None => (FitStatus::Unknown, super::device_load::LOAD_TIER_IDLE),
}
}
fn latency_ns(&self, c: &Candidate, ctx: Option<&DecisionContext>) -> u64 {
let kernel_ns = (|| {
if let (Some(judge), Some(ctx)) = (self.judge.as_ref(), ctx)
&& let Some(measured) = judge.measured_latency_ns(
ctx.op,
ctx.principal_dtype,
ctx.size_class,
c.backend,
c.kernel_source,
c.kernel_revision_hash,
)
{
return measured;
}
let (cr, bw) = default_backend_rates(c.backend);
composite_ns(&c.static_cost, cr, bw)
})();
kernel_ns.saturating_add(c.inbound_transfer_ns)
}
}
impl std::fmt::Debug for ChainedSelector {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ChainedSelector")
.field("judge", &self.judge.as_ref().map(|_| "<dyn JudgeOracle>"))
.field(
"backend_runtime_lookup",
&self.backend_runtime_lookup.as_ref().map(|_| "<closure>"),
)
.field("estimate_output_bytes", &"<closure>")
.finish()
}
}
impl RuntimeSelector for ChainedSelector {
fn select<'a>(&self, set: &'a AlternativeSet) -> Option<&'a Candidate> {
let alts = set.alternatives();
if alts.is_empty() {
return None;
}
let ctx = set.context();
let mut best: Option<(u8, u8, u64, usize)> = None;
for (i, c) in alts.iter().enumerate() {
let (fit, load_tier) = self.fit_and_load_for(c);
let pressure_tier = match fit {
FitStatus::WontFit => continue,
FitStatus::Comfortable | FitStatus::Unknown => 0u8,
FitStatus::Tight => 1u8,
};
let key = (pressure_tier, load_tier, self.latency_ns(c, ctx), i);
if best.is_none_or(|b| key < b) {
best = Some(key);
}
}
match best {
Some((_, _, _, i)) => alts.get(i),
None => set.winner(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::fused::{CostEstimate, PrecisionGuarantee};
use crate::kernel::{KernelCaps, OpParams};
use crate::ranker::{HashMapJudge, WinnerSelector};
use fuel_backend_contract::backend::BackendRuntime;
use fuel_ir::dispatch::{OpKind, SizeClass};
use fuel_ir::probe::BackendId;
use fuel_ir::{DType, DeviceLocation, Layout, Result};
use fuel_memory::Storage;
use std::sync::{Arc, RwLock};
fn noop_kernel(
_i: &[Arc<RwLock<Storage>>],
_o: &mut [Arc<RwLock<Storage>>],
_l: &[Layout],
_p: &OpParams,
) -> Result<()> {
Ok(())
}
fn make_candidate(backend: BackendId, device: DeviceLocation, cost_ns: u64) -> Candidate {
Candidate {
kernel: noop_kernel,
caps: KernelCaps::empty(),
backend,
device,
precision: PrecisionGuarantee::PRIMITIVE_DETERMINISTIC_CPU,
static_cost: CostEstimate {
flops: cost_ns,
bytes_moved: cost_ns,
kernel_overhead_ns: 0,
},
inbound_transfer_ns: 0,
op_params: OpParams::None,
coupling: Vec::new(),
kernel_source: "",
kernel_revision_hash: 0,
}
}
fn ctx() -> DecisionContext {
DecisionContext {
op: OpKind::MatMul,
principal_dtype: DType::F32,
size_class: SizeClass(16),
}
}
fn ranked_set() -> AlternativeSet {
let mut set = AlternativeSet::empty();
set.push(make_candidate(
BackendId::Cuda,
DeviceLocation::Cuda { gpu_id: 0 },
3000,
));
set.push(make_candidate(BackendId::Cpu, DeviceLocation::Cpu, 200));
set.set_context(ctx());
set
}
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 Box<dyn BackendRuntime + Send + Sync>
})
})
}
struct MockRuntimeWithLoad {
available: Option<u64>,
total: Option<u64>,
pending: Option<u32>,
capacity: u32,
}
impl BackendRuntime for MockRuntimeWithLoad {
fn available_bytes(&self) -> Option<u64> {
self.available
}
fn total_bytes(&self) -> Option<u64> {
self.total
}
fn as_backend_streams(
&self,
) -> Option<&dyn fuel_backend_contract::backend::BackendStreams> {
Some(self)
}
}
impl fuel_backend_contract::backend::BackendStreams for MockRuntimeWithLoad {
fn pending_work_count(&self) -> Option<u32> {
self.pending
}
fn slot_capacity(&self) -> u32 {
self.capacity
}
fn flush(&self) -> Result<()> {
Ok(())
}
}
fn load_lookup_for(
entries: Vec<crate::ranker::device_load::MockCombinedEntry>,
) -> BackendRuntimeLookup {
Arc::new(move |b, _d| {
entries
.iter()
.find(|(eb, _, _, _, _)| *eb == b)
.map(|&(_, a, t, p, cap)| {
Box::new(MockRuntimeWithLoad {
available: a,
total: t,
pending: p,
capacity: cap,
}) as Box<dyn BackendRuntime + Send + Sync>
})
})
}
#[test]
fn load_demotes_saturated_arm_within_vram_tier() {
let set = ranked_set();
let lookup = load_lookup_for(vec![
(BackendId::Cuda, Some(10_000), Some(10_000), Some(4), 1),
(BackendId::Cpu, Some(10_000), Some(10_000), Some(0), 1),
]);
let chained = ChainedSelector::with_default_estimator(None, Some(lookup));
assert_eq!(
chained.select(&set).expect("non-empty").backend,
BackendId::Cpu,
"saturated cost-winner demoted below the idle arm by the load leg",
);
}
#[test]
fn vram_wont_fit_outranks_load() {
let set = ranked_set();
let lookup = load_lookup_for(vec![
(BackendId::Cuda, Some(1), Some(10_000), Some(0), 1),
(BackendId::Cpu, Some(8_000), Some(10_000), Some(4), 1),
]);
let chained = ChainedSelector::with_default_estimator(None, Some(lookup));
assert_eq!(
chained.select(&set).expect("non-empty").backend,
BackendId::Cpu,
"WontFit CUDA skipped; saturated-but-fitting CPU taken — VRAM \
outranks load",
);
}
#[test]
fn tight_idle_still_loses_to_comfortable_saturated() {
let set = ranked_set();
let lookup = load_lookup_for(vec![
(BackendId::Cuda, Some(100), Some(100), Some(0), 1),
(BackendId::Cpu, Some(10_000), Some(10_000), Some(4), 1),
]);
let chained = ChainedSelector::with_default_estimator(None, Some(lookup));
assert_eq!(
chained.select(&set).expect("non-empty").backend,
BackendId::Cpu,
"Tight+idle CUDA stays demoted below Comfortable+saturated CPU; \
VRAM tier compared before load tier",
);
}
#[test]
fn no_load_preserves_static_winner() {
let set = ranked_set();
let lookup = load_lookup_for(vec![
(BackendId::Cuda, Some(10_000), Some(10_000), Some(0), 1),
(BackendId::Cpu, Some(10_000), Some(10_000), Some(0), 1),
]);
let chained = ChainedSelector::with_default_estimator(None, Some(lookup));
assert_eq!(
chained.select(&set).expect("non-empty").backend,
BackendId::Cuda,
"no load anywhere ⇒ load leg is constant 0 ⇒ static winner stands",
);
}
#[test]
fn none_count_is_no_signal_not_load() {
let set = ranked_set();
let lookup = load_lookup_for(vec![
(BackendId::Cuda, Some(10_000), Some(10_000), None, 1),
(BackendId::Cpu, Some(10_000), Some(10_000), None, 1),
]);
let chained = ChainedSelector::with_default_estimator(None, Some(lookup));
assert_eq!(
chained.select(&set).expect("non-empty").backend,
BackendId::Cuda,
"None pending_work_count ⇒ tier 0 (no signal) ⇒ static winner",
);
}
#[test]
fn no_signals_matches_winner_selector_exactly() {
let set = ranked_set();
let chained = ChainedSelector::with_default_estimator(None, None);
let baseline = WinnerSelector.select(&set).expect("non-empty");
let pick = chained.select(&set).expect("non-empty");
assert_eq!(pick.backend, baseline.backend);
assert_eq!(pick.backend, BackendId::Cuda, "static winner preserved");
}
#[test]
fn empty_judge_and_comfortable_pressure_keeps_winner() {
let set = ranked_set();
let judge: Arc<dyn JudgeOracle> = Arc::new(HashMapJudge::new());
let lookup = lookup_for(vec![
(BackendId::Cuda, Some(1_000_000), Some(2_000_000)),
(BackendId::Cpu, Some(1_000_000), Some(2_000_000)),
]);
let chained = ChainedSelector::with_default_estimator(Some(judge), Some(lookup));
let pick = chained.select(&set).expect("non-empty");
assert_eq!(pick.backend, BackendId::Cuda);
}
#[test]
fn guard_skips_wont_fit_winner() {
let set = ranked_set();
let lookup = lookup_for(vec![
(BackendId::Cuda, Some(50), Some(10_000)),
(BackendId::Cpu, Some(8_000), Some(10_000)),
]);
let chained = ChainedSelector::with_default_estimator(None, Some(lookup));
let pick = chained.select(&set).expect("non-empty");
assert_eq!(pick.backend, BackendId::Cpu, "WontFit winner skipped");
}
#[test]
fn tight_demoted_below_comfortable() {
let set = ranked_set();
let lookup = lookup_for(vec![
(BackendId::Cuda, Some(100), Some(100)),
(BackendId::Cpu, Some(10_000), Some(10_000)),
]);
let chained = ChainedSelector::with_default_estimator(None, Some(lookup));
let pick = chained.select(&set).expect("non-empty");
assert_eq!(pick.backend, BackendId::Cpu, "Tight loses to Comfortable");
}
#[test]
fn unknown_ties_with_comfortable_preserves_winner() {
let set = ranked_set();
let lookup = lookup_for(vec![(BackendId::Cpu, Some(10_000), Some(10_000))]);
let chained = ChainedSelector::with_default_estimator(None, Some(lookup));
let pick = chained.select(&set).expect("non-empty");
assert_eq!(
pick.backend,
BackendId::Cuda,
"Unknown is no-signal, not pressure",
);
}
#[test]
fn all_wont_fit_falls_back_to_winner() {
let set = ranked_set();
let lookup = lookup_for(vec![
(BackendId::Cuda, Some(1), Some(10_000)),
(BackendId::Cpu, Some(1), Some(10_000)),
]);
let chained = ChainedSelector::with_default_estimator(None, Some(lookup));
let pick = chained.select(&set).expect("non-empty per contract");
assert_eq!(pick.backend, BackendId::Cuda);
}
#[test]
fn judge_measurement_flips_winner_both_directions() {
let c = ctx();
let mut judge = HashMapJudge::new();
judge.insert(
c.op,
c.principal_dtype,
c.size_class,
BackendId::Cpu,
"",
50,
0,
);
let chained = ChainedSelector::with_default_estimator(
Some(Arc::new(judge) as Arc<dyn JudgeOracle>),
None,
);
let set = ranked_set();
assert_eq!(
chained.select(&set).expect("non-empty").backend,
BackendId::Cpu,
"fast measurement beats slower unmeasured composite",
);
let mut judge = HashMapJudge::new();
judge.insert(
c.op,
c.principal_dtype,
c.size_class,
BackendId::Cpu,
"",
5_000,
0,
);
let chained = ChainedSelector::with_default_estimator(
Some(Arc::new(judge) as Arc<dyn JudgeOracle>),
None,
);
let set = ranked_set();
assert_eq!(
chained.select(&set).expect("non-empty").backend,
BackendId::Cuda,
"slow measurement does not trump a faster unmeasured candidate",
);
}
#[test]
fn guard_overrides_judge_preference() {
let c = ctx();
let mut judge = HashMapJudge::new();
judge.insert(
c.op,
c.principal_dtype,
c.size_class,
BackendId::Cuda,
"",
1,
0,
);
let lookup = lookup_for(vec![
(BackendId::Cuda, Some(50), Some(10_000)),
(BackendId::Cpu, Some(8_000), Some(10_000)),
]);
let chained = ChainedSelector::with_default_estimator(
Some(Arc::new(judge) as Arc<dyn JudgeOracle>),
Some(lookup),
);
let set = ranked_set();
assert_eq!(
chained.select(&set).expect("non-empty").backend,
BackendId::Cpu,
);
}
#[test]
fn judge_ranks_within_pressure_tier() {
let c = ctx();
let mut set = AlternativeSet::empty();
set.push(make_candidate(
BackendId::Cuda,
DeviceLocation::Cuda { gpu_id: 0 },
100,
));
set.push(make_candidate(BackendId::Cpu, DeviceLocation::Cpu, 200));
set.push(make_candidate(
BackendId::Vulkan,
DeviceLocation::Vulkan { gpu_id: 0 },
300,
));
set.set_context(c);
let lookup = lookup_for(vec![
(BackendId::Cuda, Some(100), Some(100)),
(BackendId::Cpu, Some(10_000), Some(10_000)),
(BackendId::Vulkan, Some(10_000), Some(10_000)),
]);
let mut judge = HashMapJudge::new();
judge.insert(
c.op,
c.principal_dtype,
c.size_class,
BackendId::Cuda,
"",
1,
0,
);
judge.insert(
c.op,
c.principal_dtype,
c.size_class,
BackendId::Cpu,
"",
90,
0,
);
judge.insert(
c.op,
c.principal_dtype,
c.size_class,
BackendId::Vulkan,
"",
40,
0,
);
let chained = ChainedSelector::with_default_estimator(
Some(Arc::new(judge) as Arc<dyn JudgeOracle>),
Some(lookup),
);
assert_eq!(
chained.select(&set).expect("non-empty").backend,
BackendId::Vulkan,
"judge re-ranks the Comfortable tier; Tight stays demoted",
);
}
#[test]
fn kernel_source_disambiguates_cpu_siblings() {
let c = ctx();
let mut set = AlternativeSet::empty();
let mut aocl = make_candidate(BackendId::Cpu, DeviceLocation::Cpu, 500);
aocl.kernel_source = "aocl";
let mut mkl = make_candidate(BackendId::Cpu, DeviceLocation::Cpu, 500);
mkl.kernel_source = "mkl";
set.push(aocl);
set.push(mkl);
set.set_context(c);
let mut judge = HashMapJudge::new();
judge.insert(
c.op,
c.principal_dtype,
c.size_class,
BackendId::Cpu,
"aocl",
9_000,
0,
);
judge.insert(
c.op,
c.principal_dtype,
c.size_class,
BackendId::Cpu,
"mkl",
1_000,
0,
);
let chained = ChainedSelector::with_default_estimator(
Some(Arc::new(judge) as Arc<dyn JudgeOracle>),
None,
);
assert_eq!(
chained.select(&set).expect("non-empty").kernel_source,
"mkl",
);
}
#[test]
fn missing_context_disables_judge_leg() {
let c = ctx();
let mut set = AlternativeSet::empty();
set.push(make_candidate(
BackendId::Cuda,
DeviceLocation::Cuda { gpu_id: 0 },
100,
));
set.push(make_candidate(BackendId::Cpu, DeviceLocation::Cpu, 200));
let mut judge = HashMapJudge::new();
judge.insert(
c.op,
c.principal_dtype,
c.size_class,
BackendId::Cpu,
"",
1,
0,
);
let chained = ChainedSelector::with_default_estimator(
Some(Arc::new(judge) as Arc<dyn JudgeOracle>),
None,
);
assert_eq!(
chained.select(&set).expect("non-empty").backend,
BackendId::Cuda,
"no context → no judge key → static winner",
);
}
#[test]
fn empty_set_returns_none() {
let set = AlternativeSet::empty();
let chained = ChainedSelector::with_default_estimator(None, None);
assert!(chained.select(&set).is_none());
}
#[test]
fn debug_does_not_panic() {
let chained = ChainedSelector::with_default_estimator(
Some(Arc::new(HashMapJudge::new()) as Arc<dyn JudgeOracle>),
Some(Arc::new(|_, _| None)),
);
let s = format!("{chained:?}");
assert!(s.contains("ChainedSelector"));
}
}