Skip to main content

quantrs2_core/platform/
detector.rs

1//! Platform detection implementation
2
3use super::capabilities::*;
4use std::env;
5
6/// Detect comprehensive platform capabilities
7pub fn detect_platform_capabilities() -> PlatformCapabilities {
8    // Consult SciRS2's platform detector. It exposes the acceleration-backend
9    // view (GPU/CUDA/OpenCL/Metal flags) reflecting how the SciRS2 stack was
10    // built, plus a compile-time SIMD summary (AVX2/AVX512/NEON). We fold those
11    // SIMD signals into our own *runtime* probing below as a build-time fallback —
12    // runtime `is_x86_feature_detected!` is strictly more precise, so it wins when
13    // both are available.
14    let scirs2_caps = scirs2_core::simd_ops::PlatformCapabilities::detect();
15
16    PlatformCapabilities {
17        cpu: detect_cpu_capabilities(&scirs2_caps),
18        gpu: detect_gpu_capabilities(),
19        memory: detect_memory_capabilities(),
20        platform_type: detect_platform_type(),
21        os: detect_operating_system(),
22        architecture: detect_architecture(),
23    }
24}
25
26/// Detect CPU capabilities
27fn detect_cpu_capabilities(
28    scirs2_caps: &scirs2_core::simd_ops::PlatformCapabilities,
29) -> CpuCapabilities {
30    let logical_cores = num_cpus::get();
31    let physical_cores = num_cpus::get_physical();
32
33    CpuCapabilities {
34        physical_cores,
35        logical_cores,
36        simd: detect_simd_capabilities(scirs2_caps),
37        cache: detect_cache_info(),
38        base_clock_mhz: detect_cpu_frequency(),
39        vendor: detect_cpu_vendor(),
40        model_name: detect_cpu_model(),
41    }
42}
43
44/// Detect CPU frequency in MHz
45fn detect_cpu_frequency() -> Option<f32> {
46    use sysinfo::System;
47
48    let mut sys = System::new();
49    sys.refresh_cpu_all();
50
51    // Get frequency from first CPU (all cores typically have same base frequency)
52    sys.cpus().first().map(|cpu| cpu.frequency() as f32)
53}
54
55/// Detect SIMD capabilities.
56///
57/// CPU feature flags are probed at *runtime* via `is_x86_feature_detected!`
58/// (x86_64) / target-feature cfgs (aarch64), which reflects the actual host the
59/// binary is executing on. SciRS2's compile-time SIMD summary (`scirs2_caps`) is
60/// OR-ed in as a fallback so features baked in at build time are never lost on
61/// targets where runtime probing is unavailable.
62fn detect_simd_capabilities(
63    scirs2_caps: &scirs2_core::simd_ops::PlatformCapabilities,
64) -> SimdCapabilities {
65    #[cfg(target_arch = "x86_64")]
66    {
67        SimdCapabilities {
68            sse: is_x86_feature_detected!("sse"),
69            sse2: is_x86_feature_detected!("sse2"),
70            sse3: is_x86_feature_detected!("sse3"),
71            ssse3: is_x86_feature_detected!("ssse3"),
72            sse4_1: is_x86_feature_detected!("sse4.1"),
73            sse4_2: is_x86_feature_detected!("sse4.2"),
74            avx: is_x86_feature_detected!("avx"),
75            avx2: is_x86_feature_detected!("avx2") || scirs2_caps.avx2_available,
76            // Runtime AVX-512 probing (more precise than the previous compile-time
77            // `cfg!(target_feature)`), reconciled with SciRS2's build-time view.
78            avx512: is_x86_feature_detected!("avx512f") || scirs2_caps.avx512_available,
79            fma: is_x86_feature_detected!("fma"),
80            neon: false,
81            sve: false,
82        }
83    }
84
85    #[cfg(target_arch = "aarch64")]
86    {
87        SimdCapabilities {
88            sse: false,
89            sse2: false,
90            sse3: false,
91            ssse3: false,
92            sse4_1: false,
93            sse4_2: false,
94            avx: false,
95            avx2: false,
96            avx512: false,
97            fma: false,
98            neon: cfg!(target_feature = "neon") || scirs2_caps.neon_available,
99            sve: cfg!(target_feature = "sve"),
100        }
101    }
102
103    #[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
104    {
105        // Unknown architecture: no runtime probing available, so fall back
106        // entirely to SciRS2's compile-time SIMD summary.
107        SimdCapabilities {
108            sse: false,
109            sse2: false,
110            sse3: false,
111            ssse3: false,
112            sse4_1: false,
113            sse4_2: false,
114            avx: false,
115            avx2: scirs2_caps.avx2_available,
116            avx512: scirs2_caps.avx512_available,
117            fma: false,
118            neon: scirs2_caps.neon_available,
119            sve: false,
120        }
121    }
122}
123
124/// Detect cache information
125const fn detect_cache_info() -> CacheInfo {
126    // Basic implementation - can be enhanced with platform-specific detection
127    CacheInfo {
128        l1_data: Some(32 * 1024),        // 32KB default
129        l1_instruction: Some(32 * 1024), // 32KB default
130        l2: Some(256 * 1024),            // 256KB default
131        l3: Some(8 * 1024 * 1024),       // 8MB default
132        line_size: Some(64),             // 64 byte cache line default
133    }
134}
135
136/// Detect CPU vendor
137fn detect_cpu_vendor() -> String {
138    use sysinfo::System;
139
140    let mut sys = System::new();
141    sys.refresh_cpu_all();
142
143    // Extract vendor from CPU brand string
144    if let Some(cpu) = sys.cpus().first() {
145        let brand = cpu.brand();
146        if brand.contains("Intel") {
147            return "Intel".to_string();
148        } else if brand.contains("AMD") {
149            return "AMD".to_string();
150        } else if brand.contains("Apple") {
151            return "Apple".to_string();
152        } else if brand.contains("ARM") {
153            return "ARM".to_string();
154        } else if brand.contains("Qualcomm") {
155            return "Qualcomm".to_string();
156        }
157        // Return brand if no known vendor found
158        brand.to_string()
159    } else {
160        "Unknown".to_string()
161    }
162}
163
164/// Detect CPU model
165fn detect_cpu_model() -> String {
166    use sysinfo::System;
167
168    let mut sys = System::new();
169    sys.refresh_cpu_all();
170
171    // Get CPU brand/model name
172    sys.cpus()
173        .first()
174        .map(|cpu| cpu.brand().to_string())
175        .unwrap_or_else(|| "Unknown".to_string())
176}
177
178/// Detect GPU capabilities.
179///
180/// With the `gpu` feature enabled, this performs a *real* probe via the OxiCUDA
181/// driver (which loads `libcuda.so`/`nvcuda.dll` at runtime): each CUDA device
182/// is enumerated and its genuine name, memory, SM count, max-threads, warp size,
183/// and compute capability are reported. Without the `gpu` feature, or when no
184/// GPU/driver is present, it honestly reports no GPU (`available: false`) — it
185/// never fabricates a device.
186fn detect_gpu_capabilities() -> GpuCapabilities {
187    #[cfg(feature = "gpu")]
188    {
189        if let Some(devices) = detect_cuda_gpu_devices() {
190            if !devices.is_empty() {
191                return GpuCapabilities {
192                    available: true,
193                    devices,
194                    primary_device: Some(0),
195                };
196            }
197        }
198    }
199
200    // Honest fallback: no GPU detected (or GPU support not compiled in).
201    GpuCapabilities {
202        available: false,
203        devices: Vec::new(),
204        primary_device: None,
205    }
206}
207
208/// Enumerate real CUDA devices via OxiCUDA and map them to [`GpuDevice`].
209///
210/// Returns `None` when the driver cannot be initialized (no GPU / no driver) and
211/// `Some(vec)` otherwise. All fields are genuine driver queries; fields the
212/// driver does not expose (e.g. exact CUDA-core count) are left as `None`.
213#[cfg(feature = "gpu")]
214fn detect_cuda_gpu_devices() -> Option<Vec<GpuDevice>> {
215    oxicuda::init().ok()?;
216    let count = oxicuda::Device::count().ok()?;
217    if count <= 0 {
218        return None;
219    }
220
221    let mut devices = Vec::with_capacity(count as usize);
222    for ordinal in 0..count {
223        let Ok(device) = oxicuda::Device::get(ordinal) else {
224            continue;
225        };
226        let Ok(info) = device.info() else {
227            continue;
228        };
229        let (cc_major, cc_minor) = info.compute_capability;
230        devices.push(GpuDevice {
231            name: info.name,
232            vendor: "NVIDIA".to_string(),
233            device_type: if device.is_integrated().unwrap_or(false) {
234                GpuType::Integrated
235            } else {
236                GpuType::Discrete
237            },
238            memory_bytes: info.total_memory_bytes,
239            compute_units: info.multiprocessor_count.max(0) as usize,
240            max_workgroup_size: info.max_threads_per_block.max(0) as usize,
241            // The driver does not directly report a CUDA-core count; leave None
242            // rather than fabricating one from the SM count.
243            cuda_cores: None,
244            compute_capability: Some((cc_major.max(0) as u32, cc_minor.max(0) as u32)),
245        });
246    }
247
248    if devices.is_empty() {
249        None
250    } else {
251        Some(devices)
252    }
253}
254
255/// Detect memory capabilities
256fn detect_memory_capabilities() -> MemoryCapabilities {
257    use sysinfo::System;
258
259    // `System::new()`, not `new_all()`: only memory is read here, and `new_all` additionally
260    // enumerates every process on the host.
261    let mut sys = System::new();
262    sys.refresh_memory();
263
264    MemoryCapabilities {
265        total_memory: sys.total_memory() as usize,
266        available_memory: sys.available_memory() as usize,
267        bandwidth_gbps: detect_memory_bandwidth(),
268        numa_nodes: detect_numa_nodes(),
269        hugepage_support: detect_hugepage_support(),
270    }
271}
272
273/// Detect memory bandwidth in GB/s
274fn detect_memory_bandwidth() -> Option<f32> {
275    #[cfg(target_os = "linux")]
276    {
277        // Try to read DMI information
278        if let Ok(output) = std::process::Command::new("dmidecode")
279            .args(["-t", "memory"])
280            .output()
281        {
282            if output.status.success() {
283                if let Ok(text) = String::from_utf8(output.stdout) {
284                    // Look for "Speed:" lines in DMI output
285                    for line in text.lines() {
286                        if line.contains("Speed:") && line.contains("MT/s") {
287                            // Extract speed value
288                            if let Some(speed_str) = line.split_whitespace().nth(1) {
289                                if let Ok(speed_mts) = speed_str.parse::<f32>() {
290                                    // Estimate bandwidth: speed (MT/s) * bus width (8 bytes) / 1000
291                                    // This is a rough estimate assuming DDR with 64-bit bus
292                                    let bandwidth_gbps = (speed_mts * 8.0) / 1000.0;
293                                    return Some(bandwidth_gbps);
294                                }
295                            }
296                        }
297                    }
298                }
299            }
300        }
301
302        // Fallback: estimate based on total memory
303        // Modern DDR4: ~20-40 GB/s, DDR5: ~40-80 GB/s
304        Some(25.0) // Conservative estimate
305    }
306
307    #[cfg(target_os = "macos")]
308    {
309        // macOS: Use sysctl to get memory info
310        if let Ok(output) = std::process::Command::new("sysctl")
311            .arg("hw.memsize")
312            .output()
313        {
314            if output.status.success() {
315                // Estimate based on Apple Silicon vs Intel
316                // M1/M2/M3: ~100-400 GB/s unified memory
317                // Intel: ~20-40 GB/s
318                if std::process::Command::new("sysctl")
319                    .arg("machdep.cpu.brand_string")
320                    .output()
321                    .ok()
322                    .and_then(|o| String::from_utf8(o.stdout).ok())
323                    .map(|s| s.contains("Apple"))
324                    .unwrap_or(false)
325                {
326                    return Some(200.0); // Apple Silicon estimate
327                }
328                return Some(30.0); // Intel Mac estimate
329            }
330        }
331        Some(30.0)
332    }
333
334    #[cfg(target_os = "windows")]
335    {
336        // Windows: Rough estimate based on typical RAM speeds
337        // DDR4-3200: ~25 GB/s, DDR4-2666: ~21 GB/s
338        Some(25.0)
339    }
340
341    #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
342    {
343        None
344    }
345}
346
347/// Detect number of NUMA nodes
348fn detect_numa_nodes() -> usize {
349    #[cfg(target_os = "linux")]
350    {
351        // Check /sys/devices/system/node/ for node directories
352        if let Ok(entries) = std::fs::read_dir("/sys/devices/system/node") {
353            let node_count = entries
354                .filter_map(|e| e.ok())
355                .filter(|e| {
356                    e.file_name().to_string_lossy().starts_with("node") && e.file_name() != "node"
357                })
358                .count();
359
360            if node_count > 0 {
361                return node_count;
362            }
363        }
364
365        // Fallback: try numactl
366        if let Ok(output) = std::process::Command::new("numactl")
367            .arg("--hardware")
368            .output()
369        {
370            if output.status.success() {
371                if let Ok(text) = String::from_utf8(output.stdout) {
372                    // Look for "available: N nodes"
373                    for line in text.lines() {
374                        if line.contains("available:") && line.contains("nodes") {
375                            if let Some(word) = line.split_whitespace().nth(1) {
376                                if let Ok(n) = word.parse::<usize>() {
377                                    return n;
378                                }
379                            }
380                        }
381                    }
382                }
383            }
384        }
385
386        // Neither /sys nor numactl was readable: honest single-node fallback
387        // (NOT MEASURED). The /sys path above is the real measurement.
388        1
389    }
390
391    #[cfg(target_os = "macos")]
392    {
393        // macOS typically doesn't expose NUMA topology on consumer hardware
394        // Server-grade Mac Pros might have NUMA, but it's not common
395        1
396    }
397
398    #[cfg(target_os = "windows")]
399    {
400        // Windows NUMA topology is not measured here (it would require the
401        // `GetNumaHighestNodeNumber` Win32 call via unsafe FFI). We return the
402        // honest single-node default rather than an invented value; most
403        // desktop/laptop systems do have exactly 1 NUMA node. NOT MEASURED.
404        1
405    }
406
407    #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
408    {
409        1
410    }
411}
412
413/// Detect hugepage support
414fn detect_hugepage_support() -> bool {
415    #[cfg(target_os = "linux")]
416    {
417        std::path::Path::new("/sys/kernel/mm/hugepages").exists()
418    }
419    #[cfg(not(target_os = "linux"))]
420    {
421        false
422    }
423}
424
425/// Detect platform type
426fn detect_platform_type() -> PlatformType {
427    // Check for cloud/container environments
428    if env::var("KUBERNETES_SERVICE_HOST").is_ok()
429        || env::var("ECS_CONTAINER_METADATA_URI").is_ok()
430        || env::var("AWS_EXECUTION_ENV").is_ok()
431        || env::var("GOOGLE_CLOUD_PROJECT").is_ok()
432        || env::var("AZURE_FUNCTIONS_ENVIRONMENT").is_ok()
433    {
434        return PlatformType::Cloud;
435    }
436
437    // Check for mobile platforms
438    if cfg!(target_os = "android") || cfg!(target_os = "ios") {
439        return PlatformType::Mobile;
440    }
441
442    // Detect server vs desktop based on hardware characteristics
443    let logical_cores = num_cpus::get();
444    let physical_cores = num_cpus::get_physical();
445
446    use sysinfo::System;
447    // `System::new()`, not `new_all()`: only memory is read here, and `new_all` additionally
448    // enumerates every process on the host.
449    let mut sys = System::new();
450    sys.refresh_memory();
451    let total_memory_gb = sys.total_memory() / (1024 * 1024 * 1024);
452
453    // Server heuristics:
454    // - High core count (>16 logical cores)
455    // - Large memory (>64 GB)
456    // - NUMA nodes > 1
457    // - Specific CPU model indicators
458    // The model string is read once; each `detect_cpu_model()` call refreshes every CPU.
459    let cpu_model = detect_cpu_model();
460    let is_server = logical_cores > 16
461        || total_memory_gb > 64
462        || detect_numa_nodes() > 1
463        || cpu_model.contains("Xeon")
464        || cpu_model.contains("EPYC")
465        || cpu_model.contains("Threadripper");
466
467    if is_server {
468        PlatformType::Server
469    } else if cfg!(any(target_arch = "arm", target_arch = "aarch64")) && !cfg!(target_os = "macos")
470    {
471        // ARM but not macOS might be embedded
472        PlatformType::Embedded
473    } else {
474        PlatformType::Desktop
475    }
476}
477
478/// Detect operating system
479const fn detect_operating_system() -> OperatingSystem {
480    #[cfg(target_os = "linux")]
481    {
482        OperatingSystem::Linux
483    }
484    #[cfg(target_os = "windows")]
485    {
486        OperatingSystem::Windows
487    }
488    #[cfg(target_os = "macos")]
489    {
490        OperatingSystem::MacOS
491    }
492    #[cfg(target_os = "freebsd")]
493    {
494        OperatingSystem::FreeBSD
495    }
496    #[cfg(target_os = "android")]
497    {
498        OperatingSystem::Android
499    }
500    #[cfg(not(any(
501        target_os = "linux",
502        target_os = "windows",
503        target_os = "macos",
504        target_os = "freebsd",
505        target_os = "android"
506    )))]
507    {
508        OperatingSystem::Unknown
509    }
510}
511
512/// Detect architecture
513const fn detect_architecture() -> Architecture {
514    #[cfg(target_arch = "x86_64")]
515    {
516        Architecture::X86_64
517    }
518    #[cfg(target_arch = "aarch64")]
519    {
520        Architecture::Aarch64
521    }
522    #[cfg(target_arch = "riscv64")]
523    {
524        Architecture::Riscv64
525    }
526    #[cfg(target_arch = "wasm32")]
527    {
528        Architecture::Wasm32
529    }
530    #[cfg(not(any(
531        target_arch = "x86_64",
532        target_arch = "aarch64",
533        target_arch = "riscv64",
534        target_arch = "wasm32"
535    )))]
536    {
537        Architecture::Unknown
538    }
539}
540
541#[cfg(test)]
542mod tests {
543    use super::*;
544
545    #[test]
546    fn test_numa_detection_is_real_on_linux() {
547        // On Linux the count comes from /sys; it must be at least 1 and match a
548        // direct read of the node directories when available.
549        let n = detect_numa_nodes();
550        assert!(n >= 1, "NUMA node count must be >= 1");
551
552        #[cfg(target_os = "linux")]
553        {
554            if let Ok(entries) = std::fs::read_dir("/sys/devices/system/node") {
555                let direct = entries
556                    .filter_map(Result::ok)
557                    .filter(|e| {
558                        let name = e.file_name();
559                        let name = name.to_string_lossy();
560                        name.starts_with("node")
561                            && name["node".len()..].chars().all(|c| c.is_ascii_digit())
562                            && name.len() > "node".len()
563                    })
564                    .count();
565                if direct > 0 {
566                    assert_eq!(n, direct, "NUMA count must equal the real /sys node count");
567                }
568            }
569        }
570    }
571
572    #[test]
573    fn test_gpu_detection_consistency() {
574        // The detected GPU capabilities must be internally consistent and must
575        // reflect a real probe (no fabricated devices).
576        let caps = detect_gpu_capabilities();
577
578        // `available` implies at least one real device with sane fields.
579        assert_eq!(caps.available, !caps.devices.is_empty());
580        if caps.available {
581            assert!(caps.primary_device.is_some());
582            for dev in &caps.devices {
583                assert!(!dev.name.is_empty(), "real device must have a name");
584                // Real compute capability is never the fabricated (7,5) constant
585                // unless the hardware genuinely is 7.5 — but it must be a real
586                // Some(..) probe, not a hardcoded None-vs-constant guess.
587                if let Some((maj, _min)) = dev.compute_capability {
588                    assert!(maj >= 1, "real CC major must be >= 1");
589                }
590            }
591        }
592
593        #[cfg(not(feature = "gpu"))]
594        {
595            // Without the gpu feature, detection must honestly report no GPU.
596            assert!(!caps.available);
597            assert!(caps.devices.is_empty());
598        }
599    }
600
601    #[cfg(feature = "gpu")]
602    #[test]
603    fn test_gpu_detection_matches_oxicuda_probe() {
604        // detect_gpu_capabilities() must agree with a direct OxiCUDA probe.
605        let caps = detect_gpu_capabilities();
606        let truth = oxicuda::init().is_ok() && oxicuda::Device::count().unwrap_or(0) > 0;
607        assert_eq!(
608            caps.available, truth,
609            "platform GPU detection must match the real OxiCUDA probe"
610        );
611    }
612}