use fuel_ir::Result;
pub use fuel_ir::dispatch::{
Criterion, DEFAULT_ACCURACY_PENALTY, DispatchOptions, DispatchTable, OpKind, Pick,
ProfileEntry, ProfileReport, SizeClass,
};
use crate::judge_report_path::default_report_path;
use crate::ranker::profile_oracle::ProfileJudgeOracle;
use std::sync::{Arc, OnceLock, RwLock};
#[derive(Clone)]
struct CachedJudge {
table: Arc<DispatchTable>,
oracle: Arc<ProfileJudgeOracle>,
}
impl CachedJudge {
fn from_report(report: &ProfileReport) -> Self {
Self {
table: Arc::new(DispatchTable::build(report)),
oracle: Arc::new(ProfileJudgeOracle::from_report(report)),
}
}
}
static DISPATCH_TABLE: OnceLock<RwLock<Option<CachedJudge>>> = OnceLock::new();
fn slot() -> &'static RwLock<Option<CachedJudge>> {
DISPATCH_TABLE.get_or_init(|| {
let initial = try_load_persisted();
RwLock::new(initial)
})
}
pub fn cached() -> Option<Arc<DispatchTable>> {
slot()
.read()
.ok()
.and_then(|g| g.as_ref().map(|c| Arc::clone(&c.table)))
}
pub fn cached_oracle() -> Option<Arc<ProfileJudgeOracle>> {
slot()
.read()
.ok()
.and_then(|g| g.as_ref().map(|c| Arc::clone(&c.oracle)))
}
pub fn store(report: &ProfileReport) -> Result<()> {
if let Some(p) = default_report_path() {
if let Some(parent) = p.parent() {
let _ = std::fs::create_dir_all(parent);
}
report.save(&p)?;
}
let built = CachedJudge::from_report(report);
*slot()
.write()
.map_err(|_| fuel_ir::Error::Msg("judge cache lock poisoned".into()))? = Some(built);
Ok(())
}
pub fn invalidate() -> Result<()> {
*slot()
.write()
.map_err(|_| fuel_ir::Error::Msg("judge cache lock poisoned".into()))? = None;
if let Some(p) = fuel_hardware::probe::default_report_path() {
let _ = std::fs::remove_file(&p);
}
if let Some(p) = default_report_path() {
let _ = std::fs::remove_file(&p);
}
Ok(())
}
fn try_load_persisted() -> Option<CachedJudge> {
let probe_path = fuel_hardware::probe::default_report_path()?;
let prior_probe = fuel_hardware::probe::ProbeReport::load(&probe_path)
.ok()
.flatten()?;
let now_probe = fuel_hardware::probe::ProbeReport::probe_all();
if now_probe.diff(&prior_probe).needs_rejudge() {
return None;
}
let judge_path = default_report_path()?;
let current: Vec<_> = now_probe.equivalence_classes().into_keys().collect();
let report = ProfileReport::load(&judge_path, ¤t).ok().flatten()?;
Some(CachedJudge::from_report(&report))
}
#[cfg(test)]
mod tests {
use super::*;
use fuel_ir::DType;
use fuel_ir::probe::BackendId;
fn test_equiv_key(backend: BackendId) -> fuel_ir::probe::EquivalenceKey {
fuel_ir::probe::EquivalenceKey {
backend,
vendor_id: 0,
device_id: 0,
compute_capability: None,
driver_version: String::new(),
}
}
fn entry(backend: BackendId, op: OpKind, size: u32, latency: u64, err: f32) -> ProfileEntry {
ProfileEntry {
op,
dtype: DType::F32,
size_class: SizeClass(size),
backend,
device_index: 0,
latency_ns: latency,
iterations: 7,
max_rel_error: err,
kernel_source: String::new(),
kernel_revision_hash: 0,
device: test_equiv_key(backend),
}
}
#[allow(dead_code)]
fn entry_with_source(
backend: BackendId,
op: OpKind,
size: u32,
latency: u64,
err: f32,
kernel_source: &str,
) -> ProfileEntry {
ProfileEntry {
op,
dtype: DType::F32,
size_class: SizeClass(size),
backend,
device_index: 0,
latency_ns: latency,
iterations: 7,
max_rel_error: err,
kernel_source: kernel_source.to_string(),
kernel_revision_hash: 0,
device: test_equiv_key(backend),
}
}
fn sample_report() -> ProfileReport {
ProfileReport {
version: fuel_ir::dispatch::PROFILE_REPORT_VERSION,
entries: vec![
entry(BackendId::Cpu, OpKind::MatMul, 12, 10_000_000, 1e-6),
entry(BackendId::Cuda, OpKind::MatMul, 12, 2_000_000, 1e-4),
entry(BackendId::Cpu, OpKind::MatMul, 16, 500_000, 1e-6),
entry(BackendId::Cuda, OpKind::MatMul, 16, 1_000_000, 1e-3),
],
}
}
#[test]
fn fastest_picks_lowest_latency() {
let tbl = DispatchTable::build(&sample_report());
let p = tbl
.pick(
OpKind::MatMul,
DType::F32,
SizeClass(12),
Criterion::Fastest,
)
.unwrap();
assert_eq!(
p,
Pick {
backend: BackendId::Cuda,
device_index: 0,
kernel_source: "",
kernel_revision_hash: 0,
}
);
}
#[test]
fn most_accurate_picks_lowest_rel_err() {
let tbl = DispatchTable::build(&sample_report());
let p = tbl
.pick(
OpKind::MatMul,
DType::F32,
SizeClass(12),
Criterion::MostAccurate,
)
.unwrap();
assert_eq!(
p,
Pick {
backend: BackendId::Cpu,
device_index: 0,
kernel_source: "",
kernel_revision_hash: 0,
}
);
}
#[test]
fn pick_nearest_falls_back_to_largest_class() {
let tbl = DispatchTable::build(&sample_report());
let p = tbl
.pick_nearest(
OpKind::MatMul,
DType::F32,
SizeClass(14),
Criterion::Fastest,
)
.unwrap();
assert_eq!(
p,
Pick {
backend: BackendId::Cpu,
device_index: 0,
kernel_source: "",
kernel_revision_hash: 0,
}
);
}
#[test]
fn keys_returns_distinct_combinations() {
let tbl = DispatchTable::build(&sample_report());
let keys = tbl.keys();
assert_eq!(keys.len(), 2);
}
#[test]
fn two_kernel_sources_at_one_cpu_cell_produce_two_entries_and_picker_names_winner() {
let report = ProfileReport {
version: fuel_ir::dispatch::PROFILE_REPORT_VERSION,
entries: vec![
entry_with_source(
BackendId::Cpu,
OpKind::MatMul,
12,
1_000_000,
1e-6,
"portable-cpu",
),
entry_with_source(BackendId::Cpu, OpKind::MatMul, 12, 200_000, 1e-6, "aocl"),
],
};
assert_eq!(report.entries.len(), 2);
let portable = &report.entries[0];
let aocl = &report.entries[1];
assert_eq!(portable.kernel_source, "portable-cpu");
assert_eq!(aocl.kernel_source, "aocl");
assert_eq!(portable.backend, aocl.backend); assert_eq!(portable.device_index, aocl.device_index);
let tbl = DispatchTable::build(&report);
let p = tbl
.pick(
OpKind::MatMul,
DType::F32,
SizeClass(12),
Criterion::Fastest,
)
.unwrap();
assert_eq!(p.backend, BackendId::Cpu);
assert_eq!(p.device_index, 0);
assert_eq!(p.kernel_source, "aocl");
}
fn fastest_matmul_12_backend(table: &DispatchTable) -> BackendId {
table
.pick(
OpKind::MatMul,
DType::F32,
SizeClass(12),
Criterion::Fastest,
)
.unwrap()
.backend
}
fn swapped_backend_report() -> ProfileReport {
ProfileReport {
version: fuel_ir::dispatch::PROFILE_REPORT_VERSION,
entries: vec![
entry(BackendId::Cuda, OpKind::MatMul, 12, 10_000_000, 1e-6),
entry(BackendId::Cpu, OpKind::MatMul, 12, 2_000_000, 1e-4),
],
}
}
#[test]
fn cached_hit_miss_invalidate_sequence_and_store_is_the_only_writer() {
invalidate().unwrap();
assert!(cached().is_none(), "miss: no report stored yet");
assert!(cached_oracle().is_none(), "miss: no report stored yet");
store(&sample_report()).unwrap();
assert!(cached().is_some(), "hit: store() populated the slot");
assert!(
cached_oracle().is_some(),
"hit: store() populated the oracle half too"
);
assert_eq!(
fastest_matmul_12_backend(&cached().unwrap()),
BackendId::Cuda,
"same table as sample_report direct-build"
);
invalidate().unwrap();
assert!(cached().is_none(), "miss again: invalidate cleared it");
assert!(
cached_oracle().is_none(),
"miss again: invalidate cleared it"
);
store(&swapped_backend_report()).unwrap();
assert_eq!(
fastest_matmul_12_backend(&cached().unwrap()),
BackendId::Cpu,
"cached() reflects the SECOND store(), confirming store is the sole writer"
);
invalidate().unwrap();
}
}