use std::sync::{Arc, RwLock};
use fuel_ir::{DType, Layout, Shape};
use fuel_vulkan_backend::VulkanBackend;
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 VulkanInvoker {
backend: Arc<VulkanBackend>,
out_dtype: DType,
out_shape: Vec<usize>,
params: OpParams,
}
impl VulkanInvoker {
#[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(backend: Arc<VulkanBackend>, out_dtype: DType, out_shape: Vec<usize>) -> Self {
Self {
backend,
out_dtype,
out_shape,
params: OpParams::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_params(mut self, p: OpParams) -> Self {
self.params = p;
self
}
}
impl KernelInvoker for VulkanInvoker {
fn invoke(
&self,
entry: &BindingEntry,
inputs: &[HostTensor],
) -> Result<HostTensor, VerifyError> {
let ins: Vec<Arc<RwLock<fuel_memory::Storage>>> = inputs
.iter()
.map(|t| {
let vb = self
.backend
.upload_bytes_handle(&t.bytes)
.map_err(|e| VerifyError::Backend(e.to_string()))?;
Ok(Arc::new(RwLock::new(fuel_memory::Storage::new(
fuel_memory::BackendStorage::Vulkan(vb),
t.dtype,
))))
})
.collect::<Result<Vec<_>, VerifyError>>()?;
let elem_count = self.out_shape.iter().product::<usize>();
let out_len_bytes = elem_count.saturating_mul(self.out_dtype.size_in_bytes());
let out_vb = self
.backend
.upload_bytes_handle(&vec![0u8; out_len_bytes])
.map_err(|e| VerifyError::Backend(e.to_string()))?;
let out = Arc::new(RwLock::new(fuel_memory::Storage::new(
fuel_memory::BackendStorage::Vulkan(out_vb),
self.out_dtype,
)));
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("VulkanInvoker: output storage RwLock poisoned".to_string())
})?;
let bytes = match &guard.inner {
fuel_memory::BackendStorage::Vulkan(v) => self
.backend
.download_bytes(v)
.map_err(|e| VerifyError::Backend(e.to_string()))?,
#[allow(unreachable_patterns)]
_ => {
return Err(VerifyError::Backend(
"VulkanInvoker: output storage is not Vulkan".to_string(),
));
}
};
Ok(HostTensor {
dtype: self.out_dtype,
shape: self.out_shape.clone(),
bytes,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use fuel_ir::DType;
#[test]
#[ignore = "requires a live Vulkan device"]
fn vulkan_invoker_runs_add_elementwise_f32_end_to_end() {
let backend = match VulkanBackend::new() {
Ok(b) => b,
Err(e) => {
return fuel_test_support::hardware::skip(
fuel_test_support::hardware::Hardware::Vulkan,
fuel_test_support::hardware::Missing::device(format!(
"VulkanBackend::new: {e:?}"
)),
);
}
};
let backend = Arc::new(backend);
let e = crate::kernel::BindingEntry {
kernel: crate::vulkan_dispatch::binary::add_f32,
caps: crate::kernel::KernelCaps::empty(),
precision: crate::fused::PrecisionGuarantee::UNAUDITED,
cost: crate::kernel::unknown_cost,
kernel_source: "fuel-vulkan-kernels",
is_generic: false,
kernel_revision_hash: 0,
cost_expr: None,
};
let inv = VulkanInvoker::new(backend, 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("vulkan invoke");
let got: &[f32] = bytemuck::try_cast_slice(&out.bytes)
.expect("VulkanInvoker output bytes must cast back to f32 (len/align)");
assert_eq!(got, &[5.0, 7.0, 9.0]);
}
}