1use std::any::{Any, TypeId};
13use std::collections::HashMap;
14use std::fmt::Debug;
15
16use crate::array_protocol::{ArrayFunction, ArrayProtocol, GPUArray, NotImplemented};
17use crate::error::{CoreError, CoreResult, ErrorContext};
18use ::ndarray::{Array, Dimension};
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum GPUBackend {
23 CUDA,
25
26 ROCm,
28
29 Metal,
31
32 WebGPU,
34
35 OpenCL,
37}
38
39impl Default for GPUBackend {
40 fn default() -> Self {
41 Self::CUDA
42 }
43}
44
45#[derive(Debug, Clone)]
47pub struct GPUConfig {
48 pub backend: GPUBackend,
50
51 pub device_id: usize,
53
54 pub async_ops: bool,
56
57 pub mixed_precision: bool,
59
60 pub memory_fraction: f32,
62}
63
64impl Default for GPUConfig {
65 fn default() -> Self {
66 Self {
67 backend: GPUBackend::default(),
68 device_id: 0,
69 async_ops: true,
70 mixed_precision: false,
71 memory_fraction: 0.9,
72 }
73 }
74}
75
76pub struct GPUNdarray<T, D: Dimension>
78where
79 T: Clone + Send + Sync + 'static + num_traits::Zero,
80 T: std::ops::Div<f64, Output = T>,
81 D: Clone + Send + Sync + 'static + crate::ndarray::RemoveAxis,
82{
83 host_data: Array<T, D>,
85
86 config: GPUConfig,
88
89 on_gpu: bool,
91
92 id: String,
94}
95
96impl<T, D> Debug for GPUNdarray<T, D>
97where
98 T: Debug + Clone + Send + Sync + 'static + num_traits::Zero + std::ops::Div<f64, Output = T>,
99 D: Dimension + Debug + Clone + Send + Sync + 'static + crate::ndarray::RemoveAxis,
100{
101 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
102 f.debug_struct("GPUNdarray")
103 .field("config", &self.config)
104 .field("on_gpu", &self.on_gpu)
105 .field("id", &self.id)
106 .field("shape", &self.host_data.shape())
107 .finish()
108 }
109}
110
111impl<T, D> GPUNdarray<T, D>
112where
113 T: Clone + Send + Sync + 'static + num_traits::Zero + std::ops::Div<f64, Output = T>,
114 D: Dimension + Clone + Send + Sync + 'static + crate::ndarray::RemoveAxis,
115{
116 #[must_use]
118 pub fn new(hostdata: Array<T, D>, config: GPUConfig) -> Self {
119 let uuid = uuid::Uuid::new_v4();
120 let id = format!("uuid{uuid}");
121 let mut array = Self {
122 host_data: hostdata,
123 config,
124 on_gpu: false,
125 id,
126 };
127
128 array.on_gpu = true;
131
132 array
133 }
134
135 #[must_use]
137 pub fn shape(&self) -> &[usize] {
138 self.host_data.shape()
139 }
140
141 #[must_use]
143 pub const fn host_data(&self) -> &Array<T, D> {
144 &self.host_data
145 }
146
147 pub fn host_data_mut(&mut self) -> &mut Array<T, D> {
149 &mut self.host_data
151 }
152
153 #[must_use]
155 pub const fn config(&self) -> &GPUConfig {
156 &self.config
157 }
158
159 pub fn execute_kernel<F, R>(&self, kernel: F) -> CoreResult<R>
164 where
165 F: FnOnce(&Array<T, D>) -> CoreResult<R>,
166 {
167 kernel(&self.host_data)
170 }
171
172 pub fn sync_to_host(&mut self) -> CoreResult<()> {
177 Ok(())
180 }
181
182 pub fn sync_to_gpu(&mut self) -> CoreResult<()> {
187 self.on_gpu = true;
190 Ok(())
191 }
192}
193
194impl<T, D> ArrayProtocol for GPUNdarray<T, D>
195where
196 T: Clone + Send + Sync + 'static + num_traits::Zero,
197 T: std::ops::Div<f64, Output = T> + std::ops::Mul<Output = T> + std::ops::Add<Output = T>,
198 D: Dimension + Clone + Send + Sync + 'static + crate::ndarray::RemoveAxis,
199{
200 fn array_function(
201 &self,
202 func: &ArrayFunction,
203 _types: &[TypeId],
204 args: &[Box<dyn Any>],
205 kwargs: &HashMap<String, Box<dyn Any>>,
206 ) -> Result<Box<dyn Any>, NotImplemented> {
207 match func.name {
208 "scirs2::array_protocol::operations::sum" => {
209 let axis = kwargs.get("axis").and_then(|a| a.downcast_ref::<usize>());
212
213 if let Some(&_ax) = axis {
214 let sum = self.host_data.sum();
218 Ok(Box::new(sum))
219 } else {
220 let sum = self.host_data.sum();
222 Ok(Box::new(sum))
223 }
224 }
225 "scirs2::array_protocol::operations::mean" => {
226 let sum = self.host_data.sum();
228 let count = self.host_data.len();
229 #[allow(clippy::cast_precision_loss)]
230 let mean = sum / count as f64;
231
232 Ok(Box::new(mean))
233 }
234 "scirs2::array_protocol::operations::add" => {
235 if args.len() < 2 {
237 return Err(NotImplemented);
238 }
239
240 let other = super::downcast_array_function_arg::<Self>(args[1].as_ref());
245
246 if let Some(other) = other {
248 if self.shape() != other.shape() {
250 return Err(NotImplemented);
251 }
252
253 let Ok(result) = kernels::add(self, other) else {
255 return Err(NotImplemented);
256 };
257
258 return Ok(Box::new(Box::new(result) as Box<dyn ArrayProtocol>));
259 }
260
261 Err(NotImplemented)
264 }
265 "scirs2::array_protocol::operations::multiply" => {
266 if args.len() < 2 {
268 return Err(NotImplemented);
269 }
270
271 let other = super::downcast_array_function_arg::<Self>(args[1].as_ref());
272
273 if let Some(other) = other {
275 if self.shape() != other.shape() {
277 return Err(NotImplemented);
278 }
279
280 let Ok(result) = kernels::multiply(self, other) else {
282 return Err(NotImplemented);
283 };
284
285 return Ok(Box::new(Box::new(result) as Box<dyn ArrayProtocol>));
286 }
287
288 Err(NotImplemented)
291 }
292 "scirs2::array_protocol::operations::matmul" => {
293 if args.len() < 2 {
295 return Err(NotImplemented);
296 }
297
298 if TypeId::of::<D>() != TypeId::of::<crate::ndarray::Ix2>() {
302 return Err(NotImplemented);
303 }
304
305 let matmul_other = super::downcast_array_function_arg::<Self>(args[1].as_ref());
307 if let Some(other) = matmul_other {
308 if TypeId::of::<T>() == TypeId::of::<f64>()
311 && TypeId::of::<D>() == TypeId::of::<crate::ndarray::Ix2>()
312 {
313 let self_f64 = unsafe {
314 &*std::ptr::from_ref(self)
315 .cast::<GPUNdarray<f64, crate::ndarray::Ix2>>()
316 };
317 let other_f64 = unsafe {
318 &*std::ptr::from_ref(other)
319 .cast::<GPUNdarray<f64, crate::ndarray::Ix2>>()
320 };
321
322 match kernels::matmul(self_f64, other_f64) {
323 Ok(result) => {
324 return Ok(Box::new(Box::new(result) as Box<dyn ArrayProtocol>));
328 }
329 Err(_) => return Err(NotImplemented),
330 }
331 }
332 return Err(NotImplemented);
338 }
339
340 Err(NotImplemented)
341 }
342 "scirs2::array_protocol::operations::transpose" => {
343 if TypeId::of::<D>() != TypeId::of::<crate::ndarray::Ix2>() {
346 return Err(NotImplemented);
347 }
348
349 let transposed = self.host_data.t().to_owned();
352 let result = Self::new(transposed, self.config.clone());
353
354 Ok(Box::new(Box::new(result) as Box<dyn ArrayProtocol>))
355 }
356 "scirs2::array_protocol::operations::reshape" => {
357 if let Some(shape) = kwargs
359 .get("shape")
360 .and_then(|s| s.downcast_ref::<Vec<usize>>())
361 {
362 match self.host_data.clone().into_shape_with_order(shape.clone()) {
363 Ok(reshaped) => {
364 let result = GPUNdarray::new(reshaped, self.config.clone());
365 return Ok(Box::new(Box::new(result) as Box<dyn ArrayProtocol>));
366 }
367 Err(_) => return Err(NotImplemented),
368 }
369 }
370
371 Err(NotImplemented)
372 }
373 _ => Err(NotImplemented),
374 }
375 }
376
377 fn as_any(&self) -> &dyn Any {
378 self
379 }
380
381 fn shape(&self) -> &[usize] {
382 self.host_data.shape()
383 }
384
385 fn box_clone(&self) -> Box<dyn ArrayProtocol> {
386 Box::new(self.clone())
387 }
388}
389
390impl<T, D> GPUArray for GPUNdarray<T, D>
391where
392 T: Clone + Send + Sync + 'static + num_traits::Zero,
393 T: std::ops::Div<f64, Output = T> + std::ops::Mul<Output = T> + std::ops::Add<Output = T>,
394 D: Dimension + Clone + Send + Sync + 'static + crate::ndarray::RemoveAxis,
395{
396 fn to_gpu(&self) -> CoreResult<Box<dyn GPUArray>> {
399 Ok(Box::new(self.clone()))
401 }
402
403 fn to_cpu(&self) -> CoreResult<Box<dyn ArrayProtocol>> {
406 let array = super::NdarrayWrapper::new(self.host_data.clone());
408
409 Ok(Box::new(array) as Box<dyn ArrayProtocol>)
410 }
411
412 fn is_on_gpu(&self) -> bool {
413 self.on_gpu
414 }
415
416 fn device_info(&self) -> HashMap<String, String> {
417 let mut info = HashMap::new();
418 info.insert("backend".to_string(), format!("{:?}", self.config.backend));
419 info.insert("device_id".to_string(), self.config.device_id.to_string());
420 info.insert("on_gpu".to_string(), self.on_gpu.to_string());
421 info.insert("id".to_string(), self.id.clone());
422 info
423 }
424}
425
426impl<T, D> Clone for GPUNdarray<T, D>
427where
428 T: Clone + Send + Sync + 'static + num_traits::Zero,
429 T: std::ops::Div<f64, Output = T>,
430 D: Dimension + Clone + Send + Sync + 'static + crate::ndarray::RemoveAxis,
431{
432 fn clone(&self) -> Self {
433 Self {
434 host_data: self.host_data.clone(),
435 config: self.config.clone(),
436 on_gpu: self.on_gpu,
437 id: self.id.clone(),
438 }
439 }
440}
441
442pub struct GPUArrayBuilder {
444 config: GPUConfig,
445}
446
447impl Default for GPUArrayBuilder {
448 fn default() -> Self {
449 Self::new()
450 }
451}
452
453impl GPUArrayBuilder {
454 #[must_use]
456 pub fn new() -> Self {
457 Self {
458 config: GPUConfig::default(),
459 }
460 }
461
462 #[must_use]
464 pub const fn backend(mut self, backend: GPUBackend) -> Self {
465 self.config.backend = backend;
466 self
467 }
468
469 #[must_use]
471 pub const fn device_id(mut self, device_id: usize) -> Self {
472 self.config.device_id = device_id;
473 self
474 }
475
476 #[must_use]
478 pub const fn async_ops(mut self, asyncops: bool) -> Self {
479 self.config.async_ops = asyncops;
480 self
481 }
482
483 #[must_use]
485 pub const fn mixed_precision(mut self, mixedprecision: bool) -> Self {
486 self.config.mixed_precision = mixedprecision;
487 self
488 }
489
490 #[must_use]
492 pub const fn memory_fraction(mut self, memoryfraction: f32) -> Self {
493 self.config.memory_fraction = memoryfraction;
494 self
495 }
496
497 #[must_use]
499 pub fn build<T, D>(self, hostdata: Array<T, D>) -> GPUNdarray<T, D>
500 where
501 T: Clone + Send + Sync + 'static + num_traits::Zero + std::ops::Div<f64, Output = T>,
502 D: Dimension + Clone + Send + Sync + 'static + crate::ndarray::RemoveAxis,
503 {
504 GPUNdarray::new(hostdata, self.config)
505 }
506}
507
508pub mod kernels {
510 use super::*;
511 use ::ndarray::{Array, Dimension};
512
513 pub fn add<T, D>(a: &GPUNdarray<T, D>, b: &GPUNdarray<T, D>) -> CoreResult<GPUNdarray<T, D>>
518 where
519 T: Clone
520 + std::ops::Add<Output = T>
521 + Send
522 + Sync
523 + 'static
524 + num_traits::Zero
525 + std::ops::Div<f64, Output = T>,
526 D: Dimension + Clone + Send + Sync + 'static + crate::ndarray::RemoveAxis,
527 {
528 if a.shape() != b.shape() {
533 return Err(CoreError::ShapeError(ErrorContext::new(format!(
534 "Shape mismatch: {:?} vs {:?}",
535 a.shape(),
536 b.shape()
537 ))));
538 }
539
540 let result_data = a.host_data().clone() + b.host_data().clone();
542
543 Ok(GPUNdarray::<T, D>::new(result_data, a.config.clone()))
545 }
546
547 pub fn multiply<T, D>(
552 a: &GPUNdarray<T, D>,
553 b: &GPUNdarray<T, D>,
554 ) -> CoreResult<GPUNdarray<T, D>>
555 where
556 T: Clone
557 + std::ops::Mul<Output = T>
558 + Send
559 + Sync
560 + 'static
561 + num_traits::Zero
562 + std::ops::Div<f64, Output = T>,
563 D: Dimension + Clone + Send + Sync + 'static + crate::ndarray::RemoveAxis,
564 {
565 if a.shape() != b.shape() {
570 return Err(CoreError::ShapeError(ErrorContext::new(format!(
571 "Shape mismatch: {:?} vs {:?}",
572 a.shape(),
573 b.shape()
574 ))));
575 }
576
577 let result_data = a.host_data().clone() * b.host_data().clone();
579
580 Ok(GPUNdarray::<T, D>::new(result_data, a.config.clone()))
582 }
583
584 pub fn matmul<T>(
589 a: &GPUNdarray<T, crate::ndarray::Ix2>,
590 b: &GPUNdarray<T, crate::ndarray::Ix2>,
591 ) -> CoreResult<GPUNdarray<T, crate::ndarray::Ix2>>
592 where
593 T: Clone
594 + std::ops::Mul<Output = T>
595 + std::ops::Add<Output = T>
596 + Default
597 + Send
598 + Sync
599 + 'static
600 + num_traits::Zero
601 + std::ops::Div<f64, Output = T>,
602 {
603 let ashape = a.shape();
608 let bshape = b.shape();
609
610 if ashape.len() != 2 || bshape.len() != 2 || ashape[1] != bshape[0] {
611 return Err(CoreError::ShapeError(ErrorContext::new(format!(
612 "Incompatible shapes for matmul: {ashape:?} vs {bshape:?}"
613 ))));
614 }
615
616 let m = ashape[0];
621 let k = ashape[1];
622 let p = bshape[1];
623
624 let a_host = a.host_data();
625 let b_host = b.host_data();
626 let mut result_data = Array::<T, crate::ndarray::Ix2>::default((m, p));
627 for i in 0..m {
628 for j in 0..p {
629 let mut sum = T::zero();
630 for l in 0..k {
631 sum = sum + a_host[[i, l]].clone() * b_host[[l, j]].clone();
632 }
633 result_data[[i, j]] = sum;
634 }
635 }
636
637 Ok(GPUNdarray::<T, crate::ndarray::Ix2>::new(
639 result_data,
640 a.config.clone(),
641 ))
642 }
643}
644
645#[cfg(test)]
646mod tests {
647 use super::*;
648 use ::ndarray::{arr2, Array2};
649
650 #[test]
651 fn test_gpu_ndarray_creation() {
652 let array = Array2::<f64>::ones((10, 5));
653 let config = GPUConfig::default();
654
655 let gpu_array = GPUNdarray::new(array.clone(), config);
656
657 assert_eq!(gpu_array.shape(), &[10, 5]);
659 assert!(gpu_array.is_on_gpu());
660
661 let info = gpu_array.device_info();
663 assert_eq!(info.get("backend").expect("Operation failed"), "CUDA");
664 assert_eq!(info.get("device_id").expect("Operation failed"), "0");
665 assert_eq!(info.get("on_gpu").expect("Operation failed"), "true");
666 }
667
668 #[test]
669 fn test_gpu_array_builder() {
670 let array = Array2::<f64>::ones((10, 5));
671
672 let gpu_array = GPUArrayBuilder::new()
673 .backend(GPUBackend::CUDA)
674 .device_id(1)
675 .async_ops(true)
676 .mixed_precision(true)
677 .memory_fraction(0.8)
678 .build(array.clone());
679
680 assert_eq!(gpu_array.config.backend, GPUBackend::CUDA);
682 assert_eq!(gpu_array.config.device_id, 1);
683 assert!(gpu_array.config.async_ops);
684 assert!(gpu_array.config.mixed_precision);
685 assert_eq!(gpu_array.config.memory_fraction, 0.8);
686 }
687
688 #[test]
689 fn test_gpu_array_kernels() {
690 let a = arr2(&[[1.0, 2.0], [3.0, 4.0]]);
691 let b = arr2(&[[5.0, 6.0], [7.0, 8.0]]);
692
693 let gpu_a = GPUNdarray::new(a.clone(), GPUConfig::default());
694 let gpu_b = GPUNdarray::new(b.clone(), GPUConfig::default());
695
696 let result = kernels::add(&gpu_a, &gpu_b).expect("Operation failed");
698 let expected = a + b;
699 assert_eq!(result.host_data(), &expected);
700
701 let a = arr2(&[[1.0, 2.0], [3.0, 4.0]]);
703 let b = arr2(&[[5.0, 6.0], [7.0, 8.0]]);
704
705 let gpu_a = GPUNdarray::new(a.clone(), GPUConfig::default());
706 let gpu_b = GPUNdarray::new(b.clone(), GPUConfig::default());
707
708 let result = kernels::multiply(&gpu_a, &gpu_b).expect("Operation failed");
709 let expected = a * b;
710 assert_eq!(result.host_data(), &expected);
711 }
712
713 #[test]
718 fn test_gpu_kernel_matmul_computes_real_product() {
719 let a = arr2(&[[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]);
720 let b = arr2(&[[7.0, 8.0], [9.0, 10.0], [11.0, 12.0]]);
721
722 let gpu_a = GPUNdarray::new(a.clone(), GPUConfig::default());
723 let gpu_b = GPUNdarray::new(b.clone(), GPUConfig::default());
724
725 let result = kernels::matmul(&gpu_a, &gpu_b).expect("matmul should succeed");
726 let expected = a.dot(&b);
727
728 assert_eq!(result.shape(), &[2, 2]);
729 assert_eq!(result.host_data(), &expected);
730 assert!(result.host_data().iter().any(|&v| v != 0.0));
733 }
734}