Skip to main content

optirs_gpu/memory/vendors/
mod.rs

1// Vendor-specific GPU memory backends
2//
3// This module provides vendor-specific GPU memory management implementations
4// for different GPU architectures and platforms.
5
6pub mod cuda_backend;
7pub mod metal_backend;
8pub mod oneapi_backend;
9pub mod rocm_backend;
10
11use std::ffi::c_void;
12use std::time::Duration;
13
14pub use cuda_backend::{
15    CudaConfig, CudaError, CudaMemoryBackend, CudaMemoryType, ThreadSafeCudaBackend,
16};
17pub use metal_backend::{
18    MetalConfig, MetalError, MetalMemoryBackend, MetalMemoryType, ThreadSafeMetalBackend,
19};
20pub use oneapi_backend::{
21    OneApiConfig, OneApiError, OneApiMemoryBackend, OneApiMemoryType, ThreadSafeOneApiBackend,
22};
23pub use rocm_backend::{
24    RocmConfig, RocmError, RocmMemoryBackend, RocmMemoryType, ThreadSafeRocmBackend,
25};
26
27/// Unified GPU vendor types
28#[derive(Debug, Clone, PartialEq)]
29pub enum GpuVendor {
30    Nvidia,
31    Amd,
32    Intel,
33    Apple,
34    Unknown,
35}
36
37/// Unified memory backend trait for all GPU vendors
38pub trait GpuMemoryBackend {
39    type Error: std::error::Error + Send + Sync + 'static;
40    type MemoryType: Clone + PartialEq;
41    type Stats: Clone;
42
43    /// Allocate GPU memory
44    fn allocate(
45        &mut self,
46        size: usize,
47        memory_type: Self::MemoryType,
48    ) -> Result<*mut c_void, Self::Error>;
49
50    /// Free GPU memory
51    fn free(&mut self, ptr: *mut c_void, memory_type: Self::MemoryType) -> Result<(), Self::Error>;
52
53    /// Get memory statistics
54    fn get_stats(&self) -> Self::Stats;
55
56    /// Synchronize all operations
57    fn synchronize(&mut self) -> Result<(), Self::Error>;
58
59    /// Get GPU vendor
60    fn get_vendor(&self) -> GpuVendor;
61
62    /// Get device name
63    fn get_device_name(&self) -> &str;
64
65    /// Get total memory size
66    fn get_total_memory(&self) -> usize;
67}
68
69/// Vendor detection and backend creation
70pub struct GpuBackendFactory;
71
72impl GpuBackendFactory {
73    /// Detect available GPU vendors.
74    ///
75    /// This used to unconditionally claim NVIDIA *and* AMD *and* Intel were
76    /// all present on every Linux/Windows machine (and Intel on every Mac,
77    /// which is simply false on Apple Silicon). It now either asks something
78    /// real or reports honestly empty instead of guessing:
79    ///
80    /// * **macOS**: every system has at least one Metal-capable device, so
81    ///   this opens a real [`scirs2_core::gpu::GpuContext`] on the `Metal`
82    ///   backend and reports `Apple` only if that actually succeeds. It never
83    ///   claims `Intel`: most Macs sold since 2020 have none.
84    /// * **Linux**: reads real PCI vendor IDs from `/sys/bus/pci/devices`
85    ///   (no FFI, no root required) via the private `detect_pci_display_vendors`
86    ///   helper below.
87    /// * **Everywhere else** (including Windows): this crate has no
88    ///   dependency-free way to query real vendor hardware, so it reports an
89    ///   empty list rather than a fabricated one.
90    pub fn detect_available_vendors() -> Vec<GpuVendor> {
91        #[cfg(target_os = "macos")]
92        {
93            match scirs2_core::gpu::GpuContext::new(scirs2_core::gpu::GpuBackend::Metal) {
94                Ok(_) => vec![GpuVendor::Apple],
95                Err(_) => Vec::new(),
96            }
97        }
98
99        #[cfg(target_os = "linux")]
100        {
101            detect_pci_display_vendors(std::path::Path::new("/sys/bus/pci/devices"))
102        }
103
104        #[cfg(not(any(target_os = "macos", target_os = "linux")))]
105        {
106            Vec::new()
107        }
108    }
109
110    /// The first vendor from [`Self::detect_available_vendors`] this crate
111    /// has a backend for, preferring the one most likely to work on the
112    /// current platform. `Unknown` when detection found nothing — never a
113    /// fabricated guess.
114    pub fn get_preferred_vendor() -> GpuVendor {
115        let vendors = Self::detect_available_vendors();
116        for candidate in [
117            GpuVendor::Apple,
118            GpuVendor::Nvidia,
119            GpuVendor::Amd,
120            GpuVendor::Intel,
121        ] {
122            if vendors.contains(&candidate) {
123                return candidate;
124            }
125        }
126        GpuVendor::Unknown
127    }
128
129    /// Create backend configuration for vendor
130    pub fn create_default_config(vendor: GpuVendor) -> VendorConfig {
131        match vendor {
132            GpuVendor::Nvidia => VendorConfig::Cuda(CudaConfig::default()),
133            GpuVendor::Amd => VendorConfig::Rocm(RocmConfig::default()),
134            GpuVendor::Intel => VendorConfig::OneApi(OneApiConfig::default()),
135            GpuVendor::Apple => VendorConfig::Metal(MetalConfig::default()),
136            GpuVendor::Unknown => VendorConfig::Cuda(CudaConfig::default()), // Fallback
137        }
138    }
139}
140
141/// Parse a `/sys/bus/pci/devices`-shaped directory tree for real GPU vendor
142/// IDs: for every device directory whose `class` file starts with `0x03`
143/// (the PCI "display controller" class), read `vendor` and map the standard
144/// PCI vendor ID to a [`GpuVendor`]. Takes the root as a parameter so the
145/// parsing logic is unit-testable against a fake tree without touching the
146/// real `/sys` (which is kernel-owned and cannot be written to by a test).
147///
148/// A missing or unreadable root (non-Linux, sandboxed, containerized without
149/// `/sys` mounted, ...) returns an empty list — the honest "detection is not
150/// possible here" answer, not a guess.
151///
152/// Only the Linux arm of [`GpuBackendFactory::detect_available_vendors`]
153/// calls this in production, so non-Linux builds see it as unused from the
154/// compiler's point of view. It is not dead: `detect_pci_display_vendors_*`
155/// below deliberately exercise this pure parsing logic against a fake sysfs
156/// tree on every platform the test suite runs on (see their doc comments),
157/// not just Linux, so the tests stay platform-independent rather than being
158/// narrowed to `cfg(target_os = "linux")`. The allow below silences that
159/// cross-platform false positive instead of deleting real, tested detection
160/// logic or reducing test coverage.
161#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
162fn detect_pci_display_vendors(pci_root: &std::path::Path) -> Vec<GpuVendor> {
163    const DISPLAY_CLASS_PREFIX: &str = "0x03";
164    const NVIDIA_VENDOR_ID: &str = "0x10de";
165    const AMD_VENDOR_ID: &str = "0x1002";
166    const INTEL_VENDOR_ID: &str = "0x8086";
167
168    let Ok(entries) = std::fs::read_dir(pci_root) else {
169        return Vec::new();
170    };
171
172    let mut found = Vec::new();
173    for entry in entries.flatten() {
174        let device_dir = entry.path();
175        let class = std::fs::read_to_string(device_dir.join("class")).unwrap_or_default();
176        if !class.trim().starts_with(DISPLAY_CLASS_PREFIX) {
177            continue;
178        }
179        let vendor_id = std::fs::read_to_string(device_dir.join("vendor")).unwrap_or_default();
180        let vendor = match vendor_id.trim() {
181            NVIDIA_VENDOR_ID => GpuVendor::Nvidia,
182            AMD_VENDOR_ID => GpuVendor::Amd,
183            INTEL_VENDOR_ID => GpuVendor::Intel,
184            _ => continue,
185        };
186        if !found.contains(&vendor) {
187            found.push(vendor);
188        }
189    }
190    found
191}
192
193/// Unified configuration for all vendors
194#[derive(Debug, Clone)]
195pub enum VendorConfig {
196    Cuda(CudaConfig),
197    Rocm(RocmConfig),
198    OneApi(OneApiConfig),
199    Metal(MetalConfig),
200}
201
202/// Unified backend wrapper
203pub enum UnifiedGpuBackend {
204    Cuda(CudaMemoryBackend),
205    Rocm(RocmMemoryBackend),
206    OneApi(OneApiMemoryBackend),
207    Metal(MetalMemoryBackend),
208}
209
210impl UnifiedGpuBackend {
211    /// Create backend from configuration
212    pub fn new(config: VendorConfig) -> Result<Self, UnifiedGpuError> {
213        match config {
214            VendorConfig::Cuda(config) => {
215                let backend = CudaMemoryBackend::new(config)?;
216                Ok(UnifiedGpuBackend::Cuda(backend))
217            }
218            VendorConfig::Rocm(config) => {
219                let backend = RocmMemoryBackend::new(config)?;
220                Ok(UnifiedGpuBackend::Rocm(backend))
221            }
222            VendorConfig::OneApi(config) => {
223                let backend = OneApiMemoryBackend::new(config)?;
224                Ok(UnifiedGpuBackend::OneApi(backend))
225            }
226            VendorConfig::Metal(config) => {
227                let backend = MetalMemoryBackend::new(config)?;
228                Ok(UnifiedGpuBackend::Metal(backend))
229            }
230        }
231    }
232
233    /// Auto-detect and create best backend
234    pub fn auto_create() -> Result<Self, UnifiedGpuError> {
235        let vendor = GpuBackendFactory::get_preferred_vendor();
236        let config = GpuBackendFactory::create_default_config(vendor);
237        Self::new(config)
238    }
239
240    /// Get vendor type
241    pub fn get_vendor(&self) -> GpuVendor {
242        match self {
243            UnifiedGpuBackend::Cuda(_) => GpuVendor::Nvidia,
244            UnifiedGpuBackend::Rocm(_) => GpuVendor::Amd,
245            UnifiedGpuBackend::OneApi(_) => GpuVendor::Intel,
246            UnifiedGpuBackend::Metal(_) => GpuVendor::Apple,
247        }
248    }
249
250    /// Allocate memory with unified interface
251    pub fn allocate(&mut self, size: usize) -> Result<*mut c_void, UnifiedGpuError> {
252        match self {
253            UnifiedGpuBackend::Cuda(backend) => backend
254                .allocate(size, CudaMemoryType::Device)
255                .map_err(UnifiedGpuError::Cuda),
256            UnifiedGpuBackend::Rocm(backend) => backend
257                .allocate(size, RocmMemoryType::Device)
258                .map_err(UnifiedGpuError::Rocm),
259            UnifiedGpuBackend::OneApi(backend) => backend
260                .allocate(size, OneApiMemoryType::Device)
261                .map_err(UnifiedGpuError::OneApi),
262            UnifiedGpuBackend::Metal(backend) => backend
263                .allocate(size, MetalMemoryType::Private)
264                .map_err(UnifiedGpuError::Metal),
265        }
266    }
267
268    /// Free memory with unified interface
269    pub fn free(&mut self, ptr: *mut c_void) -> Result<(), UnifiedGpuError> {
270        match self {
271            UnifiedGpuBackend::Cuda(backend) => backend
272                .free(ptr, CudaMemoryType::Device)
273                .map_err(UnifiedGpuError::Cuda),
274            UnifiedGpuBackend::Rocm(backend) => backend
275                .free(ptr, RocmMemoryType::Device)
276                .map_err(UnifiedGpuError::Rocm),
277            UnifiedGpuBackend::OneApi(backend) => backend
278                .free(ptr, OneApiMemoryType::Device)
279                .map_err(UnifiedGpuError::OneApi),
280            UnifiedGpuBackend::Metal(backend) => backend
281                .free(ptr, MetalMemoryType::Private)
282                .map_err(UnifiedGpuError::Metal),
283        }
284    }
285
286    /// Get unified memory statistics
287    /// Get total available GPU memory
288    pub fn get_total_memory(&self) -> usize {
289        // Default to 8GB if backend doesn't provide memory info
290        // Individual backends should implement proper memory querying
291        match self {
292            UnifiedGpuBackend::Cuda(_) => 8 * 1024 * 1024 * 1024, // 8GB default for CUDA
293            UnifiedGpuBackend::Rocm(_) => 8 * 1024 * 1024 * 1024, // 8GB default for ROCm
294            UnifiedGpuBackend::OneApi(_) => 8 * 1024 * 1024 * 1024, // 8GB default for OneAPI
295            UnifiedGpuBackend::Metal(_) => 8 * 1024 * 1024 * 1024, // 8GB default for Metal
296        }
297    }
298
299    pub fn get_memory_stats(&self) -> UnifiedMemoryStats {
300        match self {
301            UnifiedGpuBackend::Cuda(backend) => {
302                let stats = backend.get_stats();
303                UnifiedMemoryStats {
304                    total_allocations: stats.total_allocations,
305                    bytes_allocated: stats.bytes_allocated,
306                    peak_memory_usage: stats.peak_memory_usage,
307                    average_allocation_time: stats.average_allocation_time,
308                }
309            }
310            UnifiedGpuBackend::Rocm(backend) => {
311                let stats = backend.get_stats();
312                UnifiedMemoryStats {
313                    total_allocations: stats.total_allocations,
314                    bytes_allocated: stats.bytes_allocated,
315                    peak_memory_usage: stats.peak_memory_usage,
316                    average_allocation_time: stats.average_allocation_time,
317                }
318            }
319            UnifiedGpuBackend::OneApi(backend) => {
320                let stats = backend.get_stats();
321                UnifiedMemoryStats {
322                    total_allocations: stats.total_allocations,
323                    bytes_allocated: stats.bytes_allocated,
324                    peak_memory_usage: stats.peak_memory_usage,
325                    average_allocation_time: stats.average_allocation_time,
326                }
327            }
328            UnifiedGpuBackend::Metal(backend) => {
329                let stats = backend.get_stats();
330                UnifiedMemoryStats {
331                    total_allocations: stats.total_allocations,
332                    bytes_allocated: stats.bytes_allocated,
333                    peak_memory_usage: stats.peak_memory_usage,
334                    average_allocation_time: stats.average_allocation_time,
335                }
336            }
337        }
338    }
339}
340
341/// Unified memory statistics across all vendors
342#[derive(Debug, Clone, Default)]
343pub struct UnifiedMemoryStats {
344    pub total_allocations: u64,
345    pub bytes_allocated: u64,
346    pub peak_memory_usage: usize,
347    pub average_allocation_time: Duration,
348}
349
350/// Unified error type for all GPU backends
351#[derive(Debug)]
352pub enum UnifiedGpuError {
353    Cuda(CudaError),
354    Rocm(RocmError),
355    OneApi(OneApiError),
356    Metal(MetalError),
357    VendorNotSupported(String),
358    InitializationFailed(String),
359}
360
361impl std::fmt::Display for UnifiedGpuError {
362    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
363        match self {
364            UnifiedGpuError::Cuda(err) => write!(f, "CUDA Error: {}", err),
365            UnifiedGpuError::Rocm(err) => write!(f, "ROCm Error: {}", err),
366            UnifiedGpuError::OneApi(err) => write!(f, "OneAPI Error: {}", err),
367            UnifiedGpuError::Metal(err) => write!(f, "Metal Error: {}", err),
368            UnifiedGpuError::VendorNotSupported(msg) => write!(f, "Vendor not supported: {}", msg),
369            UnifiedGpuError::InitializationFailed(msg) => {
370                write!(f, "Initialization failed: {}", msg)
371            }
372        }
373    }
374}
375
376impl std::error::Error for UnifiedGpuError {}
377
378impl From<CudaError> for UnifiedGpuError {
379    fn from(err: CudaError) -> Self {
380        UnifiedGpuError::Cuda(err)
381    }
382}
383
384impl From<RocmError> for UnifiedGpuError {
385    fn from(err: RocmError) -> Self {
386        UnifiedGpuError::Rocm(err)
387    }
388}
389
390impl From<OneApiError> for UnifiedGpuError {
391    fn from(err: OneApiError) -> Self {
392        UnifiedGpuError::OneApi(err)
393    }
394}
395
396impl From<MetalError> for UnifiedGpuError {
397    fn from(err: MetalError) -> Self {
398        UnifiedGpuError::Metal(err)
399    }
400}
401
402#[cfg(test)]
403mod tests {
404    use super::*;
405
406    /// Regression test for F27: `detect_available_vendors` must not claim
407    /// vendors it has no evidence for. On macOS specifically, it must never
408    /// claim `Intel` unconditionally — most Macs sold since 2020 (Apple
409    /// Silicon) have no Intel GPU at all.
410    #[test]
411    fn test_vendor_detection() {
412        let vendors = GpuBackendFactory::detect_available_vendors();
413        // No duplicates, and every entry must be a vendor this platform's
414        // detector can actually justify.
415        let mut seen = Vec::new();
416        for vendor in &vendors {
417            assert!(
418                !seen.contains(vendor),
419                "duplicate vendor reported: {vendor:?}"
420            );
421            seen.push(vendor.clone());
422        }
423        #[cfg(target_os = "macos")]
424        {
425            assert!(
426                !vendors.contains(&GpuVendor::Intel),
427                "macOS detection must never assume Intel — most Macs have none"
428            );
429            assert!(
430                !vendors.contains(&GpuVendor::Nvidia) && !vendors.contains(&GpuVendor::Amd),
431                "macOS PCI vendors are not detected by this code path"
432            );
433        }
434    }
435
436    #[test]
437    fn test_preferred_vendor() {
438        // Must be self-consistent with detection: `Unknown` is legitimate
439        // when nothing was detected (e.g. a sandboxed Linux CI runner with no
440        // `/sys/bus/pci/devices`), so this only asserts internal consistency,
441        // not "always finds something" (that was the fabrication).
442        let vendors = GpuBackendFactory::detect_available_vendors();
443        let preferred = GpuBackendFactory::get_preferred_vendor();
444        if preferred != GpuVendor::Unknown {
445            assert!(
446                vendors.contains(&preferred),
447                "preferred vendor {preferred:?} was not among the detected vendors {vendors:?}"
448            );
449        }
450    }
451
452    /// The PCI-parsing logic itself, exercised against a fake sysfs tree
453    /// (never the real `/sys`, which is kernel-owned) so it is verified on
454    /// every platform this test suite runs on, not just Linux.
455    #[test]
456    fn detect_pci_display_vendors_reads_real_vendor_ids() {
457        let root = std::env::temp_dir().join(format!(
458            "optirs_gpu_pci_test_{}_{}",
459            std::process::id(),
460            std::time::SystemTime::now()
461                .duration_since(std::time::UNIX_EPOCH)
462                .map(|d| d.as_nanos())
463                .unwrap_or(0)
464        ));
465        std::fs::create_dir_all(&root).expect("create fake pci root");
466
467        let make_device = |name: &str, class: &str, vendor: &str| {
468            let dir = root.join(name);
469            std::fs::create_dir_all(&dir).expect("create fake device dir");
470            std::fs::write(dir.join("class"), class).expect("write class");
471            std::fs::write(dir.join("vendor"), vendor).expect("write vendor");
472        };
473
474        // A real NVIDIA display controller.
475        make_device("0000:01:00.0", "0x030000\n", "0x10de\n");
476        // A real AMD display controller.
477        make_device("0000:02:00.0", "0x030000\n", "0x1002\n");
478        // A non-display NVIDIA device (e.g. an audio codec on the same
479        // card) must NOT count as a display vendor.
480        make_device("0000:01:00.1", "0x040300\n", "0x10de\n");
481        // A display controller from an unrecognised vendor must be ignored,
482        // not misattributed.
483        make_device("0000:03:00.0", "0x030000\n", "0x1234\n");
484
485        let found = detect_pci_display_vendors(&root);
486        std::fs::remove_dir_all(&root).ok();
487
488        assert_eq!(found.len(), 2, "expected exactly NVIDIA and AMD: {found:?}");
489        assert!(found.contains(&GpuVendor::Nvidia));
490        assert!(found.contains(&GpuVendor::Amd));
491        assert!(!found.contains(&GpuVendor::Intel));
492    }
493
494    #[test]
495    fn detect_pci_display_vendors_missing_root_is_empty_not_an_error() {
496        let missing = std::env::temp_dir().join("optirs_gpu_pci_test_does_not_exist_at_all");
497        assert!(detect_pci_display_vendors(&missing).is_empty());
498    }
499
500    #[test]
501    fn test_unified_backend_creation() {
502        let vendor = GpuBackendFactory::get_preferred_vendor();
503        let config = GpuBackendFactory::create_default_config(vendor);
504        let backend = UnifiedGpuBackend::new(config);
505        assert!(backend.is_ok());
506    }
507
508    #[test]
509    fn test_auto_create() {
510        let backend = UnifiedGpuBackend::auto_create();
511        assert!(backend.is_ok());
512    }
513}