Skip to main content

scirs2_core/gpu/backends/
mod.rs

1//! GPU backend implementations and detection utilities
2//!
3//! This module contains backend-specific implementations for various GPU platforms
4//! and utilities for detecting available GPU backends.
5
6use 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// Backend implementation modules
16// NOTE: The cudarc-based CUDA backend was retired from scirs2-core in 0.6.x.
17// CUDA acceleration now lives in the per-crate oxicuda-* backends (see SCIRS2_POLICY.md).
18#[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/// MSL compute kernel source strings for the Metal backend.
31///
32/// Each constant is a complete Metal Shading Language kernel that can be
33/// compiled at runtime by `MetalContext::create_compute_pipeline`.
34#[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// Re-export backend implementations
41#[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/// Information about available GPU hardware
57#[derive(Debug, Clone)]
58pub struct GpuInfo {
59    /// The GPU backend type
60    pub backend: GpuBackend,
61    /// Device name
62    pub device_name: String,
63    /// Available memory in bytes
64    pub memory_bytes: Option<u64>,
65    /// Compute capability or equivalent
66    pub compute_capability: Option<String>,
67    /// Whether the device supports tensor operations
68    pub supports_tensors: bool,
69}
70
71/// Detection results for all available GPU backends
72#[derive(Debug, Clone)]
73pub struct GpuDetectionResult {
74    /// Available GPU devices
75    pub devices: Vec<GpuInfo>,
76    /// Recommended backend for scientific computing
77    pub recommended_backend: GpuBackend,
78}
79
80/// Detect available GPU backends and devices
81#[allow(dead_code)]
82pub fn detect_gpu_backends() -> GpuDetectionResult {
83    let mut devices = Vec::new();
84
85    // Skip GPU detection in test environment to avoid segfaults from external commands
86    #[cfg(not(test))]
87    {
88        // Detect CUDA devices
89        if let Ok(cuda_devices) = detect_cuda_devices() {
90            devices.extend(cuda_devices);
91        }
92
93        // Detect ROCm devices
94        if let Ok(rocm_devices) = detect_rocm_devices() {
95            devices.extend(rocm_devices);
96        }
97
98        // Detect Metal devices (macOS)
99        #[cfg(target_os = "macos")]
100        if let Ok(metal_devices) = detect_metal_devices() {
101            devices.extend(metal_devices);
102        }
103
104        // Detect OpenCL devices
105        if let Ok(opencl_devices) = detect_opencl_devices() {
106            devices.extend(opencl_devices);
107        }
108    }
109
110    // Determine recommended backend
111    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    // Always add CPU fallback
136    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/// Detect ROCm devices using rocm-smi
151#[allow(dead_code)]
152fn detect_rocm_devices() -> Result<Vec<GpuInfo>, GpuError> {
153    let mut devices = Vec::new();
154
155    // Try to run rocm-smi to detect ROCm devices
156    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                // Skip header line
168                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                    // Parse memory (format might be like "16368 MB")
178                    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; // Convert MB to bytes
185
186                    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, // Modern AMD GPUs support matrix operations
192                    });
193                }
194            }
195        }
196        _ => {
197            // rocm-smi not available or failed
198            // In a real implementation, we could try other methods like:
199            // - Direct HIP runtime API calls
200            // - /sys/class/drm/cardX/ on Linux
201            // - rocminfo command
202        }
203    }
204
205    if devices.is_empty() {
206        Err(GpuError::BackendNotAvailable("ROCm".to_string()))
207    } else {
208        Ok(devices)
209    }
210}
211
212/// Detect CUDA devices using nvidia-ml-py or nvidia-smi
213#[allow(dead_code)]
214fn detect_cuda_devices() -> Result<Vec<GpuInfo>, GpuError> {
215    let mut devices = Vec::new();
216
217    // Try to run nvidia-smi to detect CUDA devices
218    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; // Convert MB to bytes
235                    let compute_capability = parts[2].to_string();
236
237                    // Parse compute capability to determine tensor core support
238                    let supports_tensors =
239                        if let Some(major_str) = compute_capability.split('.').next() {
240                            major_str.parse::<u32>().unwrap_or(0) >= 7 // Tensor cores available on Volta+ (7.0+)
241                        } 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            // nvidia-smi not available or failed
257            // In a real implementation, we could try other methods like:
258            // - Direct CUDA runtime API calls
259            // - nvidia-ml-py if available
260            // - /proc/driver/nvidia/gpus/ on Linux
261        }
262    }
263
264    if devices.is_empty() {
265        Err(GpuError::BackendNotAvailable("CUDA".to_string()))
266    } else {
267        Ok(devices)
268    }
269}
270
271/// Detect Metal devices (macOS only)
272#[cfg(target_os = "macos")]
273#[allow(dead_code)]
274fn detect_metal_devices() -> Result<Vec<GpuInfo>, GpuError> {
275    let mut devices = Vec::new();
276
277    // Try to detect Metal devices using system_profiler
278    match Command::new("system_profiler")
279        .arg("SPDisplaysDataType")
280        .arg("-json")
281        .output()
282    {
283        Ok(output) if output.status.success() => {
284            // Try to parse JSON output (requires serialization feature for serde_json)
285            #[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                        // Pre-compile regex outside loop for performance
296                        #[cfg(feature = "validation")]
297                        let vram_regex = Regex::new(r"(\d+)\s*(GB|MB)").ok();
298
299                        for display in displays {
300                            // Extract GPU information from each display
301                            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                                // Try to extract VRAM if available
314                                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                                    // Parse VRAM string like "8 GB" or "8192 MB"
324                                    #[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                                // Extract Metal family support
343                                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 JSON parsing failed, was skipped, or no devices found, try to detect via Metal API
358            if devices.is_empty() {
359                // Check if Metal is available
360                #[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 family detection would go here
374                        // Note: MTLGPUFamily is not exposed in the current metal crate
375                        gpu_info.compute_capability = Some("Metal GPU".to_string());
376
377                        devices.push(gpu_info);
378                    }
379                }
380
381                // Fallback if Metal crate not available but we're on macOS
382                #[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            // system_profiler failed, try Metal API directly
396            #[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/// Detect Metal devices (non-macOS - not available)
427#[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/// Detect OpenCL devices
436#[allow(dead_code)]
437fn detect_opencl_devices() -> Result<Vec<GpuInfo>, GpuError> {
438    let mut devices = Vec::new();
439
440    // Try to detect OpenCL devices using clinfo
441    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                    // In a real implementation, we would parse clinfo output properly
448                    // For now, just add a generic OpenCL device
449                    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; // Just add one for demo
457                }
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/// Check if a specific backend is properly installed and functional
473#[allow(dead_code)]
474pub fn check_backend_installation(backend: GpuBackend) -> Result<bool, GpuError> {
475    match backend {
476        GpuBackend::Cuda => {
477            // Check for CUDA installation
478            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            // Check for ROCm installation
485            match Command::new("hipcc").arg("--version").output() {
486                Ok(output) if output.status.success() => Ok(true),
487                _ => {
488                    // Also try rocm-smi as an alternative check
489                    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                // Metal is always available on macOS
500                Ok(true)
501            }
502            #[cfg(not(target_os = "macos"))]
503            {
504                Ok(false)
505            }
506        }
507        GpuBackend::OpenCL => {
508            // Check for OpenCL installation
509            match Command::new("clinfo").output() {
510                Ok(output) if output.status.success() => Ok(true),
511                _ => Ok(false),
512            }
513        }
514        GpuBackend::Wgpu => {
515            // WebGPU is always available through wgpu crate
516            Ok(true)
517        }
518        GpuBackend::Cpu => Ok(true),
519    }
520}
521
522/// Get detailed information about a specific GPU device
523#[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/// Initialize the optimal GPU backend for the current system
541#[allow(dead_code)]
542pub fn initialize_optimal_backend() -> Result<GpuBackend, GpuError> {
543    let detection_result = detect_gpu_backends();
544
545    // Try backends in order of preference for scientific computing
546    let preference_order = [
547        GpuBackend::Cuda,   // Best for scientific computing
548        GpuBackend::Rocm,   // Second best for scientific computing (AMD)
549        GpuBackend::Metal,  // Good on Apple hardware
550        GpuBackend::OpenCL, // Widely compatible
551        GpuBackend::Wgpu,   // Modern cross-platform
552        GpuBackend::Cpu,    // Always available fallback
553    ];
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    // Should never reach here since CPU is always available
566    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), // 10GB
579            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        // Should always have at least CPU fallback
595        assert!(!result.devices.is_empty());
596        assert!(result
597            .devices
598            .iter()
599            .any(|d: &GpuInfo| d.backend == GpuBackend::Cpu));
600
601        // Should have a recommended backend
602        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        // CPU should always be available
615        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        // WebGPU should always be available through wgpu crate
622        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        // Should return a valid backend
640        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        // Try to get info for a non-existent device
653        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        // CPU device should always be available
665        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), // 16GB
693            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    // Mock tests to verify error handling in detection functions
735    #[test]
736    fn test_detect_cuda_deviceserror_handling() {
737        // In the real implementation, detect_cuda_devices returns an error
738        // when nvidia-smi is not available. We can't easily test this without
739        // mocking the Command execution, but we can at least call the function
740        let _ = detect_cuda_devices();
741    }
742
743    #[test]
744    fn test_detect_rocm_deviceserror_handling() {
745        // Similar to CUDA test
746        let _ = detect_rocm_devices();
747    }
748
749    #[test]
750    fn test_detect_opencl_deviceserror_handling() {
751        // Similar to CUDA test
752        let _ = detect_opencl_devices();
753    }
754
755    #[test]
756    fn test_backend_preference_order() {
757        // Test that initialize_optimal_backend respects the preference order
758        let result = detect_gpu_backends();
759
760        // If we have multiple backends, the recommended should follow preference
761        if result
762            .devices
763            .iter()
764            .any(|d: &GpuInfo| d.backend == GpuBackend::Cuda)
765        {
766            // If CUDA is available, it should be preferred
767            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}