use std::sync::{Arc, RwLock};
use fuel_ir::{DType, Layout, Shape};
use crate::fkc::verify::bit_stability::{HostTensor, KernelInvoker, VerifyError};
use crate::kernel::{BindingEntry, OpParams};
#[allow(
dead_code,
reason = "GAP-236 (unpublished verify API): fkc::verify's modules are private, so nothing outside this crate can reach it. Does NOT retire itself -- the expiry lives in GAP-236 and in Unpopped's handback precondition guard, which fires on their side when the API is named."
)]
pub struct CpuInvoker {
out_dtype: DType,
out_shape: Vec<usize>,
params: OpParams,
out_seed: Option<Vec<u8>>,
}
impl CpuInvoker {
#[allow(
dead_code,
reason = "GAP-236 (unpublished verify API): fkc::verify's modules are private, so nothing outside this crate can reach it. Does NOT retire itself -- the expiry lives in GAP-236 and in Unpopped's handback precondition guard, which fires on their side when the API is named."
)]
pub fn new(out_dtype: DType, out_shape: Vec<usize>) -> Self {
Self {
out_dtype,
out_shape,
params: OpParams::None,
out_seed: None,
}
}
#[allow(
dead_code,
reason = "GAP-236 (unpublished verify API): fkc::verify's modules are private, so nothing outside this crate can reach it. Does NOT retire itself -- the expiry lives in GAP-236 and in Unpopped's handback precondition guard, which fires on their side when the API is named."
)]
pub fn with_seeded_output(mut self, bytes: Vec<u8>) -> Self {
self.out_seed = Some(bytes);
self
}
#[allow(
dead_code,
reason = "GAP-236 (unpublished verify API): fkc::verify's modules are private, so nothing outside this crate can reach it. Does NOT retire itself -- the expiry lives in GAP-236 and in Unpopped's handback precondition guard, which fires on their side when the API is named."
)]
pub fn with_params(mut self, p: OpParams) -> Self {
self.params = p;
self
}
}
impl KernelInvoker for CpuInvoker {
fn invoke(
&self,
entry: &BindingEntry,
inputs: &[HostTensor],
) -> Result<HostTensor, VerifyError> {
let ins: Vec<Arc<RwLock<fuel_memory::Storage>>> = inputs
.iter()
.map(|t| {
Arc::new(RwLock::new(fuel_memory::Storage::new(
fuel_memory::BackendStorage::Cpu(
fuel_cpu_backend::CpuStorageBytes::from_slice(&t.bytes),
),
t.dtype,
)))
})
.collect();
let elem_count = self.out_shape.iter().product::<usize>();
let out_storage = match &self.out_seed {
None => fuel_memory::alloc_cpu_zeroed(self.out_dtype, elem_count)
.map_err(|e| VerifyError::Backend(e.to_string()))?,
Some(bytes) => {
let want = elem_count * self.out_dtype.size_in_bytes();
if bytes.len() != want {
return Err(VerifyError::Backend(format!(
"CpuInvoker: seeded output is {} bytes, expected {want} for {:?} x {:?} — refusing to run rather than seed a partial buffer and leave a zeroed tail",
bytes.len(),
self.out_dtype,
self.out_shape,
)));
}
fuel_memory::Storage::new(
fuel_memory::BackendStorage::Cpu(
fuel_cpu_backend::CpuStorageBytes::from_slice(bytes),
),
self.out_dtype,
)
}
};
let out = Arc::new(RwLock::new(out_storage));
let mut outs = [out.clone()];
let layouts: Vec<Layout> = inputs
.iter()
.map(|t| Layout::contiguous(Shape::from_dims(&t.shape)))
.chain(std::iter::once(Layout::contiguous(Shape::from_dims(
&self.out_shape,
))))
.collect();
(entry.kernel)(&ins, &mut outs, &layouts, &self.params)
.map_err(|e| VerifyError::Invoke(format!("{e:?}")))?;
let guard = out.read().map_err(|_| {
VerifyError::Backend("CpuInvoker: output storage RwLock poisoned".to_string())
})?;
let bytes = crate::dispatch::cpu_input(&guard)
.map_err(|e| VerifyError::Backend(e.to_string()))?
.bytes()
.to_vec();
Ok(HostTensor {
dtype: self.out_dtype,
shape: self.out_shape.clone(),
bytes,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::fkc::verify::bit_stability::{HostTensor, KernelInvoker};
use fuel_ir::DType;
#[test]
fn a_seeded_output_buffer_actually_reaches_an_inplace_kernel() {
let entry = crate::kernel::BindingEntry {
kernel: crate::dispatch::relu_inplace_f32_cpu_wrapper,
caps: crate::kernel::KernelCaps::empty(),
precision: crate::fused::PrecisionGuarantee::UNAUDITED,
cost: crate::kernel::unknown_cost,
kernel_source: "portable-cpu",
is_generic: false,
kernel_revision_hash: 0,
cost_expr: None,
};
let seed: [f32; 4] = [-2.0, 1.5, -0.25, 3.0];
let seed_bytes = bytemuck::cast_slice(&seed).to_vec();
let zeroed = CpuInvoker::new(DType::F32, vec![4])
.invoke(&entry, &[])
.expect("zeroed invoke");
let seeded = CpuInvoker::new(DType::F32, vec![4])
.with_seeded_output(seed_bytes.clone())
.invoke(&entry, &[])
.expect("seeded invoke");
assert_ne!(
zeroed.bytes, seeded.bytes,
"seeded and zeroed runs produced identical bytes — `with_seeded_output` is inert, and every in-place verification through it is vacuous"
);
let got: Vec<f32> = bytemuck::cast_slice(&seeded.bytes).to_vec();
assert_eq!(
got,
vec![0.0f32, 1.5, 0.0, 3.0],
"expected relu of the seed"
);
let z: Vec<f32> = bytemuck::cast_slice(&zeroed.bytes).to_vec();
assert_eq!(z, vec![0.0f32; 4]);
}
#[test]
fn a_wrong_length_seed_is_refused_instead_of_partially_applied() {
let entry = crate::kernel::BindingEntry {
kernel: crate::dispatch::relu_inplace_f32_cpu_wrapper,
caps: crate::kernel::KernelCaps::empty(),
precision: crate::fused::PrecisionGuarantee::UNAUDITED,
cost: crate::kernel::unknown_cost,
kernel_source: "portable-cpu",
is_generic: false,
kernel_revision_hash: 0,
cost_expr: None,
};
let short: Vec<u8> = vec![0u8; 3 * 4]; let err = CpuInvoker::new(DType::F32, vec![4])
.with_seeded_output(short)
.invoke(&entry, &[]);
assert!(
err.is_err(),
"a 3-element seed for a 4-element output was accepted"
);
}
#[test]
fn cpu_invoker_runs_add_elementwise_f32_end_to_end() {
let e = crate::kernel::BindingEntry {
kernel: crate::dispatch::add_elementwise_f32_cpu_wrapper,
caps: crate::kernel::KernelCaps::empty(),
precision: crate::fused::PrecisionGuarantee::UNAUDITED,
cost: crate::kernel::unknown_cost,
kernel_source: "portable-cpu",
is_generic: false,
kernel_revision_hash: 0,
cost_expr: None,
};
let inv = CpuInvoker::new(DType::F32, vec![3]);
let a = HostTensor {
dtype: DType::F32,
shape: vec![3],
bytes: bytemuck::cast_slice(&[1.0f32, 2.0, 3.0]).to_vec(),
};
let b = HostTensor {
dtype: DType::F32,
shape: vec![3],
bytes: bytemuck::cast_slice(&[4.0f32, 5.0, 6.0]).to_vec(),
};
let out = inv.invoke(&e, &[a, b]).expect("cpu invoke");
let got: &[f32] = bytemuck::try_cast_slice(&out.bytes)
.expect("CpuInvoker output bytes must cast back to f32 (len/align)");
assert_eq!(got, &[5.0, 7.0, 9.0]);
}
}