scirs2_core/gpu/backends/
mod.rs1use crate::gpu::{GpuBackend, GpuError};
7use std::process::Command;
8
9#[cfg(all(target_os = "macos", feature = "serialization"))]
10use serde_json;
11
12#[cfg(feature = "validation")]
13use regex::Regex;
14
15#[cfg(feature = "opencl")]
19pub mod opencl;
20
21#[cfg(feature = "wgpu")]
22pub mod wgpu;
23
24#[cfg(all(feature = "metal", target_os = "macos"))]
25pub mod metal;
26
27#[cfg(all(feature = "metal", target_os = "macos"))]
28pub mod metal_mps;
29
30#[cfg(all(feature = "metal", target_os = "macos"))]
35pub mod msl_kernels;
36
37#[cfg(all(feature = "mpsgraph", target_os = "macos"))]
38pub mod metal_mpsgraph;
39
40#[cfg(feature = "opencl")]
42pub use opencl::OpenCLContext;
43
44#[cfg(feature = "wgpu")]
45pub use wgpu::{run_vector_add_wgsl, try_compile_wgsl, WebGPUContext, WgpuComputePipeline};
46
47#[cfg(all(feature = "metal", target_os = "macos"))]
48pub use metal::{MetalBufferOptions, MetalContext, MetalStorageMode};
49
50#[cfg(all(feature = "metal", target_os = "macos"))]
51pub use metal_mps::{MPSContext, MPSDataType, MPSOperations};
52
53#[cfg(all(feature = "mpsgraph", target_os = "macos"))]
54pub use metal_mpsgraph::MPSGraphContext;
55
56#[derive(Debug, Clone)]
58pub struct GpuInfo {
59 pub backend: GpuBackend,
61 pub device_name: String,
63 pub memory_bytes: Option<u64>,
65 pub compute_capability: Option<String>,
67 pub supports_tensors: bool,
69}
70
71#[derive(Debug, Clone)]
73pub struct GpuDetectionResult {
74 pub devices: Vec<GpuInfo>,
76 pub recommended_backend: GpuBackend,
78}
79
80#[allow(dead_code)]
82pub fn detect_gpu_backends() -> GpuDetectionResult {
83 let mut devices = Vec::new();
84
85 #[cfg(not(test))]
87 {
88 if let Ok(cuda_devices) = detect_cuda_devices() {
90 devices.extend(cuda_devices);
91 }
92
93 if let Ok(rocm_devices) = detect_rocm_devices() {
95 devices.extend(rocm_devices);
96 }
97
98 #[cfg(target_os = "macos")]
100 if let Ok(metal_devices) = detect_metal_devices() {
101 devices.extend(metal_devices);
102 }
103
104 if let Ok(opencl_devices) = detect_opencl_devices() {
106 devices.extend(opencl_devices);
107 }
108 }
109
110 let recommended_backend = if devices
112 .iter()
113 .any(|d: &GpuInfo| d.backend == GpuBackend::Cuda)
114 {
115 GpuBackend::Cuda
116 } else if devices
117 .iter()
118 .any(|d: &GpuInfo| d.backend == GpuBackend::Rocm)
119 {
120 GpuBackend::Rocm
121 } else if devices
122 .iter()
123 .any(|d: &GpuInfo| d.backend == GpuBackend::Metal)
124 {
125 GpuBackend::Metal
126 } else if devices
127 .iter()
128 .any(|d: &GpuInfo| d.backend == GpuBackend::OpenCL)
129 {
130 GpuBackend::OpenCL
131 } else {
132 GpuBackend::Cpu
133 };
134
135 devices.push(GpuInfo {
137 backend: GpuBackend::Cpu,
138 device_name: "CPU".to_string(),
139 memory_bytes: None,
140 compute_capability: None,
141 supports_tensors: false,
142 });
143
144 GpuDetectionResult {
145 devices,
146 recommended_backend,
147 }
148}
149
150#[allow(dead_code)]
152fn detect_rocm_devices() -> Result<Vec<GpuInfo>, GpuError> {
153 let mut devices = Vec::new();
154
155 match Command::new("rocm-smi")
157 .arg("--showproductname")
158 .arg("--showmeminfo")
159 .arg("vram")
160 .arg("--csv")
161 .output()
162 {
163 Ok(output) if output.status.success() => {
164 let output_str = String::from_utf8_lossy(&output.stdout);
165
166 for line in output_str.lines().skip(1) {
167 if line.trim().is_empty() {
169 continue;
170 }
171
172 let parts: Vec<&str> = line.split(',').map(|s| s.trim()).collect();
173 if parts.len() >= 3 {
174 let device_name = parts[1].trim_matches('"').to_string();
175 let memory_str = parts[2].trim_matches('"');
176
177 let memory_mb = memory_str
179 .split_whitespace()
180 .next()
181 .and_then(|s| s.parse::<u64>().ok())
182 .unwrap_or(0)
183 * 1024
184 * 1024; devices.push(GpuInfo {
187 backend: GpuBackend::Rocm,
188 device_name,
189 memory_bytes: Some(memory_mb),
190 compute_capability: Some("RDNA/CDNA".to_string()),
191 supports_tensors: true, });
193 }
194 }
195 }
196 _ => {
197 }
203 }
204
205 if devices.is_empty() {
206 Err(GpuError::BackendNotAvailable("ROCm".to_string()))
207 } else {
208 Ok(devices)
209 }
210}
211
212#[allow(dead_code)]
214fn detect_cuda_devices() -> Result<Vec<GpuInfo>, GpuError> {
215 let mut devices = Vec::new();
216
217 match Command::new("nvidia-smi")
219 .arg("--query-gpu=name,memory.total,compute_cap")
220 .arg("--format=csv,noheader,nounits")
221 .output()
222 {
223 Ok(output) if output.status.success() => {
224 let output_str = String::from_utf8_lossy(&output.stdout);
225
226 for line in output_str.lines() {
227 if line.trim().is_empty() {
228 continue;
229 }
230
231 let parts: Vec<&str> = line.split(',').map(|s| s.trim()).collect();
232 if parts.len() >= 3 {
233 let device_name = parts[0].to_string();
234 let memory_mb = parts[1].parse::<u64>().unwrap_or(0) * 1024 * 1024; let compute_capability = parts[2].to_string();
236
237 let supports_tensors =
239 if let Some(major_str) = compute_capability.split('.').next() {
240 major_str.parse::<u32>().unwrap_or(0) >= 7 } else {
242 false
243 };
244
245 devices.push(GpuInfo {
246 backend: GpuBackend::Cuda,
247 device_name,
248 memory_bytes: Some(memory_mb),
249 compute_capability: Some(compute_capability),
250 supports_tensors,
251 });
252 }
253 }
254 }
255 _ => {
256 }
262 }
263
264 if devices.is_empty() {
265 Err(GpuError::BackendNotAvailable("CUDA".to_string()))
266 } else {
267 Ok(devices)
268 }
269}
270
271#[cfg(target_os = "macos")]
273#[allow(dead_code)]
274fn detect_metal_devices() -> Result<Vec<GpuInfo>, GpuError> {
275 let mut devices = Vec::new();
276
277 match Command::new("system_profiler")
279 .arg("SPDisplaysDataType")
280 .arg("-json")
281 .output()
282 {
283 Ok(output) if output.status.success() => {
284 #[cfg(feature = "serialization")]
286 {
287 use std::str::FromStr;
288 let output_str = String::from_utf8_lossy(&output.stdout);
289
290 if let Ok(json_value) = serde_json::Value::from_str(&output_str) {
291 if let Some(displays) = json_value
292 .get("SPDisplaysDataType")
293 .and_then(|v: &serde_json::Value| v.as_array())
294 {
295 #[cfg(feature = "validation")]
297 let vram_regex = Regex::new(r"(\d+)\s*(GB|MB)").ok();
298
299 for display in displays {
300 if let Some(model) = display
302 .get("sppci_model")
303 .and_then(|v: &serde_json::Value| v.as_str())
304 {
305 let mut gpu_info = GpuInfo {
306 backend: GpuBackend::Metal,
307 device_name: model.to_string(),
308 memory_bytes: None,
309 compute_capability: None,
310 supports_tensors: true,
311 };
312
313 if let Some(vram_str) = display
315 .get("vram_pcie")
316 .and_then(|v: &serde_json::Value| v.as_str())
317 .or_else(|| {
318 display
319 .get("vram")
320 .and_then(|v: &serde_json::Value| v.as_str())
321 })
322 {
323 #[cfg(feature = "validation")]
325 if let Some(captures) =
326 vram_regex.as_ref().and_then(|re| re.captures(vram_str))
327 {
328 if let (Some(value), Some(unit)) =
329 (captures.get(1), captures.get(2))
330 {
331 if let Ok(num) = u64::from_str(value.as_str()) {
332 gpu_info.memory_bytes = Some(match unit.as_str() {
333 "GB" => num * 1024 * 1024 * 1024,
334 "MB" => num * 1024 * 1024,
335 _ => 0,
336 });
337 }
338 }
339 }
340 }
341
342 if let Some(metal_family) = display
344 .get("sppci_metal_family")
345 .and_then(|v: &serde_json::Value| v.as_str())
346 {
347 gpu_info.compute_capability = Some(metal_family.to_string());
348 }
349
350 devices.push(gpu_info);
351 }
352 }
353 }
354 }
355 }
356
357 if devices.is_empty() {
359 #[cfg(feature = "metal")]
361 {
362 use metal::Device;
363 if let Some(device) = Device::system_default() {
364 let name = device.name().to_string();
365 let mut gpu_info = GpuInfo {
366 backend: GpuBackend::Metal,
367 device_name: name.clone(),
368 memory_bytes: None,
369 compute_capability: None,
370 supports_tensors: true,
371 };
372
373 gpu_info.compute_capability = Some("Metal GPU".to_string());
376
377 devices.push(gpu_info);
378 }
379 }
380
381 #[cfg(not(feature = "metal"))]
383 {
384 devices.push(GpuInfo {
385 backend: GpuBackend::Metal,
386 device_name: "Metal GPU".to_string(),
387 memory_bytes: None,
388 compute_capability: None,
389 supports_tensors: true,
390 });
391 }
392 }
393 }
394 _ => {
395 #[cfg(feature = "metal")]
397 {
398 use metal::Device;
399 if let Some(device) = Device::system_default() {
400 devices.push(GpuInfo {
401 backend: GpuBackend::Metal,
402 device_name: device.name().to_string(),
403 memory_bytes: None,
404 compute_capability: None,
405 supports_tensors: true,
406 });
407 } else {
408 return Err(GpuError::BackendNotAvailable("Metal".to_string()));
409 }
410 }
411
412 #[cfg(not(feature = "metal"))]
413 {
414 return Err(GpuError::BackendNotAvailable("Metal".to_string()));
415 }
416 }
417 }
418
419 if devices.is_empty() {
420 Err(GpuError::BackendNotAvailable("Metal".to_string()))
421 } else {
422 Ok(devices)
423 }
424}
425
426#[cfg(not(target_os = "macos"))]
428#[allow(dead_code)]
429fn detect_metal_devices() -> Result<Vec<GpuInfo>, GpuError> {
430 Err(GpuError::BackendNotAvailable(
431 "Metal (not macOS)".to_string(),
432 ))
433}
434
435#[allow(dead_code)]
437fn detect_opencl_devices() -> Result<Vec<GpuInfo>, GpuError> {
438 let mut devices = Vec::new();
439
440 match Command::new("clinfo").arg("--list").output() {
442 Ok(output) if output.status.success() => {
443 let output_str = String::from_utf8_lossy(&output.stdout);
444
445 for line in output_str.lines() {
446 if line.trim().starts_with("Platform") || line.trim().starts_with("Device") {
447 devices.push(GpuInfo {
450 backend: GpuBackend::OpenCL,
451 device_name: "OpenCL Device".to_string(),
452 memory_bytes: None,
453 compute_capability: None,
454 supports_tensors: false,
455 });
456 break; }
458 }
459 }
460 _ => {
461 return Err(GpuError::BackendNotAvailable("OpenCL".to_string()));
462 }
463 }
464
465 if devices.is_empty() {
466 Err(GpuError::BackendNotAvailable("OpenCL".to_string()))
467 } else {
468 Ok(devices)
469 }
470}
471
472#[allow(dead_code)]
474pub fn check_backend_installation(backend: GpuBackend) -> Result<bool, GpuError> {
475 match backend {
476 GpuBackend::Cuda => {
477 match Command::new("nvcc").arg("--version").output() {
479 Ok(output) if output.status.success() => Ok(true),
480 _ => Ok(false),
481 }
482 }
483 GpuBackend::Rocm => {
484 match Command::new("hipcc").arg("--version").output() {
486 Ok(output) if output.status.success() => Ok(true),
487 _ => {
488 match Command::new("rocm-smi").arg("--version").output() {
490 Ok(output) if output.status.success() => Ok(true),
491 _ => Ok(false),
492 }
493 }
494 }
495 }
496 GpuBackend::Metal => {
497 #[cfg(target_os = "macos")]
498 {
499 Ok(true)
501 }
502 #[cfg(not(target_os = "macos"))]
503 {
504 Ok(false)
505 }
506 }
507 GpuBackend::OpenCL => {
508 match Command::new("clinfo").output() {
510 Ok(output) if output.status.success() => Ok(true),
511 _ => Ok(false),
512 }
513 }
514 GpuBackend::Wgpu => {
515 Ok(true)
517 }
518 GpuBackend::Cpu => Ok(true),
519 }
520}
521
522#[allow(dead_code)]
524pub fn get_device_info(backend: GpuBackend, device_id: usize) -> Result<GpuInfo, GpuError> {
525 let detection_result = detect_gpu_backends();
526
527 detection_result
528 .devices
529 .into_iter()
530 .filter(|d| d.backend == backend)
531 .nth(device_id)
532 .ok_or_else(|| {
533 GpuError::InvalidParameter(format!(
534 "Device {device_id} not found for backend {:?}",
535 backend
536 ))
537 })
538}
539
540#[allow(dead_code)]
542pub fn initialize_optimal_backend() -> Result<GpuBackend, GpuError> {
543 let detection_result = detect_gpu_backends();
544
545 let preference_order = [
547 GpuBackend::Cuda, GpuBackend::Rocm, GpuBackend::Metal, GpuBackend::OpenCL, GpuBackend::Wgpu, GpuBackend::Cpu, ];
554
555 for backend in preference_order.iter() {
556 if detection_result
557 .devices
558 .iter()
559 .any(|d: &GpuInfo| d.backend == *backend)
560 {
561 return Ok(*backend);
562 }
563 }
564
565 Ok(GpuBackend::Cpu)
567}
568
569#[cfg(test)]
570mod tests {
571 use super::*;
572
573 #[test]
574 fn test_gpu_info_creation() {
575 let info = GpuInfo {
576 backend: GpuBackend::Cuda,
577 device_name: "NVIDIA GeForce RTX 3080".to_string(),
578 memory_bytes: Some(10 * 1024 * 1024 * 1024), compute_capability: Some("8.6".to_string()),
580 supports_tensors: true,
581 };
582
583 assert_eq!(info.backend, GpuBackend::Cuda);
584 assert_eq!(info.device_name, "NVIDIA GeForce RTX 3080");
585 assert_eq!(info.memory_bytes, Some(10 * 1024 * 1024 * 1024));
586 assert_eq!(info.compute_capability, Some("8.6".to_string()));
587 assert!(info.supports_tensors);
588 }
589
590 #[test]
591 fn test_gpu_detection_result_with_cpu_fallback() {
592 let result = detect_gpu_backends();
593
594 assert!(!result.devices.is_empty());
596 assert!(result
597 .devices
598 .iter()
599 .any(|d: &GpuInfo| d.backend == GpuBackend::Cpu));
600
601 match result.recommended_backend {
603 GpuBackend::Cuda
604 | GpuBackend::Rocm
605 | GpuBackend::Metal
606 | GpuBackend::OpenCL
607 | GpuBackend::Cpu => {}
608 _ => panic!("Unexpected recommended backend"),
609 }
610 }
611
612 #[test]
613 fn test_check_backend_installation_cpu() {
614 let result = check_backend_installation(GpuBackend::Cpu).expect("Operation failed");
616 assert!(result);
617 }
618
619 #[test]
620 fn test_check_backend_installation_wgpu() {
621 let result = check_backend_installation(GpuBackend::Wgpu).expect("Operation failed");
623 assert!(result);
624 }
625
626 #[test]
627 fn test_check_backend_installation_metal() {
628 let result = check_backend_installation(GpuBackend::Metal).expect("Operation failed");
629 #[cfg(target_os = "macos")]
630 assert!(result);
631 #[cfg(not(target_os = "macos"))]
632 assert!(!result);
633 }
634
635 #[test]
636 fn test_initialize_optimal_backend() {
637 let backend = initialize_optimal_backend().expect("Operation failed");
638
639 match backend {
641 GpuBackend::Cuda
642 | GpuBackend::Rocm
643 | GpuBackend::Wgpu
644 | GpuBackend::Metal
645 | GpuBackend::OpenCL
646 | GpuBackend::Cpu => {}
647 }
648 }
649
650 #[test]
651 fn test_get_device_info_invalid_device() {
652 let result = get_device_info(GpuBackend::Cpu, 100);
654
655 assert!(result.is_err());
656 match result {
657 Err(GpuError::InvalidParameter(_)) => {}
658 _ => panic!("Expected InvalidParameter error"),
659 }
660 }
661
662 #[test]
663 fn test_get_device_info_cpu() {
664 let result = get_device_info(GpuBackend::Cpu, 0);
666
667 assert!(result.is_ok());
668 let info = result.expect("Operation failed");
669 assert_eq!(info.backend, GpuBackend::Cpu);
670 assert_eq!(info.device_name, "CPU");
671 assert!(!info.supports_tensors);
672 }
673
674 #[test]
675 fn test_detect_metal_devices_non_macos() {
676 #[cfg(not(target_os = "macos"))]
677 {
678 let result = detect_metal_devices();
679 assert!(result.is_err());
680 match result {
681 Err(GpuError::BackendNotAvailable(_)) => {}
682 _ => panic!("Expected BackendNotAvailable error"),
683 }
684 }
685 }
686
687 #[test]
688 fn test_gpu_info_clone() {
689 let info = GpuInfo {
690 backend: GpuBackend::Rocm,
691 device_name: "AMD Radeon RX 6900 XT".to_string(),
692 memory_bytes: Some(16 * 1024 * 1024 * 1024), compute_capability: Some("RDNA2".to_string()),
694 supports_tensors: true,
695 };
696
697 let cloned = info.clone();
698 assert_eq!(info.backend, cloned.backend);
699 assert_eq!(info.device_name, cloned.device_name);
700 assert_eq!(info.memory_bytes, cloned.memory_bytes);
701 assert_eq!(info.compute_capability, cloned.compute_capability);
702 assert_eq!(info.supports_tensors, cloned.supports_tensors);
703 }
704
705 #[test]
706 fn test_gpu_detection_result_clone() {
707 let devices = vec![
708 GpuInfo {
709 backend: GpuBackend::Cuda,
710 device_name: "NVIDIA A100".to_string(),
711 memory_bytes: Some(40 * 1024 * 1024 * 1024),
712 compute_capability: Some("8.0".to_string()),
713 supports_tensors: true,
714 },
715 GpuInfo {
716 backend: GpuBackend::Cpu,
717 device_name: "CPU".to_string(),
718 memory_bytes: None,
719 compute_capability: None,
720 supports_tensors: false,
721 },
722 ];
723
724 let result = GpuDetectionResult {
725 devices: devices.clone(),
726 recommended_backend: GpuBackend::Cuda,
727 };
728
729 let cloned = result.clone();
730 assert_eq!(result.devices.len(), cloned.devices.len());
731 assert_eq!(result.recommended_backend, cloned.recommended_backend);
732 }
733
734 #[test]
736 fn test_detect_cuda_deviceserror_handling() {
737 let _ = detect_cuda_devices();
741 }
742
743 #[test]
744 fn test_detect_rocm_deviceserror_handling() {
745 let _ = detect_rocm_devices();
747 }
748
749 #[test]
750 fn test_detect_opencl_deviceserror_handling() {
751 let _ = detect_opencl_devices();
753 }
754
755 #[test]
756 fn test_backend_preference_order() {
757 let result = detect_gpu_backends();
759
760 if result
762 .devices
763 .iter()
764 .any(|d: &GpuInfo| d.backend == GpuBackend::Cuda)
765 {
766 let optimal = initialize_optimal_backend().expect("Operation failed");
768 if result
769 .devices
770 .iter()
771 .filter(|d| d.backend == GpuBackend::Cuda)
772 .count()
773 > 0
774 {
775 assert_eq!(optimal, GpuBackend::Cuda);
776 }
777 }
778 }
779}