use fuel_ir::dispatch::OpKind;
use fuel_ir::probe::BackendId;
use fuel_ir::{DType, DeviceLocation};
use super::alternative_set::AlternativeSet;
use super::candidate::Candidate;
use crate::kernel::{KernelBindingTable, OpParams};
pub fn enumerate_candidates(
op_kind: OpKind,
dtypes: &[DType],
placements: &[(BackendId, DeviceLocation)],
op_params: &OpParams,
bindings: &KernelBindingTable,
) -> AlternativeSet {
let mut set = AlternativeSet::empty();
for &(backend, device) in placements {
for entry in bindings.lookup_alternatives(op_kind, dtypes, backend) {
set.push(Candidate {
kernel: entry.kernel,
caps: entry.caps,
backend,
device,
precision: entry.precision,
static_cost: Default::default(),
inbound_transfer_ns: 0,
op_params: op_params.clone(),
coupling: Vec::new(),
kernel_source: entry.kernel_source,
kernel_revision_hash: entry.kernel_revision_hash,
});
}
}
set
}
#[cfg(test)]
mod tests {
use super::*;
use crate::fused::PrecisionGuarantee;
use crate::kernel::{KernelCaps, unknown_cost};
use fuel_ir::{Layout, Result};
use fuel_memory::Storage;
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 noop_c(
_i: &[Arc<RwLock<Storage>>],
_o: &mut [Arc<RwLock<Storage>>],
_l: &[Layout],
_p: &OpParams,
) -> Result<()> {
Ok(())
}
fn one_dtype_key() -> [DType; 3] {
[DType::F32, DType::F32, DType::F32]
}
fn empty_table() -> KernelBindingTable {
KernelBindingTable::new()
}
type KernelFn = fn(
&[Arc<RwLock<Storage>>],
&mut [Arc<RwLock<Storage>>],
&[Layout],
&OpParams,
) -> Result<()>;
fn table_with(entries: &[(BackendId, KernelFn)]) -> KernelBindingTable {
let mut t = KernelBindingTable::new();
for &(backend, kernel) in entries {
t.register_full(
OpKind::AddElementwise,
&one_dtype_key(),
backend,
kernel,
KernelCaps::empty(),
PrecisionGuarantee::PRIMITIVE_DETERMINISTIC_CPU,
unknown_cost,
);
}
t
}
#[test]
fn empty_placements_yields_empty_set() {
let bindings = table_with(&[(BackendId::Cpu, noop_a)]);
let set = enumerate_candidates(
OpKind::AddElementwise,
&one_dtype_key(),
&[],
&OpParams::None,
&bindings,
);
assert!(set.is_empty());
}
#[test]
fn empty_binding_table_yields_empty_set() {
let bindings = empty_table();
let set = enumerate_candidates(
OpKind::AddElementwise,
&one_dtype_key(),
&[(BackendId::Cpu, DeviceLocation::Cpu)],
&OpParams::None,
&bindings,
);
assert!(set.is_empty());
}
#[test]
fn single_placement_single_alternative() {
let bindings = table_with(&[(BackendId::Cpu, noop_a)]);
let set = enumerate_candidates(
OpKind::AddElementwise,
&one_dtype_key(),
&[(BackendId::Cpu, DeviceLocation::Cpu)],
&OpParams::None,
&bindings,
);
assert_eq!(set.len(), 1);
let c = set.winner().unwrap();
assert_eq!(c.backend, BackendId::Cpu);
assert_eq!(c.device, DeviceLocation::Cpu);
}
#[test]
fn cross_co_located_backends_aggregated() {
let bindings = table_with(&[
(BackendId::Cpu, noop_a),
(BackendId::Cuda, noop_b),
(BackendId::Vulkan, noop_c),
]);
let set = enumerate_candidates(
OpKind::AddElementwise,
&one_dtype_key(),
&[
(BackendId::Cpu, DeviceLocation::Cpu),
(BackendId::Cuda, DeviceLocation::Cpu),
(BackendId::Vulkan, DeviceLocation::Cpu),
],
&OpParams::None,
&bindings,
);
assert_eq!(set.len(), 3);
let backends: Vec<BackendId> = set.alternatives().iter().map(|c| c.backend).collect();
assert_eq!(
backends,
vec![BackendId::Cpu, BackendId::Cuda, BackendId::Vulkan]
);
assert!(
set.alternatives()
.iter()
.all(|c| c.device == DeviceLocation::Cpu)
);
}
#[test]
fn multi_alt_per_backend_within_one_placement() {
let mut bindings = KernelBindingTable::new();
bindings.register_full(
OpKind::AddElementwise,
&one_dtype_key(),
BackendId::Cpu,
noop_a,
KernelCaps::empty(),
PrecisionGuarantee::PRIMITIVE_DETERMINISTIC_CPU,
unknown_cost,
);
bindings.register_full(
OpKind::AddElementwise,
&one_dtype_key(),
BackendId::Cpu,
noop_b,
KernelCaps::empty(),
PrecisionGuarantee::PRIMITIVE_DETERMINISTIC_CPU,
unknown_cost,
);
let set = enumerate_candidates(
OpKind::AddElementwise,
&one_dtype_key(),
&[(BackendId::Cpu, DeviceLocation::Cpu)],
&OpParams::None,
&bindings,
);
assert_eq!(set.len(), 2, "both alternatives at the same key surface");
}
#[test]
fn placement_with_no_kernel_silently_contributes_zero() {
let bindings = table_with(&[(BackendId::Cpu, noop_a)]);
let set = enumerate_candidates(
OpKind::AddElementwise,
&one_dtype_key(),
&[
(BackendId::Cuda, DeviceLocation::Cuda { gpu_id: 0 }),
(BackendId::Cpu, DeviceLocation::Cpu),
],
&OpParams::None,
&bindings,
);
assert_eq!(set.len(), 1);
assert_eq!(set.winner().unwrap().backend, BackendId::Cpu);
}
#[test]
fn enumerator_does_not_truncate() {
let mut bindings = KernelBindingTable::new();
for k in [noop_a, noop_b, noop_c] {
bindings.register_full(
OpKind::AddElementwise,
&one_dtype_key(),
BackendId::Cpu,
k,
KernelCaps::empty(),
PrecisionGuarantee::PRIMITIVE_DETERMINISTIC_CPU,
unknown_cost,
);
}
let set = enumerate_candidates(
OpKind::AddElementwise,
&one_dtype_key(),
&[(BackendId::Cpu, DeviceLocation::Cpu)],
&OpParams::None,
&bindings,
);
assert_eq!(set.len(), 3, "enumerator keeps every candidate; no top-N");
}
#[test]
fn op_params_cloned_onto_every_candidate() {
let bindings = table_with(&[(BackendId::Cpu, noop_a), (BackendId::Cuda, noop_b)]);
let params = OpParams::Reduce {
dims: vec![0, 2],
keepdim: false,
};
let set = enumerate_candidates(
OpKind::AddElementwise,
&one_dtype_key(),
&[
(BackendId::Cpu, DeviceLocation::Cpu),
(BackendId::Cuda, DeviceLocation::Cpu),
],
¶ms,
&bindings,
);
assert_eq!(set.len(), 2);
for c in set.alternatives() {
match &c.op_params {
OpParams::Reduce { dims, keepdim } => {
assert_eq!(dims, &vec![0, 2]);
assert!(!*keepdim);
}
other => panic!("expected Reduce, got {other:?}"),
}
}
}
}