optirs_gpu/memory/vendors/
mod.rs1pub 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#[derive(Debug, Clone, PartialEq)]
29pub enum GpuVendor {
30 Nvidia,
31 Amd,
32 Intel,
33 Apple,
34 Unknown,
35}
36
37pub trait GpuMemoryBackend {
39 type Error: std::error::Error + Send + Sync + 'static;
40 type MemoryType: Clone + PartialEq;
41 type Stats: Clone;
42
43 fn allocate(
45 &mut self,
46 size: usize,
47 memory_type: Self::MemoryType,
48 ) -> Result<*mut c_void, Self::Error>;
49
50 fn free(&mut self, ptr: *mut c_void, memory_type: Self::MemoryType) -> Result<(), Self::Error>;
52
53 fn get_stats(&self) -> Self::Stats;
55
56 fn synchronize(&mut self) -> Result<(), Self::Error>;
58
59 fn get_vendor(&self) -> GpuVendor;
61
62 fn get_device_name(&self) -> &str;
64
65 fn get_total_memory(&self) -> usize;
67}
68
69pub struct GpuBackendFactory;
71
72impl GpuBackendFactory {
73 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 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 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()), }
138 }
139}
140
141#[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#[derive(Debug, Clone)]
195pub enum VendorConfig {
196 Cuda(CudaConfig),
197 Rocm(RocmConfig),
198 OneApi(OneApiConfig),
199 Metal(MetalConfig),
200}
201
202pub enum UnifiedGpuBackend {
204 Cuda(CudaMemoryBackend),
205 Rocm(RocmMemoryBackend),
206 OneApi(OneApiMemoryBackend),
207 Metal(MetalMemoryBackend),
208}
209
210impl UnifiedGpuBackend {
211 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 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 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 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 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 pub fn get_total_memory(&self) -> usize {
289 match self {
292 UnifiedGpuBackend::Cuda(_) => 8 * 1024 * 1024 * 1024, UnifiedGpuBackend::Rocm(_) => 8 * 1024 * 1024 * 1024, UnifiedGpuBackend::OneApi(_) => 8 * 1024 * 1024 * 1024, UnifiedGpuBackend::Metal(_) => 8 * 1024 * 1024 * 1024, }
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#[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#[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 #[test]
411 fn test_vendor_detection() {
412 let vendors = GpuBackendFactory::detect_available_vendors();
413 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 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 #[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 make_device("0000:01:00.0", "0x030000\n", "0x10de\n");
476 make_device("0000:02:00.0", "0x030000\n", "0x1002\n");
478 make_device("0000:01:00.1", "0x040300\n", "0x10de\n");
481 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}