use fuel_ir::backend::BackendCapabilities;
use fuel_ir::dispatch::{OpKind, SizeClass};
use fuel_ir::probe::BackendId;
use fuel_ir::{DType, DeviceLocation, Shape};
use super::alternative_set::AlternativeSet;
use super::judge::JudgeOracle;
use crate::fused::CostEstimate;
use crate::kernel::KernelBindingTable;
#[cfg(test)]
use crate::kernel::OpParams;
pub trait TransferEstimator: Send + Sync {
fn estimate_transfer_ns(&self, src: DeviceLocation, dst: DeviceLocation, bytes: u64) -> u64;
}
pub fn apply_inbound_transfer_costs(
set: &mut AlternativeSet,
inputs: &[(DeviceLocation, u64)],
estimator: &dyn TransferEstimator,
) {
for i in 0..set.len() {
let dst = set.alternatives()[i].device;
let mut total: u64 = 0;
for &(src, bytes) in inputs {
total = total.saturating_add(estimator.estimate_transfer_ns(src, dst, bytes));
}
set.set_inbound_transfer_ns(i, total);
}
}
pub const CPU_COMPUTE_FLOPS_PER_NS: f64 = 1.0;
pub const CPU_MEM_BYTES_PER_NS: f64 = 4.0;
pub const GPU_COMPUTE_FLOPS_PER_NS: f64 = 30.0;
pub const GPU_MEM_BYTES_PER_NS: f64 = 40.0;
pub fn default_backend_rates(backend: BackendId) -> (f64, f64) {
match backend {
BackendId::Cpu => (CPU_COMPUTE_FLOPS_PER_NS, CPU_MEM_BYTES_PER_NS),
_ => (GPU_COMPUTE_FLOPS_PER_NS, GPU_MEM_BYTES_PER_NS),
}
}
pub fn composite_ns(
cost: &CostEstimate,
compute_rate_flops_per_ns: f64,
mem_bandwidth_bytes_per_ns: f64,
) -> u64 {
let compute_rate = if compute_rate_flops_per_ns.is_finite() && compute_rate_flops_per_ns > 0.0 {
compute_rate_flops_per_ns
} else {
CPU_COMPUTE_FLOPS_PER_NS
};
let bandwidth = if mem_bandwidth_bytes_per_ns.is_finite() && mem_bandwidth_bytes_per_ns > 0.0 {
mem_bandwidth_bytes_per_ns
} else {
CPU_MEM_BYTES_PER_NS
};
let compute_ns = (cost.flops as f64 / compute_rate) as u64;
let memory_ns = (cost.bytes_moved as f64 / bandwidth) as u64;
let parallel_ns = compute_ns.max(memory_ns);
parallel_ns.saturating_add(cost.kernel_overhead_ns as u64)
}
pub type CapabilitiesLookup<'a> = dyn Fn(BackendId) -> Option<&'a BackendCapabilities> + 'a;
pub fn compute_static_costs(
set: &mut AlternativeSet,
op_kind: OpKind,
dtypes: &[DType],
shapes: &[Shape],
bindings: &KernelBindingTable,
capabilities_for: &CapabilitiesLookup<'_>,
judge: Option<&dyn JudgeOracle>,
) {
for i in 0..set.len() {
let (kernel_ptr, backend, op_params) = {
let c = &set.alternatives()[i];
(
c.kernel as *const () as usize,
c.backend,
c.op_params.clone(),
)
};
let entries = bindings.lookup_alternatives(op_kind, dtypes, backend);
let Some(entry) = entries
.iter()
.find(|e| (e.kernel as *const () as usize) == kernel_ptr)
else {
continue;
};
let Some(caps) = capabilities_for(backend) else {
continue;
};
let cost = match entry.cost_expr {
Some(expr) => crate::fkc::cost_estimate(expr, op_kind, shapes, dtypes, &op_params)
.unwrap_or_else(|_| (entry.cost)(shapes, dtypes, &op_params, caps)),
None => (entry.cost)(shapes, dtypes, &op_params, caps),
};
set.set_static_cost(i, cost);
}
let Some(judge) = judge else {
return;
};
let principal_dtype = match dtypes.first() {
Some(&dt) => dt,
None => return,
};
let size_class = SizeClass::for_op(op_kind, shapes);
for i in 0..set.len() {
let backend = set.alternatives()[i].backend;
let kernel_source = set.alternatives()[i].kernel_source;
let kernel_revision_hash = set.alternatives()[i].kernel_revision_hash;
let Some(latency_ns) = judge.measured_latency_ns(
op_kind,
principal_dtype,
size_class,
backend,
kernel_source,
kernel_revision_hash,
) else {
continue;
};
let overhead = latency_ns.min(u32::MAX as u64) as u32;
set.set_static_cost(
i,
CostEstimate {
flops: 0,
bytes_moved: 0,
kernel_overhead_ns: overhead,
},
);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::fused::PrecisionGuarantee;
use crate::kernel::{KernelBindingTable, KernelCaps, unknown_cost};
use crate::ranker::alternative_set::AlternativeSet;
use crate::ranker::candidate::Candidate;
use fuel_ir::backend::{BackendCapabilities, SubstrateClass, TransferPath};
use fuel_ir::dispatch::OpKind;
use fuel_ir::probe::BackendId;
use fuel_ir::{DType, DeviceLocation, Layout, Result, Shape};
use fuel_memory::Storage;
use std::collections::{HashMap, HashSet};
use std::sync::{Arc, RwLock};
fn noop_a(
_i: &[Arc<RwLock<Storage>>],
_o: &mut [Arc<RwLock<Storage>>],
_l: &[Layout],
_p: &OpParams,
) -> Result<()> {
Ok(())
}
fn noop_b(
_i: &[Arc<RwLock<Storage>>],
_o: &mut [Arc<RwLock<Storage>>],
_l: &[Layout],
_p: &OpParams,
) -> Result<()> {
Ok(())
}
fn caps_for_test(backend_id: BackendId) -> BackendCapabilities {
BackendCapabilities {
backend_id,
device_location: DeviceLocation::Cpu,
op_dtype_support: HashSet::new(),
required_alignment: 64,
access_granularity_bits: 8,
transfer_paths: vec![(DeviceLocation::Cpu, TransferPath::SameDevice)],
storage_substrate: SubstrateClass::HostBytes,
compute_throughput_flops_per_ns: default_backend_rates(backend_id).0,
mem_bandwidth_bytes_per_ns: default_backend_rates(backend_id).1,
}
}
fn candidate_with_cost(kernel: crate::kernel::KernelRef, cost: CostEstimate) -> Candidate {
Candidate {
kernel,
caps: KernelCaps::empty(),
backend: BackendId::Cpu,
device: DeviceLocation::Cpu,
precision: PrecisionGuarantee::PRIMITIVE_DETERMINISTIC_CPU,
static_cost: cost,
inbound_transfer_ns: 0,
op_params: OpParams::None,
coupling: Vec::new(),
kernel_source: "",
kernel_revision_hash: 0,
}
}
struct FlatEstimator {
ns_per_byte: u64,
latency_ns: u64,
}
impl TransferEstimator for FlatEstimator {
fn estimate_transfer_ns(
&self,
src: DeviceLocation,
dst: DeviceLocation,
bytes: u64,
) -> u64 {
if src == dst {
return 0;
}
self.latency_ns
.saturating_add(bytes.saturating_mul(self.ns_per_byte))
}
}
#[test]
fn composite_ns_zero_cost_is_zero() {
assert_eq!(
composite_ns(
&CostEstimate::default(),
CPU_COMPUTE_FLOPS_PER_NS,
CPU_MEM_BYTES_PER_NS
),
0
);
}
#[test]
fn composite_ns_flops_dominant() {
let c = CostEstimate {
flops: 1000,
bytes_moved: 0,
kernel_overhead_ns: 0,
};
assert_eq!(
composite_ns(&c, CPU_COMPUTE_FLOPS_PER_NS, CPU_MEM_BYTES_PER_NS),
1000
);
}
#[test]
fn composite_ns_memory_dominant() {
let c = CostEstimate {
flops: 0,
bytes_moved: 4000,
kernel_overhead_ns: 0,
};
assert_eq!(
composite_ns(&c, CPU_COMPUTE_FLOPS_PER_NS, CPU_MEM_BYTES_PER_NS),
1000
);
}
#[test]
fn composite_ns_takes_max_of_compute_and_memory() {
let c = CostEstimate {
flops: 500,
bytes_moved: 4000,
kernel_overhead_ns: 0,
};
assert_eq!(
composite_ns(&c, CPU_COMPUTE_FLOPS_PER_NS, CPU_MEM_BYTES_PER_NS),
1000
);
}
#[test]
fn composite_ns_overhead_serial_after_parallel() {
let c = CostEstimate {
flops: 500,
bytes_moved: 3200,
kernel_overhead_ns: 200,
};
assert_eq!(
composite_ns(&c, CPU_COMPUTE_FLOPS_PER_NS, CPU_MEM_BYTES_PER_NS),
1000
);
}
#[test]
fn composite_ns_saturates_at_u64_max() {
let c = CostEstimate {
flops: u64::MAX,
bytes_moved: u64::MAX,
kernel_overhead_ns: u32::MAX,
};
assert_eq!(
composite_ns(&c, CPU_COMPUTE_FLOPS_PER_NS, CPU_MEM_BYTES_PER_NS),
u64::MAX
);
}
#[test]
fn rank_by_composite_cost_orders_ascending() {
let mut set = AlternativeSet::from_candidates(vec![
candidate_with_cost(
noop_a,
CostEstimate {
flops: 300,
bytes_moved: 0,
kernel_overhead_ns: 0,
},
),
candidate_with_cost(
noop_b,
CostEstimate {
flops: 100,
bytes_moved: 0,
kernel_overhead_ns: 0,
},
),
candidate_with_cost(
noop_a,
CostEstimate {
flops: 200,
bytes_moved: 0,
kernel_overhead_ns: 0,
},
),
]);
set.rank_by_composite_cost();
let costs: Vec<u64> = set
.alternatives()
.iter()
.map(|c| c.static_cost.flops)
.collect();
assert_eq!(costs, vec![100, 200, 300]);
}
#[test]
fn rank_then_retain_keeps_undominated_only() {
let mut set = AlternativeSet::from_candidates(
(0..5)
.map(|i| {
candidate_with_cost(
noop_a,
CostEstimate {
flops: (5 - i) * 100,
bytes_moved: 0,
kernel_overhead_ns: 0,
},
)
})
.collect(),
);
set.rank_by_composite_cost();
set.retain_per_device_frontier(crate::ranker::KEEP_PER_DEVICE);
assert_eq!(
set.len(),
1,
"all but the time-best are dominated on one device"
);
let costs: Vec<u64> = set
.alternatives()
.iter()
.map(|c| c.static_cost.flops)
.collect();
assert_eq!(costs, vec![100], "the fastest (arm-0 winner) survives");
}
fn make_cost_fn(flops: u64, bytes: u64, overhead: u32) -> crate::kernel::CostFn {
let _ = (flops, bytes, overhead);
|_, _, _, _| CostEstimate {
flops: 1000,
bytes_moved: 0,
kernel_overhead_ns: 0,
}
}
#[test]
fn compute_static_costs_populates_via_binding_lookup() {
fn cost_a(_: &[Shape], _: &[DType], _: &OpParams, _: &BackendCapabilities) -> CostEstimate {
CostEstimate {
flops: 500,
bytes_moved: 0,
kernel_overhead_ns: 0,
}
}
let mut bindings = KernelBindingTable::new();
let dtypes = [DType::F32, DType::F32, DType::F32];
bindings.register_full(
OpKind::AddElementwise,
&dtypes,
BackendId::Cpu,
noop_a,
KernelCaps::empty(),
PrecisionGuarantee::PRIMITIVE_DETERMINISTIC_CPU,
cost_a,
);
let mut set = AlternativeSet::from_candidates(vec![candidate_with_cost(
noop_a,
CostEstimate::default(),
)]);
let cpu_caps = caps_for_test(BackendId::Cpu);
let lookup: HashMap<BackendId, BackendCapabilities> =
[(BackendId::Cpu, cpu_caps)].into_iter().collect();
let lookup_fn = |b: BackendId| lookup.get(&b);
compute_static_costs(
&mut set,
OpKind::AddElementwise,
&dtypes,
&[Shape::from(vec![4])],
&bindings,
&lookup_fn,
None,
);
assert_eq!(set.alternatives()[0].static_cost.flops, 500);
let _ = make_cost_fn; }
#[test]
fn compute_static_costs_prefers_declared_cost_expr() {
let expr = crate::fkc::cost_compile::intern_cost_expr(
&crate::fkc::cost_expr::compile_field(Some("2 * n")).unwrap(),
)
.expect("expr interns");
let mut bindings = KernelBindingTable::new();
let dtypes = [DType::F32, DType::F32, DType::F32];
bindings.register_full_with_source_generic(
OpKind::AddElementwise,
&dtypes,
BackendId::Cpu,
noop_a,
KernelCaps::empty(),
PrecisionGuarantee::PRIMITIVE_DETERMINISTIC_CPU,
unknown_cost,
"",
false,
0,
Some(expr),
);
let mut set = AlternativeSet::from_candidates(vec![candidate_with_cost(
noop_a,
CostEstimate::default(),
)]);
let cpu_caps = caps_for_test(BackendId::Cpu);
let lookup: HashMap<BackendId, BackendCapabilities> =
[(BackendId::Cpu, cpu_caps)].into_iter().collect();
compute_static_costs(
&mut set,
OpKind::AddElementwise,
&dtypes,
&[Shape::from(vec![3])],
&bindings,
&|b| lookup.get(&b),
None,
);
assert_eq!(
set.alternatives()[0].static_cost.flops,
6,
"2 * n with n = elem_count([3]) = 3",
);
}
fn sentinel_cost_42(
_s: &[Shape],
_d: &[DType],
_p: &OpParams,
_c: &BackendCapabilities,
) -> CostEstimate {
CostEstimate {
flops: 42,
..Default::default()
}
}
#[test]
fn compute_static_costs_degrades_to_fn_pointer_on_cost_expr_eval_error() {
let expr = crate::fkc::cost_compile::intern_cost_expr(
&crate::fkc::cost_expr::compile_field(Some("undefined_symbol_xyz")).unwrap(),
)
.expect("expr interns (a bare Sym node is never CompiledCostExpr::Unknown)");
let mut bindings = KernelBindingTable::new();
let dtypes = [DType::F32, DType::F32, DType::F32];
bindings.register_full_with_source_generic(
OpKind::AddElementwise,
&dtypes,
BackendId::Cpu,
noop_a,
KernelCaps::empty(),
PrecisionGuarantee::PRIMITIVE_DETERMINISTIC_CPU,
sentinel_cost_42,
"",
false,
0,
Some(expr),
);
let mut set = AlternativeSet::from_candidates(vec![candidate_with_cost(
noop_a,
CostEstimate::default(),
)]);
let cpu_caps = caps_for_test(BackendId::Cpu);
let lookup: HashMap<BackendId, BackendCapabilities> =
[(BackendId::Cpu, cpu_caps)].into_iter().collect();
compute_static_costs(
&mut set,
OpKind::AddElementwise,
&dtypes,
&[Shape::from(vec![3])],
&bindings,
&|b| lookup.get(&b),
None,
);
assert_eq!(
set.alternatives()[0].static_cost.flops,
42,
"undefined symbol => cost_estimate errors => .unwrap_or_else falls back \
to sentinel_cost_42, never panics",
);
}
#[test]
fn compute_static_costs_leaves_default_when_no_capabilities() {
let mut bindings = KernelBindingTable::new();
let dtypes = [DType::F32, DType::F32, DType::F32];
bindings.register_full(
OpKind::AddElementwise,
&dtypes,
BackendId::Cpu,
noop_a,
KernelCaps::empty(),
PrecisionGuarantee::PRIMITIVE_DETERMINISTIC_CPU,
unknown_cost,
);
let mut set = AlternativeSet::from_candidates(vec![candidate_with_cost(
noop_a,
CostEstimate::default(),
)]);
let empty_lookup = |_: BackendId| -> Option<&BackendCapabilities> { None };
compute_static_costs(
&mut set,
OpKind::AddElementwise,
&dtypes,
&[Shape::from(vec![4])],
&bindings,
&empty_lookup,
None,
);
assert_eq!(set.alternatives()[0].static_cost, CostEstimate::default());
}
#[test]
fn compute_static_costs_skips_candidates_without_matching_binding() {
let mut bindings = KernelBindingTable::new();
let dtypes = [DType::F32, DType::F32, DType::F32];
bindings.register_full(
OpKind::AddElementwise,
&dtypes,
BackendId::Cpu,
noop_a,
KernelCaps::empty(),
PrecisionGuarantee::PRIMITIVE_DETERMINISTIC_CPU,
unknown_cost,
);
let mut set = AlternativeSet::from_candidates(
vec![candidate_with_cost(noop_b, CostEstimate::default())],
);
let cpu_caps = caps_for_test(BackendId::Cpu);
let lookup: HashMap<BackendId, BackendCapabilities> =
[(BackendId::Cpu, cpu_caps)].into_iter().collect();
compute_static_costs(
&mut set,
OpKind::AddElementwise,
&dtypes,
&[Shape::from(vec![4])],
&bindings,
&|b| lookup.get(&b),
None,
);
assert_eq!(set.alternatives()[0].static_cost, CostEstimate::default());
}
#[test]
fn judge_refinement_replaces_layer1_with_measured_latency() {
use crate::ranker::judge::HashMapJudge;
fn layer1(_: &[Shape], _: &[DType], _: &OpParams, _: &BackendCapabilities) -> CostEstimate {
CostEstimate {
flops: 1000,
bytes_moved: 0,
kernel_overhead_ns: 0,
}
}
let mut bindings = KernelBindingTable::new();
let dtypes = [DType::F32, DType::F32, DType::F32];
bindings.register_full(
OpKind::AddElementwise,
&dtypes,
BackendId::Cpu,
noop_a,
KernelCaps::empty(),
PrecisionGuarantee::PRIMITIVE_DETERMINISTIC_CPU,
layer1,
);
let mut set = AlternativeSet::from_candidates(vec![candidate_with_cost(
noop_a,
CostEstimate::default(),
)]);
let cpu_caps = caps_for_test(BackendId::Cpu);
let lookup: HashMap<BackendId, BackendCapabilities> =
[(BackendId::Cpu, cpu_caps)].into_iter().collect();
let shapes = [Shape::from(vec![4])];
let sc = SizeClass::from_elem_count(4);
let mut judge = HashMapJudge::new();
judge.insert(
OpKind::AddElementwise,
DType::F32,
sc,
BackendId::Cpu,
"",
250,
0,
);
compute_static_costs(
&mut set,
OpKind::AddElementwise,
&dtypes,
&shapes,
&bindings,
&|b| lookup.get(&b),
Some(&judge),
);
let c = &set.alternatives()[0];
assert_eq!(c.static_cost.flops, 0, "Layer-2 zeroes FLOPs");
assert_eq!(c.static_cost.bytes_moved, 0, "Layer-2 zeroes bytes");
assert_eq!(
c.static_cost.kernel_overhead_ns, 250,
"Layer-2 stamps latency"
);
let (cr, bw) = default_backend_rates(BackendId::Cuda);
assert_eq!(
composite_ns(&c.static_cost, cr, bw),
250,
"composite returns measurement"
);
}
#[test]
fn judge_missing_measurement_leaves_layer1_intact() {
fn layer1(_: &[Shape], _: &[DType], _: &OpParams, _: &BackendCapabilities) -> CostEstimate {
CostEstimate {
flops: 1000,
bytes_moved: 4000,
kernel_overhead_ns: 50,
}
}
let mut bindings = KernelBindingTable::new();
let dtypes = [DType::F32, DType::F32, DType::F32];
bindings.register_full(
OpKind::AddElementwise,
&dtypes,
BackendId::Cpu,
noop_a,
KernelCaps::empty(),
PrecisionGuarantee::PRIMITIVE_DETERMINISTIC_CPU,
layer1,
);
let mut set = AlternativeSet::from_candidates(vec![candidate_with_cost(
noop_a,
CostEstimate::default(),
)]);
let cpu_caps = caps_for_test(BackendId::Cpu);
let lookup: HashMap<BackendId, BackendCapabilities> =
[(BackendId::Cpu, cpu_caps)].into_iter().collect();
let empty_judge = crate::ranker::judge::HashMapJudge::new();
compute_static_costs(
&mut set,
OpKind::AddElementwise,
&dtypes,
&[Shape::from(vec![4])],
&bindings,
&|b| lookup.get(&b),
Some(&empty_judge),
);
let c = &set.alternatives()[0];
assert_eq!(c.static_cost.flops, 1000, "Layer-1 FLOPs survive");
assert_eq!(c.static_cost.bytes_moved, 4000);
assert_eq!(c.static_cost.kernel_overhead_ns, 50);
}
#[test]
fn judge_refinement_per_backend_can_flip_winner() {
fn cpu_layer1(
_: &[Shape],
_: &[DType],
_: &OpParams,
_: &BackendCapabilities,
) -> CostEstimate {
CostEstimate {
flops: 100,
bytes_moved: 0,
kernel_overhead_ns: 0,
}
}
fn aocl_layer1(
_: &[Shape],
_: &[DType],
_: &OpParams,
_: &BackendCapabilities,
) -> CostEstimate {
CostEstimate {
flops: 1000,
bytes_moved: 0,
kernel_overhead_ns: 0,
}
}
let mut bindings = KernelBindingTable::new();
let dtypes = [DType::F32, DType::F32, DType::F32];
bindings.register_full(
OpKind::AddElementwise,
&dtypes,
BackendId::Cpu,
noop_a,
KernelCaps::empty(),
PrecisionGuarantee::PRIMITIVE_DETERMINISTIC_CPU,
cpu_layer1,
);
bindings.register_full(
OpKind::AddElementwise,
&dtypes,
BackendId::Cuda,
noop_b,
KernelCaps::empty(),
PrecisionGuarantee::PRIMITIVE_DETERMINISTIC_CPU,
aocl_layer1,
);
let mut set = AlternativeSet::from_candidates(vec![
Candidate {
backend: BackendId::Cpu,
..candidate_with_cost(noop_a, CostEstimate::default())
},
Candidate {
backend: BackendId::Cuda,
..candidate_with_cost(noop_b, CostEstimate::default())
},
]);
let lookup: HashMap<BackendId, BackendCapabilities> = [
(BackendId::Cpu, caps_for_test(BackendId::Cpu)),
(BackendId::Cuda, caps_for_test(BackendId::Cuda)),
]
.into_iter()
.collect();
let sc = SizeClass::from_elem_count(4);
let mut judge = crate::ranker::judge::HashMapJudge::new();
judge.insert(
OpKind::AddElementwise,
DType::F32,
sc,
BackendId::Cpu,
"",
500,
0,
);
judge.insert(
OpKind::AddElementwise,
DType::F32,
sc,
BackendId::Cuda,
"",
100,
0,
);
compute_static_costs(
&mut set,
OpKind::AddElementwise,
&dtypes,
&[Shape::from(vec![4])],
&bindings,
&|b| lookup.get(&b),
Some(&judge),
);
set.rank_by_composite_cost();
assert_eq!(
set.winner().unwrap().backend,
BackendId::Cuda,
"Layer-2 measurement reverses Layer-1 verdict",
);
}
#[test]
fn judge_partial_coverage_mixes_layer1_and_layer2() {
fn cheap(_: &[Shape], _: &[DType], _: &OpParams, _: &BackendCapabilities) -> CostEstimate {
CostEstimate {
flops: 50,
bytes_moved: 0,
kernel_overhead_ns: 0,
}
}
fn expensive(
_: &[Shape],
_: &[DType],
_: &OpParams,
_: &BackendCapabilities,
) -> CostEstimate {
CostEstimate {
flops: 10_000,
bytes_moved: 0,
kernel_overhead_ns: 0,
}
}
let mut bindings = KernelBindingTable::new();
let dtypes = [DType::F32, DType::F32, DType::F32];
bindings.register_full(
OpKind::AddElementwise,
&dtypes,
BackendId::Cpu,
noop_a,
KernelCaps::empty(),
PrecisionGuarantee::PRIMITIVE_DETERMINISTIC_CPU,
cheap,
);
bindings.register_full(
OpKind::AddElementwise,
&dtypes,
BackendId::Cuda,
noop_b,
KernelCaps::empty(),
PrecisionGuarantee::PRIMITIVE_DETERMINISTIC_CPU,
expensive,
);
let mut set = AlternativeSet::from_candidates(vec![
Candidate {
backend: BackendId::Cpu,
..candidate_with_cost(noop_a, CostEstimate::default())
},
Candidate {
backend: BackendId::Cuda,
..candidate_with_cost(noop_b, CostEstimate::default())
},
]);
let lookup: HashMap<BackendId, BackendCapabilities> = [
(BackendId::Cpu, caps_for_test(BackendId::Cpu)),
(BackendId::Cuda, caps_for_test(BackendId::Cuda)),
]
.into_iter()
.collect();
let sc = SizeClass::from_elem_count(4);
let mut judge = crate::ranker::judge::HashMapJudge::new();
judge.insert(
OpKind::AddElementwise,
DType::F32,
sc,
BackendId::Cuda,
"",
20,
0,
);
compute_static_costs(
&mut set,
OpKind::AddElementwise,
&dtypes,
&[Shape::from(vec![4])],
&bindings,
&|b| lookup.get(&b),
Some(&judge),
);
set.rank_by_composite_cost();
assert_eq!(
set.winner().unwrap().backend,
BackendId::Cuda,
"partial Judge coverage still influences ranking",
);
}
#[test]
fn judge_saturates_above_u32_max_ns() {
fn layer1(_: &[Shape], _: &[DType], _: &OpParams, _: &BackendCapabilities) -> CostEstimate {
CostEstimate {
flops: 1,
bytes_moved: 0,
kernel_overhead_ns: 0,
}
}
let mut bindings = KernelBindingTable::new();
let dtypes = [DType::F32, DType::F32, DType::F32];
bindings.register_full(
OpKind::AddElementwise,
&dtypes,
BackendId::Cpu,
noop_a,
KernelCaps::empty(),
PrecisionGuarantee::PRIMITIVE_DETERMINISTIC_CPU,
layer1,
);
let mut set = AlternativeSet::from_candidates(vec![candidate_with_cost(
noop_a,
CostEstimate::default(),
)]);
let lookup: HashMap<BackendId, BackendCapabilities> =
[(BackendId::Cpu, caps_for_test(BackendId::Cpu))]
.into_iter()
.collect();
let mut judge = crate::ranker::judge::HashMapJudge::new();
judge.insert(
OpKind::AddElementwise,
DType::F32,
SizeClass::from_elem_count(4),
BackendId::Cpu,
"",
u64::MAX,
0,
);
compute_static_costs(
&mut set,
OpKind::AddElementwise,
&dtypes,
&[Shape::from(vec![4])],
&bindings,
&|b| lookup.get(&b),
Some(&judge),
);
assert_eq!(
set.alternatives()[0].static_cost.kernel_overhead_ns,
u32::MAX,
"u64 → u32 saturating cast pins at u32::MAX",
);
}
#[test]
fn inbound_transfer_sums_over_offdevice_inputs_only() {
let cuda0 = DeviceLocation::Cuda { gpu_id: 0 };
let mut set = AlternativeSet::from_candidates(vec![
candidate_with_cost(noop_a, CostEstimate::default()),
Candidate {
backend: BackendId::Cuda,
device: cuda0,
..candidate_with_cost(noop_b, CostEstimate::default())
},
]);
let est = FlatEstimator {
ns_per_byte: 1,
latency_ns: 100,
};
let inputs = [(DeviceLocation::Cpu, 12_u64), (DeviceLocation::Cpu, 8_u64)];
apply_inbound_transfer_costs(&mut set, &inputs, &est);
assert_eq!(
set.alternatives()[0].inbound_transfer_ns,
0,
"co-resident inputs price zero",
);
assert_eq!(
set.alternatives()[1].inbound_transfer_ns,
(100 + 12) + (100 + 8),
"off-device candidate pays latency + bytes per input",
);
}
#[test]
fn inbound_transfer_empty_inputs_prices_zero() {
let mut set = AlternativeSet::from_candidates(vec![candidate_with_cost(
noop_a,
CostEstimate::default(),
)]);
let est = FlatEstimator {
ns_per_byte: 1_000_000,
latency_ns: u64::MAX,
};
apply_inbound_transfer_costs(&mut set, &[], &est);
assert_eq!(set.alternatives()[0].inbound_transfer_ns, 0);
}
#[test]
fn inbound_transfer_saturates() {
let cuda0 = DeviceLocation::Cuda { gpu_id: 0 };
let mut set = AlternativeSet::from_candidates(vec![Candidate {
backend: BackendId::Cuda,
device: cuda0,
..candidate_with_cost(noop_a, CostEstimate::default())
}]);
let est = FlatEstimator {
ns_per_byte: 0,
latency_ns: u64::MAX,
};
let inputs = [(DeviceLocation::Cpu, 1_u64), (DeviceLocation::Cpu, 1_u64)];
apply_inbound_transfer_costs(&mut set, &inputs, &est);
assert_eq!(set.alternatives()[0].inbound_transfer_ns, u64::MAX);
}
#[test]
fn inbound_transfer_flips_rank_only_when_it_dominates() {
let cuda0 = DeviceLocation::Cuda { gpu_id: 0 };
let local = |ns: u64| {
candidate_with_cost(
noop_a,
CostEstimate {
flops: ns,
bytes_moved: 0,
kernel_overhead_ns: 0,
},
)
};
let remote = |ns: u64| Candidate {
backend: BackendId::Cuda,
device: cuda0,
..candidate_with_cost(
noop_b,
CostEstimate {
flops: ns,
bytes_moved: 0,
kernel_overhead_ns: 0,
},
)
};
let est = FlatEstimator {
ns_per_byte: 0,
latency_ns: 1_000,
};
let inputs = [(DeviceLocation::Cpu, 4_u64)];
let mut tiny = AlternativeSet::from_candidates(vec![remote(500), local(600)]);
apply_inbound_transfer_costs(&mut tiny, &inputs, &est);
tiny.rank_by_composite_cost();
assert_eq!(tiny.winner().unwrap().device, DeviceLocation::Cpu);
let mut huge = AlternativeSet::from_candidates(vec![local(20_000), remote(10_000)]);
apply_inbound_transfer_costs(&mut huge, &inputs, &est);
huge.rank_by_composite_cost();
assert_eq!(huge.winner().unwrap().device, cuda0);
}
}