use std::sync::Arc;
use fuel_ir::DType;
use fuel_ir::dispatch::OpKind;
use fuel_ir::probe::BackendId;
use fuel_vulkan_backend::VulkanBackend;
use super::accuracy_common::{
Claim, OpAccuracy, f32s, find_entry, measure_matmul, measure_rms_norm, measure_rope,
measure_softmax,
};
use super::bit_stability::{HostTensor, KernelInvoker, VerifyError};
use super::invoker_cpu::CpuInvoker;
use super::invoker_vulkan::VulkanInvoker;
use super::ledger::LedgerRecord;
use crate::kernel::{BindingEntry, KernelBindingTable, OpParams};
const BACKEND: &str = "Vulkan";
const HARNESS: &str = "board-240/seed_vulkan_matmul_accuracy";
pub(crate) const MATMUL_F32_CLAIM: Claim = Claim {
max_relative: 1.0e-3,
max_absolute: 1.0e-3,
};
pub(crate) const RMS_NORM_F32_CLAIM: Claim = Claim {
max_relative: 1.0e-5,
max_absolute: 1.0e-5,
};
pub(crate) const SOFTMAX_F32_CLAIM: Claim = Claim {
max_relative: 1.0e-5,
max_absolute: 1.0e-5,
};
pub(crate) const ROPE_F32_CLAIM: Claim = Claim {
max_relative: 1.0e-5,
max_absolute: 1.0e-5,
};
fn run_vulkan(
backend: &Arc<VulkanBackend>,
entry: &BindingEntry,
params: OpParams,
inputs: &[HostTensor],
out_shape: Vec<usize>,
) -> Result<Vec<f32>, VerifyError> {
let inv = VulkanInvoker::new(backend.clone(), DType::F32, out_shape).with_params(params);
Ok(f32s(&inv.invoke(entry, inputs)?))
}
#[derive(Debug)]
pub(crate) struct AccuracyRun {
pub device_name: String,
pub ops: Vec<OpAccuracy>,
}
impl AccuracyRun {
pub(crate) fn records(&self) -> Vec<LedgerRecord> {
self.ops
.iter()
.flat_map(|o| o.records(BACKEND, HARNESS, &self.device_name))
.collect()
}
}
pub(crate) fn measure_vulkan_f32_accuracy(
selection: fuel_vulkan_backend::DeviceSelection,
) -> Result<AccuracyRun, VerifyError> {
let mut table = KernelBindingTable::new();
crate::vulkan_dispatch::register_vulkan_kernels(&mut table);
let mut cpu_table = KernelBindingTable::new();
crate::dispatch::register_cpu_kernels(&mut cpu_table);
let backend = VulkanBackend::with_selection(selection)
.map(Arc::new)
.map_err(|e| VerifyError::Backend(format!("no Vulkan device: {e}")))?;
let device_name = backend.device_name.clone();
let run = |entry: &BindingEntry, params: OpParams, inputs: &[HostTensor], shape: Vec<usize>| {
run_vulkan(&backend, entry, params, inputs, shape)
};
let rope_dt = [DType::F32; 4];
let cpu_rope = find_entry(&cpu_table, OpKind::Rope, &rope_dt, BackendId::Cpu)?;
let cpu_reference = |params: OpParams, inputs: &[HostTensor], shape: Vec<usize>| {
let cpu = CpuInvoker::new(DType::F32, shape).with_params(params);
Ok(f32s(&cpu.invoke(cpu_rope, inputs)?))
};
let entry = |op: OpKind, dt: &[DType]| find_entry(&table, op, dt, BackendId::Vulkan);
let ops = vec![
measure_matmul(
&run,
entry(OpKind::MatMul, &[DType::F32; 3])?,
MATMUL_F32_CLAIM,
)?,
measure_rms_norm(
&run,
entry(OpKind::RmsNormLastDim, &[DType::F32; 2])?,
RMS_NORM_F32_CLAIM,
)?,
measure_softmax(
&run,
entry(OpKind::SoftmaxLastDim, &[DType::F32; 2])?,
SOFTMAX_F32_CLAIM,
)?,
measure_rope(
&run,
&cpu_reference,
entry(OpKind::Rope, &rope_dt)?,
ROPE_F32_CLAIM,
)?,
];
Ok(AccuracyRun { device_name, ops })
}
fn print_run(run: &AccuracyRun) {
println!("[board-240] adapter: {}", run.device_name);
for o in &run.ops {
for c in &o.cases {
println!(
"[board-240] {:?} {:32} abs {:.3e} rel {:.3e}",
o.op, c.label, c.abs, c.rel
);
}
println!(
"[board-240] {:?} WORST abs {:.3e} (claim {}) rel {:.3e} (claim {})",
o.op, o.worst_abs, o.claim.max_absolute, o.worst_rel, o.claim.max_relative
);
}
}
#[cfg(feature = "dev-tools")]
pub(crate) fn run_seed_vulkan_matmul_accuracy() {
let run = measure_vulkan_f32_accuracy(fuel_vulkan_backend::DeviceSelection::PreferDiscrete)
.expect("vulkan f32 accuracy measurement runs");
print_run(&run);
let records = run.records();
for r in &records {
println!(
"[board-240] {} {} -> {}",
r.evidence["op"].as_str().unwrap_or("?"),
r.claim,
r.result
);
}
assert!(
records.iter().all(|r| r.result == "pass"),
"a measurement above the declared claim must not be written as a pass; fix the kernel or \
raise the claim in the contract AND in this module"
);
let summary = super::ledger::write_merged_ledger(&records);
println!(
"[board-240] merged {} record(s) into {} existing -> {} total, written to {}",
summary.fresh,
summary.before,
summary.after,
summary.path.display()
);
}
#[cfg(feature = "dev-tools")]
pub(crate) fn run_check_vulkan_accuracy() {
let selection = match std::env::var("FUEL_VULKAN_TEST_ADAPTER") {
Ok(name) => fuel_vulkan_backend::DeviceSelection::ByName(name),
Err(_) => fuel_vulkan_backend::DeviceSelection::PreferDiscrete,
};
let run = measure_vulkan_f32_accuracy(selection).expect("vulkan f32 accuracy measurement runs");
print_run(&run);
let failing: Vec<_> = run
.records()
.into_iter()
.filter(|r| r.result != "pass")
.collect();
for r in &failing {
println!(
"[board-240] EXCEEDS CLAIM: {} {}",
r.evidence["op"], r.claim
);
}
assert!(
failing.is_empty(),
"{} measurement(s) exceed their claim",
failing.len()
);
}
#[cfg(test)]
mod tests {
use super::super::accuracy_common::worst_error;
use super::*;
#[test]
fn worst_error_sees_a_corrupted_element() {
let want = vec![1.0f64, 2.0, 3.0];
let mut got = vec![1.0f32, 2.0, 3.0];
let (abs, rel) = worst_error(&got, &want);
assert!(abs <= ROPE_F32_CLAIM.max_absolute && rel <= ROPE_F32_CLAIM.max_relative);
got[1] += 0.01;
let (abs, rel) = worst_error(&got, &want);
assert!(abs > MATMUL_F32_CLAIM.max_absolute || rel > MATMUL_F32_CLAIM.max_relative);
}
#[test]
fn claim_constants_match_the_contracts() {
fn section<'a>(text: &'a str, kernel: &str) -> &'a str {
let marker = format!("kernel: {kernel}");
let mut start = 0;
loop {
let at = text[start..]
.find(&marker)
.unwrap_or_else(|| panic!("section `{kernel}` present"));
let after = start + at + marker.len();
if matches!(text.as_bytes().get(after), Some(b'\r' | b'\n')) {
let begin = start + at;
let rest = &text[begin..];
return &rest[..rest.find("```").expect("section ends")];
}
start = after;
}
}
let cases = [
(
include_str!("../../../kernel-contracts/vulkan/matmul.fkc.md"),
"matmul_f32",
MATMUL_F32_CLAIM,
),
(
include_str!("../../../kernel-contracts/vulkan/norm.fkc.md"),
"rms_norm_last_dim_f32",
RMS_NORM_F32_CLAIM,
),
(
include_str!("../../../kernel-contracts/vulkan/norm.fkc.md"),
"softmax_last_dim_f32",
SOFTMAX_F32_CLAIM,
),
(
include_str!("../../../kernel-contracts/vulkan/rope.fkc.md"),
"rope_f32",
ROPE_F32_CLAIM,
),
];
for (text, kernel, claim) in cases {
let s = section(text, kernel);
assert!(
s.contains(&format!("max_relative: {}", claim.max_relative)),
"{kernel}: contract max_relative differs from the claim constant"
);
assert!(
s.contains(&format!("max_absolute: {}", claim.max_absolute)),
"{kernel}: contract max_absolute differs from the claim constant"
);
}
}
#[test]
fn every_claim_is_within_the_default_bound() {
use crate::ranker::filters::DEFAULT_ACCURACY_BOUND;
for c in [
MATMUL_F32_CLAIM,
RMS_NORM_F32_CLAIM,
SOFTMAX_F32_CLAIM,
ROPE_F32_CLAIM,
] {
assert!(c.max_relative <= DEFAULT_ACCURACY_BOUND.max_relative);
assert!(c.max_absolute <= DEFAULT_ACCURACY_BOUND.max_absolute);
}
}
}