1use std::collections::HashMap;
53use std::sync::{Arc, OnceLock, RwLock};
54
55use rlx_ir::{DType, Shape};
56
57macro_rules! dtype_variants {
71 (
72 $(
73 $variant:ident => $rust_ty:ty,
74 $as_method:ident, $as_mut_method:ident,
75 $expect_method:ident, $expect_mut_method:ident,
76 )*
77 ) => {
78 pub enum CpuTensorRef<'a> {
81 $(
82 $variant { data: &'a [$rust_ty], shape: &'a Shape },
83 )*
84 }
85
86 pub enum CpuTensorMut<'a> {
88 $(
89 $variant { data: &'a mut [$rust_ty], shape: &'a Shape },
90 )*
91 }
92
93 impl<'a> CpuTensorRef<'a> {
94 pub fn shape(&self) -> &Shape {
95 match self {
96 $( Self::$variant { shape, .. } => shape, )*
97 }
98 }
99 pub fn dtype(&self) -> DType { self.shape().dtype() }
100
101 $(
102 pub fn $as_method(&self) -> Option<&[$rust_ty]> {
103 if let Self::$variant { data, .. } = self { Some(data) } else { None }
104 }
105 pub fn $expect_method(&self, role: &str) -> Result<&[$rust_ty], String> {
106 self.$as_method().ok_or_else(|| format!(
107 "{role}: expected {:?}, got {:?}",
108 DType::$variant, self.dtype()))
109 }
110 )*
111 }
112
113 impl<'a> CpuTensorMut<'a> {
114 pub fn shape(&self) -> &Shape {
115 match self {
116 $( Self::$variant { shape, .. } => shape, )*
117 }
118 }
119 pub fn dtype(&self) -> DType { self.shape().dtype() }
120
121 $(
122 pub fn $as_mut_method(self) -> Option<&'a mut [$rust_ty]> {
123 if let Self::$variant { data, .. } = self { Some(data) } else { None }
124 }
125 pub fn $expect_mut_method(self, role: &str) -> Result<&'a mut [$rust_ty], String> {
126 let dt = self.dtype();
127 self.$as_mut_method().ok_or_else(|| format!(
128 "{role}: expected {:?}, got {dt:?}", DType::$variant))
129 }
130 )*
131 }
132 };
133}
134
135dtype_variants! {
140 F32 => f32, as_f32, as_f32_mut, expect_f32, expect_f32_mut,
141 F64 => f64, as_f64, as_f64_mut, expect_f64, expect_f64_mut,
142 F16 => half::f16, as_f16, as_f16_mut, expect_f16, expect_f16_mut,
143 BF16 => half::bf16, as_bf16, as_bf16_mut, expect_bf16, expect_bf16_mut,
144 I8 => i8, as_i8, as_i8_mut, expect_i8, expect_i8_mut,
145 I16 => i16, as_i16, as_i16_mut, expect_i16, expect_i16_mut,
146 I32 => i32, as_i32, as_i32_mut, expect_i32, expect_i32_mut,
147 I64 => i64, as_i64, as_i64_mut, expect_i64, expect_i64_mut,
148 U8 => u8, as_u8, as_u8_mut, expect_u8, expect_u8_mut,
149 U32 => u32, as_u32, as_u32_mut, expect_u32, expect_u32_mut,
150 Bool => u8, as_bool, as_bool_mut, expect_bool, expect_bool_mut,
151}
152
153pub trait CpuKernel: Send + Sync {
161 fn name(&self) -> &str;
162
163 fn execute(
164 &self,
165 inputs: &[CpuTensorRef<'_>],
166 output: CpuTensorMut<'_>,
167 attrs: &[u8],
168 ) -> Result<(), String>;
169}
170
171pub struct CpuKernelRegistry {
172 kernels: RwLock<HashMap<String, Arc<dyn CpuKernel>>>,
173}
174
175impl CpuKernelRegistry {
176 pub fn new() -> Self {
177 Self {
178 kernels: RwLock::new(HashMap::new()),
179 }
180 }
181
182 pub fn register(&self, k: Arc<dyn CpuKernel>) {
186 let name = k.name().to_string();
187 let mut g = self.kernels.write().unwrap();
188 if g.contains_key(&name) {
189 eprintln!(
190 "rlx-cpu: CpuKernel '{name}' was already registered — \
191 replacing the previous entry"
192 );
193 }
194 g.insert(name, k);
195 }
196
197 pub fn lookup(&self, name: &str) -> Option<Arc<dyn CpuKernel>> {
198 self.kernels.read().unwrap().get(name).cloned()
199 }
200}
201
202impl Default for CpuKernelRegistry {
203 fn default() -> Self {
204 Self::new()
205 }
206}
207
208pub fn global_cpu_kernels() -> &'static CpuKernelRegistry {
209 static R: OnceLock<CpuKernelRegistry> = OnceLock::new();
210 R.get_or_init(CpuKernelRegistry::new)
211}
212
213pub fn register_cpu_kernel(k: Arc<dyn CpuKernel>) {
214 global_cpu_kernels().register(k);
215}
216
217pub fn lookup_cpu_kernel(name: &str) -> Option<Arc<dyn CpuKernel>> {
218 global_cpu_kernels().lookup(name)
219}
220
221pub fn run_f32_custom_op_host(
233 name: &str,
234 inputs: &[(&[u8], &Shape)],
235 out: (&mut [u8], &Shape),
236 attrs: &[u8],
237) -> Result<(), String> {
238 let kernel = lookup_cpu_kernel(name)
239 .ok_or_else(|| format!("no CpuKernel registered for custom op '{name}'"))?;
240
241 fn as_f32<'a>(bytes: &'a [u8], role: &str) -> Result<&'a [f32], String> {
242 if !(bytes.as_ptr() as usize).is_multiple_of(std::mem::align_of::<f32>())
243 || !bytes.len().is_multiple_of(4)
244 {
245 return Err(format!("{role}: host buffer not f32-aligned/sized"));
246 }
247 Ok(unsafe { std::slice::from_raw_parts(bytes.as_ptr() as *const f32, bytes.len() / 4) })
249 }
250
251 let refs: Vec<CpuTensorRef<'_>> = inputs
252 .iter()
253 .enumerate()
254 .map(|(i, &(b, sh))| {
255 as_f32(b, &format!("{name} input {i}"))
256 .map(|data| CpuTensorRef::F32 { data, shape: sh })
257 })
258 .collect::<Result<_, _>>()?;
259
260 let (out_bytes, out_shape) = out;
261 if !(out_bytes.as_ptr() as usize).is_multiple_of(std::mem::align_of::<f32>())
262 || !out_bytes.len().is_multiple_of(4)
263 {
264 return Err(format!("{name} output: host buffer not f32-aligned/sized"));
265 }
266 let out_len = out_bytes.len() / 4;
267 let out_data =
269 unsafe { std::slice::from_raw_parts_mut(out_bytes.as_mut_ptr() as *mut f32, out_len) };
270 let out_view = CpuTensorMut::F32 {
271 data: out_data,
272 shape: out_shape,
273 };
274
275 kernel.execute(&refs, out_view, attrs)
276}
277
278fn as_typed<'a, T>(bytes: &'a [u8], role: &str) -> Result<&'a [T], String> {
282 let sz = std::mem::size_of::<T>();
283 if !(bytes.as_ptr() as usize).is_multiple_of(std::mem::align_of::<T>())
284 || !bytes.len().is_multiple_of(sz)
285 {
286 return Err(format!("{role}: host buffer not {sz}-byte aligned/sized"));
287 }
288 Ok(unsafe { std::slice::from_raw_parts(bytes.as_ptr() as *const T, bytes.len() / sz) })
289}
290
291fn as_typed_mut<'a, T>(bytes: &'a mut [u8], role: &str) -> Result<&'a mut [T], String> {
292 let sz = std::mem::size_of::<T>();
293 if !(bytes.as_ptr() as usize).is_multiple_of(std::mem::align_of::<T>())
294 || !bytes.len().is_multiple_of(sz)
295 {
296 return Err(format!("{role}: host buffer not {sz}-byte aligned/sized"));
297 }
298 let n = bytes.len() / sz;
299 Ok(unsafe { std::slice::from_raw_parts_mut(bytes.as_mut_ptr() as *mut T, n) })
300}
301
302fn cpu_ref_from_bytes<'a>(
303 b: &'a [u8],
304 sh: &'a Shape,
305 role: &str,
306) -> Result<CpuTensorRef<'a>, String> {
307 Ok(match sh.dtype() {
308 DType::F32 => CpuTensorRef::F32 {
309 data: as_typed(b, role)?,
310 shape: sh,
311 },
312 DType::F64 => CpuTensorRef::F64 {
313 data: as_typed(b, role)?,
314 shape: sh,
315 },
316 DType::F16 => CpuTensorRef::F16 {
317 data: as_typed(b, role)?,
318 shape: sh,
319 },
320 DType::BF16 => CpuTensorRef::BF16 {
321 data: as_typed(b, role)?,
322 shape: sh,
323 },
324 DType::I8 => CpuTensorRef::I8 {
325 data: as_typed(b, role)?,
326 shape: sh,
327 },
328 DType::I16 => CpuTensorRef::I16 {
329 data: as_typed(b, role)?,
330 shape: sh,
331 },
332 DType::I32 => CpuTensorRef::I32 {
333 data: as_typed(b, role)?,
334 shape: sh,
335 },
336 DType::I64 => CpuTensorRef::I64 {
337 data: as_typed(b, role)?,
338 shape: sh,
339 },
340 DType::U8 => CpuTensorRef::U8 {
341 data: as_typed(b, role)?,
342 shape: sh,
343 },
344 DType::U32 => CpuTensorRef::U32 {
345 data: as_typed(b, role)?,
346 shape: sh,
347 },
348 DType::Bool => CpuTensorRef::Bool {
349 data: as_typed(b, role)?,
350 shape: sh,
351 },
352 other => {
353 return Err(format!(
354 "{role}: unsupported dtype {other:?} for host custom-op"
355 ));
356 }
357 })
358}
359
360fn cpu_mut_from_bytes<'a>(b: &'a mut [u8], sh: &'a Shape) -> Result<CpuTensorMut<'a>, String> {
361 Ok(match sh.dtype() {
362 DType::F32 => CpuTensorMut::F32 {
363 data: as_typed_mut(b, "output")?,
364 shape: sh,
365 },
366 DType::F64 => CpuTensorMut::F64 {
367 data: as_typed_mut(b, "output")?,
368 shape: sh,
369 },
370 DType::F16 => CpuTensorMut::F16 {
371 data: as_typed_mut(b, "output")?,
372 shape: sh,
373 },
374 DType::BF16 => CpuTensorMut::BF16 {
375 data: as_typed_mut(b, "output")?,
376 shape: sh,
377 },
378 DType::I8 => CpuTensorMut::I8 {
379 data: as_typed_mut(b, "output")?,
380 shape: sh,
381 },
382 DType::I16 => CpuTensorMut::I16 {
383 data: as_typed_mut(b, "output")?,
384 shape: sh,
385 },
386 DType::I32 => CpuTensorMut::I32 {
387 data: as_typed_mut(b, "output")?,
388 shape: sh,
389 },
390 DType::I64 => CpuTensorMut::I64 {
391 data: as_typed_mut(b, "output")?,
392 shape: sh,
393 },
394 DType::U8 => CpuTensorMut::U8 {
395 data: as_typed_mut(b, "output")?,
396 shape: sh,
397 },
398 DType::U32 => CpuTensorMut::U32 {
399 data: as_typed_mut(b, "output")?,
400 shape: sh,
401 },
402 DType::Bool => CpuTensorMut::Bool {
403 data: as_typed_mut(b, "output")?,
404 shape: sh,
405 },
406 other => {
407 return Err(format!(
408 "output: unsupported dtype {other:?} for host custom-op"
409 ));
410 }
411 })
412}
413
414pub fn run_custom_op_host(
421 name: &str,
422 inputs: &[(&[u8], &Shape)],
423 out: (&mut [u8], &Shape),
424 attrs: &[u8],
425) -> Result<(), String> {
426 let kernel = lookup_cpu_kernel(name)
427 .ok_or_else(|| format!("no CpuKernel registered for custom op '{name}'"))?;
428 let refs: Vec<CpuTensorRef<'_>> = inputs
429 .iter()
430 .enumerate()
431 .map(|(i, &(b, sh))| cpu_ref_from_bytes(b, sh, &format!("{name} input {i}")))
432 .collect::<Result<_, _>>()?;
433 let (out_bytes, out_shape) = out;
434 let out_view = cpu_mut_from_bytes(out_bytes, out_shape)?;
435 kernel.execute(&refs, out_view, attrs)
436}