Skip to main content

tenflowers_core/
dispatch_registry.rs

1/// Unified Operation Dispatch Registry for TenfloweRS
2///
3/// This module provides a centralized system for registering and dispatching
4/// tensor operations across different backends (CPU, GPU, BLAS, etc.) with
5/// feature gating and capability detection.
6use crate::{Device, Result, Shape, Tensor, TensorError};
7use std::collections::HashMap;
8use std::sync::{Arc, RwLock};
9
10/// Backend type for kernel implementations
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
12pub enum BackendType {
13    /// CPU implementation (always available)
14    Cpu,
15    /// SIMD-optimized CPU implementation
16    #[cfg(feature = "simd")]
17    SimdCpu,
18    /// BLAS-accelerated implementation
19    #[cfg(feature = "blas")]
20    Blas,
21    /// GPU implementation via WGPU
22    #[cfg(feature = "gpu")]
23    Gpu,
24    /// CUDA implementation
25    #[cfg(all(feature = "cuda", any(target_os = "linux", target_os = "windows")))]
26    Cuda,
27    /// ROCm implementation
28    #[cfg(feature = "rocm")]
29    Rocm,
30    /// Metal Performance Shaders
31    #[cfg(all(feature = "metal", target_os = "macos"))]
32    Metal,
33}
34
35impl BackendType {
36    /// Check if this backend is available at runtime
37    pub fn is_available(&self) -> bool {
38        match self {
39            BackendType::Cpu => true,
40            #[cfg(feature = "simd")]
41            BackendType::SimdCpu => true,
42            #[cfg(feature = "blas")]
43            BackendType::Blas => crate::ops::lapack::is_lapack_available(),
44            #[cfg(feature = "gpu")]
45            BackendType::Gpu => true, // WGPU availability checked at context creation
46            #[cfg(all(feature = "cuda", any(target_os = "linux", target_os = "windows")))]
47            BackendType::Cuda => crate::gpu::cuda_kernels::is_cuda_available(),
48            #[cfg(feature = "rocm")]
49            BackendType::Rocm => false, // NOTE(v0.2): Implement ROCm availability check
50            #[cfg(all(feature = "metal", target_os = "macos"))]
51            BackendType::Metal => true,
52        }
53    }
54
55    /// Get priority for backend selection (higher = preferred)
56    pub fn priority(&self) -> u8 {
57        match self {
58            BackendType::Cpu => 0,
59            #[cfg(feature = "simd")]
60            BackendType::SimdCpu => 10,
61            #[cfg(feature = "blas")]
62            BackendType::Blas => 20,
63            #[cfg(feature = "gpu")]
64            BackendType::Gpu => 30,
65            #[cfg(all(feature = "cuda", any(target_os = "linux", target_os = "windows")))]
66            BackendType::Cuda => 40,
67            #[cfg(feature = "rocm")]
68            BackendType::Rocm => 40,
69            #[cfg(all(feature = "metal", target_os = "macos"))]
70            BackendType::Metal => 50,
71        }
72    }
73
74    /// Get backend from device
75    pub fn from_device(device: &Device) -> Self {
76        match device {
77            Device::Cpu => BackendType::Cpu,
78            #[cfg(feature = "gpu")]
79            Device::Gpu(_) => BackendType::Gpu,
80            #[cfg(feature = "rocm")]
81            Device::Rocm(_) => BackendType::Rocm,
82        }
83    }
84}
85
86/// Operation metadata and description
87#[derive(Debug, Clone)]
88pub struct OperationDescriptor {
89    /// Unique operation name
90    pub name: String,
91    /// Operation category (e.g., "binary", "reduction", "matmul")
92    pub category: String,
93    /// Semantic version
94    pub version: String,
95    /// Supported data types
96    pub supported_dtypes: Vec<crate::DType>,
97    /// Minimum supported rank
98    pub min_rank: Option<usize>,
99    /// Maximum supported rank
100    pub max_rank: Option<usize>,
101    /// Whether operation supports broadcasting
102    pub supports_broadcast: bool,
103    /// Whether operation is in-place capable
104    pub supports_inplace: bool,
105}
106
107impl OperationDescriptor {
108    /// Create a new operation descriptor
109    pub fn new(name: impl Into<String>, category: impl Into<String>) -> Self {
110        Self {
111            name: name.into(),
112            category: category.into(),
113            version: "1.0.0".to_string(),
114            supported_dtypes: vec![crate::DType::Float32, crate::DType::Float64],
115            min_rank: None,
116            max_rank: None,
117            supports_broadcast: false,
118            supports_inplace: false,
119        }
120    }
121
122    /// Set supported data types
123    pub fn with_dtypes(mut self, dtypes: Vec<crate::DType>) -> Self {
124        self.supported_dtypes = dtypes;
125        self
126    }
127
128    /// Set rank constraints
129    pub fn with_rank_range(mut self, min: Option<usize>, max: Option<usize>) -> Self {
130        self.min_rank = min;
131        self.max_rank = max;
132        self
133    }
134
135    /// Enable broadcasting support
136    pub fn with_broadcast(mut self) -> Self {
137        self.supports_broadcast = true;
138        self
139    }
140
141    /// Enable in-place support
142    pub fn with_inplace(mut self) -> Self {
143        self.supports_inplace = true;
144        self
145    }
146}
147
148/// Kernel implementation function signature for unary operations
149pub type UnaryKernelFn<T> = fn(&Tensor<T>) -> Result<Tensor<T>>;
150
151/// Kernel implementation function signature for binary operations
152pub type BinaryKernelFn<T> = fn(&Tensor<T>, &Tensor<T>) -> Result<Tensor<T>>;
153
154/// Kernel implementation for a specific backend
155#[derive(Clone)]
156pub struct KernelImplementation<T> {
157    pub backend: BackendType,
158    pub unary_fn: Option<UnaryKernelFn<T>>,
159    pub binary_fn: Option<BinaryKernelFn<T>>,
160}
161
162impl<T> KernelImplementation<T> {
163    /// Create a new unary kernel implementation
164    pub fn unary(backend: BackendType, func: UnaryKernelFn<T>) -> Self {
165        Self {
166            backend,
167            unary_fn: Some(func),
168            binary_fn: None,
169        }
170    }
171
172    /// Create a new binary kernel implementation
173    pub fn binary(backend: BackendType, func: BinaryKernelFn<T>) -> Self {
174        Self {
175            backend,
176            unary_fn: None,
177            binary_fn: Some(func),
178        }
179    }
180}
181
182/// Registered operation with all backend implementations
183struct RegisteredOperation<T> {
184    descriptor: OperationDescriptor,
185    kernels: Vec<KernelImplementation<T>>,
186}
187
188impl<T> RegisteredOperation<T> {
189    fn new(descriptor: OperationDescriptor) -> Self {
190        Self {
191            descriptor,
192            kernels: Vec::new(),
193        }
194    }
195
196    fn add_kernel(&mut self, kernel: KernelImplementation<T>) {
197        self.kernels.push(kernel);
198    }
199
200    /// Select the best available kernel for the given device
201    fn select_kernel(&self, device: &Device) -> Option<&KernelImplementation<T>> {
202        let preferred_backend = BackendType::from_device(device);
203
204        // First, try to find the preferred backend
205        if let Some(kernel) = self
206            .kernels
207            .iter()
208            .find(|k| k.backend == preferred_backend && k.backend.is_available())
209        {
210            return Some(kernel);
211        }
212
213        // Fall back to the highest priority available backend
214        self.kernels
215            .iter()
216            .filter(|k| k.backend.is_available())
217            .max_by_key(|k| k.backend.priority())
218    }
219}
220
221/// Global operation dispatch registry
222pub struct DispatchRegistry<T> {
223    operations: Arc<RwLock<HashMap<String, RegisteredOperation<T>>>>,
224}
225
226impl<T> Default for DispatchRegistry<T> {
227    fn default() -> Self {
228        Self::new()
229    }
230}
231
232impl<T> DispatchRegistry<T> {
233    /// Create a new dispatch registry
234    pub fn new() -> Self {
235        Self {
236            operations: Arc::new(RwLock::new(HashMap::new())),
237        }
238    }
239
240    /// Register a new operation
241    pub fn register_operation(&self, descriptor: OperationDescriptor) -> Result<()> {
242        let mut ops = self.operations.write().map_err(|_| {
243            TensorError::invalid_operation_simple(
244                "dispatch registry write lock poisoned".to_string(),
245            )
246        })?;
247
248        if ops.contains_key(&descriptor.name) {
249            return Err(TensorError::invalid_argument(format!(
250                "Operation '{}' is already registered",
251                descriptor.name
252            )));
253        }
254
255        ops.insert(
256            descriptor.name.clone(),
257            RegisteredOperation::new(descriptor),
258        );
259        Ok(())
260    }
261
262    /// Register a kernel implementation for an operation
263    pub fn register_kernel(
264        &self,
265        operation_name: &str,
266        kernel: KernelImplementation<T>,
267    ) -> Result<()> {
268        let mut ops = self.operations.write().map_err(|_| {
269            TensorError::invalid_operation_simple(
270                "dispatch registry write lock poisoned".to_string(),
271            )
272        })?;
273
274        let op = ops.get_mut(operation_name).ok_or_else(|| {
275            TensorError::invalid_argument(format!(
276                "Operation '{}' not found. Register the operation first.",
277                operation_name
278            ))
279        })?;
280
281        op.add_kernel(kernel);
282        Ok(())
283    }
284
285    /// Dispatch a unary operation
286    pub fn dispatch_unary(&self, operation_name: &str, input: &Tensor<T>) -> Result<Tensor<T>> {
287        let ops = self.operations.read().map_err(|_| {
288            TensorError::invalid_operation_simple(
289                "dispatch registry read lock poisoned".to_string(),
290            )
291        })?;
292
293        let op = ops.get(operation_name).ok_or_else(|| {
294            TensorError::invalid_argument(format!(
295                "Operation '{}' not found in registry",
296                operation_name
297            ))
298        })?;
299
300        let kernel = op.select_kernel(input.device()).ok_or_else(|| {
301            TensorError::invalid_argument(format!(
302                "No available kernel for operation '{}' on device {:?}",
303                operation_name,
304                input.device()
305            ))
306        })?;
307
308        let kernel_fn = kernel.unary_fn.ok_or_else(|| {
309            TensorError::invalid_argument(format!(
310                "Operation '{}' does not support unary execution",
311                operation_name
312            ))
313        })?;
314
315        kernel_fn(input)
316    }
317
318    /// Dispatch a binary operation
319    pub fn dispatch_binary(
320        &self,
321        operation_name: &str,
322        lhs: &Tensor<T>,
323        rhs: &Tensor<T>,
324    ) -> Result<Tensor<T>> {
325        // Check device compatibility
326        if lhs.device() != rhs.device() {
327            return Err(TensorError::device_mismatch(
328                operation_name,
329                &format!("{:?}", lhs.device()),
330                &format!("{:?}", rhs.device()),
331            ));
332        }
333
334        let ops = self.operations.read().map_err(|_| {
335            TensorError::invalid_operation_simple(
336                "dispatch registry read lock poisoned".to_string(),
337            )
338        })?;
339
340        let op = ops.get(operation_name).ok_or_else(|| {
341            TensorError::invalid_argument(format!(
342                "Operation '{}' not found in registry",
343                operation_name
344            ))
345        })?;
346
347        let kernel = op.select_kernel(lhs.device()).ok_or_else(|| {
348            TensorError::invalid_argument(format!(
349                "No available kernel for operation '{}' on device {:?}",
350                operation_name,
351                lhs.device()
352            ))
353        })?;
354
355        let kernel_fn = kernel.binary_fn.ok_or_else(|| {
356            TensorError::invalid_argument(format!(
357                "Operation '{}' does not support binary execution",
358                operation_name
359            ))
360        })?;
361
362        kernel_fn(lhs, rhs)
363    }
364
365    /// Get operation descriptor
366    pub fn get_operation(&self, name: &str) -> Option<OperationDescriptor> {
367        self.operations
368            .read()
369            .ok()?
370            .get(name)
371            .map(|op| op.descriptor.clone())
372    }
373
374    /// List all registered operations
375    pub fn list_operations(&self) -> Vec<String> {
376        match self.operations.read() {
377            Ok(ops) => ops.keys().cloned().collect(),
378            Err(_) => Vec::new(),
379        }
380    }
381
382    /// Get available backends for an operation
383    pub fn available_backends(&self, operation_name: &str) -> Vec<BackendType> {
384        match self.operations.read() {
385            Ok(ops) => {
386                if let Some(op) = ops.get(operation_name) {
387                    op.kernels
388                        .iter()
389                        .filter(|k| k.backend.is_available())
390                        .map(|k| k.backend)
391                        .collect()
392                } else {
393                    Vec::new()
394                }
395            }
396            Err(_) => Vec::new(),
397        }
398    }
399}
400
401/// Macro to simplify operation registration
402#[macro_export]
403macro_rules! register_operation {
404    ($registry:expr, $name:expr, $category:expr) => {
405        $registry.register_operation(
406            $crate::OperationDescriptor::new($name, $category)
407        ).expect("operation registration should succeed");
408    };
409    ($registry:expr, $name:expr, $category:expr, dtypes: [$($dtype:expr),*]) => {
410        $registry.register_operation(
411            $crate::OperationDescriptor::new($name, $category)
412                .with_dtypes(vec![$($dtype),*])
413        ).expect("operation registration with dtypes should succeed");
414    };
415    ($registry:expr, $name:expr, $category:expr, rank: $min:expr, $max:expr) => {
416        $registry.register_operation(
417            $crate::OperationDescriptor::new($name, $category)
418                .with_rank_range(Some($min), Some($max))
419        ).expect("operation registration with rank range should succeed");
420    };
421}
422
423/// Macro to register a unary kernel
424#[macro_export]
425macro_rules! register_unary_kernel {
426    ($registry:expr, $op_name:expr, $backend:expr, $func:expr) => {
427        $registry
428            .register_kernel(
429                $op_name,
430                $crate::KernelImplementation::unary($backend, $func),
431            )
432            .expect("unary kernel registration should succeed");
433    };
434}
435
436/// Macro to register a binary kernel
437#[macro_export]
438macro_rules! register_binary_kernel {
439    ($registry:expr, $op_name:expr, $backend:expr, $func:expr) => {
440        $registry
441            .register_kernel(
442                $op_name,
443                $crate::KernelImplementation::binary($backend, $func),
444            )
445            .expect("binary kernel registration should succeed");
446    };
447}
448
449/// Benchmark result for dispatch overhead measurements.
450///
451/// Produced by [`DispatchRegistry::benchmark_overhead`], which runs a fixed
452/// number of no-op dispatches and collects per-sample nanosecond timings.
453#[derive(Debug, Clone)]
454pub struct DispatchBenchmarkResult {
455    /// Minimum observed latency in nanoseconds.
456    pub min_ns: u64,
457    /// Maximum observed latency in nanoseconds.
458    pub max_ns: u64,
459    /// Arithmetic mean latency in nanoseconds (truncated to integer).
460    pub avg_ns: u64,
461    /// 95th-percentile latency in nanoseconds.
462    pub p95_ns: u64,
463    /// Number of samples collected.
464    pub sample_count: usize,
465}
466
467impl DispatchBenchmarkResult {
468    /// Build a `DispatchBenchmarkResult` from a **pre-sorted** slice of nanosecond samples.
469    ///
470    /// Returns `None` when `samples` is empty.
471    pub fn from_sorted_samples(samples: &[u64]) -> Option<Self> {
472        if samples.is_empty() {
473            return None;
474        }
475
476        let min_ns = *samples.first().unwrap_or(&0);
477        let max_ns = *samples.last().unwrap_or(&0);
478
479        let sum: u64 = samples.iter().sum();
480        let avg_ns = sum / samples.len() as u64;
481
482        // p95: index = floor(0.95 * n), clamped to valid range.
483        let p95_idx = ((samples.len() as f64 * 0.95) as usize).min(samples.len() - 1);
484        let p95_ns = samples[p95_idx];
485
486        Some(Self {
487            min_ns,
488            max_ns,
489            avg_ns,
490            p95_ns,
491            sample_count: samples.len(),
492        })
493    }
494}
495
496impl<T> DispatchRegistry<T> {
497    /// Measure the per-call overhead of a registry read-lock + no-op lookup.
498    ///
499    /// Runs 1 000 no-op dispatches ("__overhead_probe__"), collects the timing
500    /// for each call, sorts the samples, and returns the aggregated statistics.
501    ///
502    /// The probe operation is expected to be absent from the registry, so each
503    /// call exercises the read-lock acquisition plus a single failed hash-map
504    /// probe — which is the hot path for every real dispatch.
505    pub fn benchmark_overhead(&self) -> DispatchBenchmarkResult {
506        const SAMPLE_COUNT: usize = 1_000;
507        const PROBE_NAME: &str = "__overhead_probe__";
508
509        let mut samples: Vec<u64> = Vec::with_capacity(SAMPLE_COUNT);
510
511        for _ in 0..SAMPLE_COUNT {
512            let start = std::time::Instant::now();
513            // Perform a failed lookup to measure lock + probe cost.
514            let _ = self.get_operation(PROBE_NAME);
515            let elapsed_ns = start.elapsed().as_nanos() as u64;
516            samples.push(elapsed_ns);
517        }
518
519        samples.sort_unstable();
520
521        // SAFETY: samples is always non-empty (SAMPLE_COUNT > 0).
522        DispatchBenchmarkResult::from_sorted_samples(&samples).unwrap_or(DispatchBenchmarkResult {
523            min_ns: 0,
524            max_ns: 0,
525            avg_ns: 0,
526            p95_ns: 0,
527            sample_count: 0,
528        })
529    }
530}
531
532/// Global registry instance (lazily initialized)
533use lazy_static::lazy_static;
534
535lazy_static! {
536    /// Global f32 dispatch registry
537    pub static ref F32_REGISTRY: DispatchRegistry<f32> = DispatchRegistry::new();
538
539    /// Global f64 dispatch registry
540    pub static ref F64_REGISTRY: DispatchRegistry<f64> = DispatchRegistry::new();
541
542    /// Global i32 dispatch registry
543    pub static ref I32_REGISTRY: DispatchRegistry<i32> = DispatchRegistry::new();
544}
545
546/// Get the global registry for a specific type
547pub fn get_registry<T: 'static>() -> Option<&'static DispatchRegistry<T>> {
548    use std::any::TypeId;
549
550    let type_id = TypeId::of::<T>();
551
552    if type_id == TypeId::of::<f32>() {
553        // SAFETY: We've checked that T is f32
554        Some(unsafe {
555            &*(&*F32_REGISTRY as *const DispatchRegistry<f32> as *const DispatchRegistry<T>)
556        })
557    } else if type_id == TypeId::of::<f64>() {
558        // SAFETY: We've checked that T is f64
559        Some(unsafe {
560            &*(&*F64_REGISTRY as *const DispatchRegistry<f64> as *const DispatchRegistry<T>)
561        })
562    } else if type_id == TypeId::of::<i32>() {
563        // SAFETY: We've checked that T is i32
564        Some(unsafe {
565            &*(&*I32_REGISTRY as *const DispatchRegistry<i32> as *const DispatchRegistry<T>)
566        })
567    } else {
568        None
569    }
570}
571
572#[cfg(test)]
573mod tests {
574    use super::*;
575    use scirs2_core::ndarray::array;
576
577    #[test]
578    fn test_backend_type_priority() {
579        assert!(BackendType::Cpu.priority() < BackendType::Cpu.priority() + 1);
580
581        #[cfg(feature = "simd")]
582        assert!(BackendType::SimdCpu.priority() > BackendType::Cpu.priority());
583    }
584
585    #[test]
586    fn test_operation_descriptor() {
587        let desc = OperationDescriptor::new("test_op", "binary")
588            .with_dtypes(vec![crate::DType::Float32])
589            .with_broadcast()
590            .with_rank_range(Some(1), Some(4));
591
592        assert_eq!(desc.name, "test_op");
593        assert_eq!(desc.category, "binary");
594        assert!(desc.supports_broadcast);
595        assert_eq!(desc.min_rank, Some(1));
596        assert_eq!(desc.max_rank, Some(4));
597    }
598
599    #[test]
600    fn test_registry_creation() {
601        let registry: DispatchRegistry<f32> = DispatchRegistry::new();
602        assert_eq!(registry.list_operations().len(), 0);
603    }
604
605    #[test]
606    fn test_operation_registration() {
607        let registry: DispatchRegistry<f32> = DispatchRegistry::new();
608
609        let desc = OperationDescriptor::new("add", "binary");
610        registry
611            .register_operation(desc)
612            .expect("test: register_operation should succeed");
613
614        assert_eq!(registry.list_operations().len(), 1);
615        assert!(registry.get_operation("add").is_some());
616    }
617
618    #[test]
619    fn test_duplicate_registration_fails() {
620        let registry: DispatchRegistry<f32> = DispatchRegistry::new();
621
622        let desc1 = OperationDescriptor::new("add", "binary");
623        let desc2 = OperationDescriptor::new("add", "binary");
624
625        registry
626            .register_operation(desc1)
627            .expect("test: register_operation should succeed");
628        assert!(registry.register_operation(desc2).is_err());
629    }
630
631    #[test]
632    fn test_kernel_registration() {
633        let registry: DispatchRegistry<f32> = DispatchRegistry::new();
634
635        // Register operation
636        let desc = OperationDescriptor::new("abs", "unary");
637        registry
638            .register_operation(desc)
639            .expect("test: register_operation should succeed");
640
641        // Register CPU kernel
642        fn abs_cpu(x: &Tensor<f32>) -> Result<Tensor<f32>> {
643            let data = x.data();
644            let abs_data: Vec<f32> = data.iter().map(|v| v.abs()).collect();
645            let array = scirs2_core::ndarray::ArrayD::from_shape_vec(x.shape().dims(), abs_data)
646                .expect("test: operation should succeed");
647            Ok(Tensor::from_array(array))
648        }
649
650        let kernel = KernelImplementation::unary(BackendType::Cpu, abs_cpu);
651        registry
652            .register_kernel("abs", kernel)
653            .expect("test: register_kernel should succeed");
654
655        assert_eq!(registry.available_backends("abs").len(), 1);
656    }
657
658    #[test]
659    fn test_unary_dispatch() {
660        let registry: DispatchRegistry<f32> = DispatchRegistry::new();
661
662        // Register operation
663        let desc = OperationDescriptor::new("negate", "unary");
664        registry
665            .register_operation(desc)
666            .expect("test: register_operation should succeed");
667
668        // Register CPU kernel
669        fn negate_cpu(x: &Tensor<f32>) -> Result<Tensor<f32>> {
670            let data = x.data();
671            let neg_data: Vec<f32> = data.iter().map(|v| -v).collect();
672            let array = scirs2_core::ndarray::ArrayD::from_shape_vec(x.shape().dims(), neg_data)
673                .expect("test: operation should succeed");
674            Ok(Tensor::from_array(array))
675        }
676
677        let kernel = KernelImplementation::unary(BackendType::Cpu, negate_cpu);
678        registry
679            .register_kernel("negate", kernel)
680            .expect("test: register_kernel should succeed");
681
682        // Test dispatch
683        let input = Tensor::from_array(array![1.0f32, 2.0, 3.0].into_dyn());
684        let result = registry
685            .dispatch_unary("negate", &input)
686            .expect("test: dispatch_unary should succeed");
687
688        assert_eq!(result.data(), &[-1.0f32, -2.0, -3.0]);
689    }
690
691    #[test]
692    fn test_binary_dispatch() {
693        let registry: DispatchRegistry<f32> = DispatchRegistry::new();
694
695        // Register operation
696        let desc = OperationDescriptor::new("add", "binary");
697        registry
698            .register_operation(desc)
699            .expect("test: register_operation should succeed");
700
701        // Register CPU kernel
702        fn add_cpu(a: &Tensor<f32>, b: &Tensor<f32>) -> Result<Tensor<f32>> {
703            let a_data = a.data();
704            let b_data = b.data();
705            let sum_data: Vec<f32> = a_data
706                .iter()
707                .zip(b_data.iter())
708                .map(|(x, y)| x + y)
709                .collect();
710            let array = scirs2_core::ndarray::ArrayD::from_shape_vec(a.shape().dims(), sum_data)
711                .expect("test: operation should succeed");
712            Ok(Tensor::from_array(array))
713        }
714
715        let kernel = KernelImplementation::binary(BackendType::Cpu, add_cpu);
716        registry
717            .register_kernel("add", kernel)
718            .expect("test: register_kernel should succeed");
719
720        // Test dispatch
721        let a = Tensor::from_array(array![1.0f32, 2.0, 3.0].into_dyn());
722        let b = Tensor::from_array(array![4.0f32, 5.0, 6.0].into_dyn());
723        let result = registry
724            .dispatch_binary("add", &a, &b)
725            .expect("test: dispatch_binary should succeed");
726
727        assert_eq!(result.data(), &[5.0f32, 7.0, 9.0]);
728    }
729
730    #[test]
731    fn test_device_mismatch_error() {
732        let registry: DispatchRegistry<f32> = DispatchRegistry::new();
733
734        let desc = OperationDescriptor::new("add", "binary");
735        registry
736            .register_operation(desc)
737            .expect("test: register_operation should succeed");
738
739        fn add_cpu(a: &Tensor<f32>, b: &Tensor<f32>) -> Result<Tensor<f32>> {
740            Ok(a.clone())
741        }
742
743        let kernel = KernelImplementation::binary(BackendType::Cpu, add_cpu);
744        registry
745            .register_kernel("add", kernel)
746            .expect("test: register_kernel should succeed");
747
748        let a = Tensor::from_array(array![1.0f32].into_dyn());
749        let b = Tensor::from_array(array![2.0f32].into_dyn());
750
751        // Both on CPU, should work
752        let result = registry.dispatch_binary("add", &a, &b);
753        assert!(result.is_ok());
754    }
755
756    #[test]
757    fn test_global_registry_access() {
758        let registry = get_registry::<f32>();
759        assert!(registry.is_some());
760
761        let registry = get_registry::<f64>();
762        assert!(registry.is_some());
763
764        let registry = get_registry::<i32>();
765        assert!(registry.is_some());
766    }
767}