use std::collections::HashMap;
use std::sync::{Arc, OnceLock, RwLock};
use rlx_ir::{DType, Shape};
macro_rules! dtype_variants {
(
$(
$variant:ident => $rust_ty:ty,
$as_method:ident, $as_mut_method:ident,
$expect_method:ident, $expect_mut_method:ident,
)*
) => {
pub enum CpuTensorRef<'a> {
$(
$variant { data: &'a [$rust_ty], shape: &'a Shape },
)*
}
pub enum CpuTensorMut<'a> {
$(
$variant { data: &'a mut [$rust_ty], shape: &'a Shape },
)*
}
impl<'a> CpuTensorRef<'a> {
pub fn shape(&self) -> &Shape {
match self {
$( Self::$variant { shape, .. } => shape, )*
}
}
pub fn dtype(&self) -> DType { self.shape().dtype() }
$(
pub fn $as_method(&self) -> Option<&[$rust_ty]> {
if let Self::$variant { data, .. } = self { Some(data) } else { None }
}
pub fn $expect_method(&self, role: &str) -> Result<&[$rust_ty], String> {
self.$as_method().ok_or_else(|| format!(
"{role}: expected {:?}, got {:?}",
DType::$variant, self.dtype()))
}
)*
}
impl<'a> CpuTensorMut<'a> {
pub fn shape(&self) -> &Shape {
match self {
$( Self::$variant { shape, .. } => shape, )*
}
}
pub fn dtype(&self) -> DType { self.shape().dtype() }
$(
pub fn $as_mut_method(self) -> Option<&'a mut [$rust_ty]> {
if let Self::$variant { data, .. } = self { Some(data) } else { None }
}
pub fn $expect_mut_method(self, role: &str) -> Result<&'a mut [$rust_ty], String> {
let dt = self.dtype();
self.$as_mut_method().ok_or_else(|| format!(
"{role}: expected {:?}, got {dt:?}", DType::$variant))
}
)*
}
};
}
dtype_variants! {
F32 => f32, as_f32, as_f32_mut, expect_f32, expect_f32_mut,
F64 => f64, as_f64, as_f64_mut, expect_f64, expect_f64_mut,
F16 => half::f16, as_f16, as_f16_mut, expect_f16, expect_f16_mut,
BF16 => half::bf16, as_bf16, as_bf16_mut, expect_bf16, expect_bf16_mut,
I8 => i8, as_i8, as_i8_mut, expect_i8, expect_i8_mut,
I16 => i16, as_i16, as_i16_mut, expect_i16, expect_i16_mut,
I32 => i32, as_i32, as_i32_mut, expect_i32, expect_i32_mut,
I64 => i64, as_i64, as_i64_mut, expect_i64, expect_i64_mut,
U8 => u8, as_u8, as_u8_mut, expect_u8, expect_u8_mut,
U32 => u32, as_u32, as_u32_mut, expect_u32, expect_u32_mut,
Bool => u8, as_bool, as_bool_mut, expect_bool, expect_bool_mut,
}
pub trait CpuKernel: Send + Sync {
fn name(&self) -> &str;
fn execute(
&self,
inputs: &[CpuTensorRef<'_>],
output: CpuTensorMut<'_>,
attrs: &[u8],
) -> Result<(), String>;
}
pub struct CpuKernelRegistry {
kernels: RwLock<HashMap<String, Arc<dyn CpuKernel>>>,
}
impl CpuKernelRegistry {
pub fn new() -> Self {
Self {
kernels: RwLock::new(HashMap::new()),
}
}
pub fn register(&self, k: Arc<dyn CpuKernel>) {
let name = k.name().to_string();
let mut g = self.kernels.write().unwrap();
if g.contains_key(&name) {
eprintln!(
"rlx-cpu: CpuKernel '{name}' was already registered — \
replacing the previous entry"
);
}
g.insert(name, k);
}
pub fn lookup(&self, name: &str) -> Option<Arc<dyn CpuKernel>> {
self.kernels.read().unwrap().get(name).cloned()
}
}
impl Default for CpuKernelRegistry {
fn default() -> Self {
Self::new()
}
}
pub fn global_cpu_kernels() -> &'static CpuKernelRegistry {
static R: OnceLock<CpuKernelRegistry> = OnceLock::new();
R.get_or_init(CpuKernelRegistry::new)
}
pub fn register_cpu_kernel(k: Arc<dyn CpuKernel>) {
global_cpu_kernels().register(k);
}
pub fn lookup_cpu_kernel(name: &str) -> Option<Arc<dyn CpuKernel>> {
global_cpu_kernels().lookup(name)
}
pub fn run_f32_custom_op_host(
name: &str,
inputs: &[(&[u8], &Shape)],
out: (&mut [u8], &Shape),
attrs: &[u8],
) -> Result<(), String> {
let kernel = lookup_cpu_kernel(name)
.ok_or_else(|| format!("no CpuKernel registered for custom op '{name}'"))?;
fn as_f32<'a>(bytes: &'a [u8], role: &str) -> Result<&'a [f32], String> {
if !(bytes.as_ptr() as usize).is_multiple_of(std::mem::align_of::<f32>())
|| !bytes.len().is_multiple_of(4)
{
return Err(format!("{role}: host buffer not f32-aligned/sized"));
}
Ok(unsafe { std::slice::from_raw_parts(bytes.as_ptr() as *const f32, bytes.len() / 4) })
}
let refs: Vec<CpuTensorRef<'_>> = inputs
.iter()
.enumerate()
.map(|(i, &(b, sh))| {
as_f32(b, &format!("{name} input {i}"))
.map(|data| CpuTensorRef::F32 { data, shape: sh })
})
.collect::<Result<_, _>>()?;
let (out_bytes, out_shape) = out;
if !(out_bytes.as_ptr() as usize).is_multiple_of(std::mem::align_of::<f32>())
|| !out_bytes.len().is_multiple_of(4)
{
return Err(format!("{name} output: host buffer not f32-aligned/sized"));
}
let out_len = out_bytes.len() / 4;
let out_data =
unsafe { std::slice::from_raw_parts_mut(out_bytes.as_mut_ptr() as *mut f32, out_len) };
let out_view = CpuTensorMut::F32 {
data: out_data,
shape: out_shape,
};
kernel.execute(&refs, out_view, attrs)
}
fn as_typed<'a, T>(bytes: &'a [u8], role: &str) -> Result<&'a [T], String> {
let sz = std::mem::size_of::<T>();
if !(bytes.as_ptr() as usize).is_multiple_of(std::mem::align_of::<T>())
|| !bytes.len().is_multiple_of(sz)
{
return Err(format!("{role}: host buffer not {sz}-byte aligned/sized"));
}
Ok(unsafe { std::slice::from_raw_parts(bytes.as_ptr() as *const T, bytes.len() / sz) })
}
fn as_typed_mut<'a, T>(bytes: &'a mut [u8], role: &str) -> Result<&'a mut [T], String> {
let sz = std::mem::size_of::<T>();
if !(bytes.as_ptr() as usize).is_multiple_of(std::mem::align_of::<T>())
|| !bytes.len().is_multiple_of(sz)
{
return Err(format!("{role}: host buffer not {sz}-byte aligned/sized"));
}
let n = bytes.len() / sz;
Ok(unsafe { std::slice::from_raw_parts_mut(bytes.as_mut_ptr() as *mut T, n) })
}
fn cpu_ref_from_bytes<'a>(
b: &'a [u8],
sh: &'a Shape,
role: &str,
) -> Result<CpuTensorRef<'a>, String> {
Ok(match sh.dtype() {
DType::F32 => CpuTensorRef::F32 {
data: as_typed(b, role)?,
shape: sh,
},
DType::F64 => CpuTensorRef::F64 {
data: as_typed(b, role)?,
shape: sh,
},
DType::F16 => CpuTensorRef::F16 {
data: as_typed(b, role)?,
shape: sh,
},
DType::BF16 => CpuTensorRef::BF16 {
data: as_typed(b, role)?,
shape: sh,
},
DType::I8 => CpuTensorRef::I8 {
data: as_typed(b, role)?,
shape: sh,
},
DType::I16 => CpuTensorRef::I16 {
data: as_typed(b, role)?,
shape: sh,
},
DType::I32 => CpuTensorRef::I32 {
data: as_typed(b, role)?,
shape: sh,
},
DType::I64 => CpuTensorRef::I64 {
data: as_typed(b, role)?,
shape: sh,
},
DType::U8 => CpuTensorRef::U8 {
data: as_typed(b, role)?,
shape: sh,
},
DType::U32 => CpuTensorRef::U32 {
data: as_typed(b, role)?,
shape: sh,
},
DType::Bool => CpuTensorRef::Bool {
data: as_typed(b, role)?,
shape: sh,
},
other => {
return Err(format!(
"{role}: unsupported dtype {other:?} for host custom-op"
));
}
})
}
fn cpu_mut_from_bytes<'a>(b: &'a mut [u8], sh: &'a Shape) -> Result<CpuTensorMut<'a>, String> {
Ok(match sh.dtype() {
DType::F32 => CpuTensorMut::F32 {
data: as_typed_mut(b, "output")?,
shape: sh,
},
DType::F64 => CpuTensorMut::F64 {
data: as_typed_mut(b, "output")?,
shape: sh,
},
DType::F16 => CpuTensorMut::F16 {
data: as_typed_mut(b, "output")?,
shape: sh,
},
DType::BF16 => CpuTensorMut::BF16 {
data: as_typed_mut(b, "output")?,
shape: sh,
},
DType::I8 => CpuTensorMut::I8 {
data: as_typed_mut(b, "output")?,
shape: sh,
},
DType::I16 => CpuTensorMut::I16 {
data: as_typed_mut(b, "output")?,
shape: sh,
},
DType::I32 => CpuTensorMut::I32 {
data: as_typed_mut(b, "output")?,
shape: sh,
},
DType::I64 => CpuTensorMut::I64 {
data: as_typed_mut(b, "output")?,
shape: sh,
},
DType::U8 => CpuTensorMut::U8 {
data: as_typed_mut(b, "output")?,
shape: sh,
},
DType::U32 => CpuTensorMut::U32 {
data: as_typed_mut(b, "output")?,
shape: sh,
},
DType::Bool => CpuTensorMut::Bool {
data: as_typed_mut(b, "output")?,
shape: sh,
},
other => {
return Err(format!(
"output: unsupported dtype {other:?} for host custom-op"
));
}
})
}
pub fn run_custom_op_host(
name: &str,
inputs: &[(&[u8], &Shape)],
out: (&mut [u8], &Shape),
attrs: &[u8],
) -> Result<(), String> {
let kernel = lookup_cpu_kernel(name)
.ok_or_else(|| format!("no CpuKernel registered for custom op '{name}'"))?;
let refs: Vec<CpuTensorRef<'_>> = inputs
.iter()
.enumerate()
.map(|(i, &(b, sh))| cpu_ref_from_bytes(b, sh, &format!("{name} input {i}")))
.collect::<Result<_, _>>()?;
let (out_bytes, out_shape) = out;
let out_view = cpu_mut_from_bytes(out_bytes, out_shape)?;
kernel.execute(&refs, out_view, attrs)
}