1use super::capabilities::*;
4use std::env;
5
6pub fn detect_platform_capabilities() -> PlatformCapabilities {
8 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
26fn 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
44fn detect_cpu_frequency() -> Option<f32> {
46 use sysinfo::System;
47
48 let mut sys = System::new();
49 sys.refresh_cpu_all();
50
51 sys.cpus().first().map(|cpu| cpu.frequency() as f32)
53}
54
55fn 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 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 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
124const fn detect_cache_info() -> CacheInfo {
126 CacheInfo {
128 l1_data: Some(32 * 1024), l1_instruction: Some(32 * 1024), l2: Some(256 * 1024), l3: Some(8 * 1024 * 1024), line_size: Some(64), }
134}
135
136fn detect_cpu_vendor() -> String {
138 use sysinfo::System;
139
140 let mut sys = System::new();
141 sys.refresh_cpu_all();
142
143 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 brand.to_string()
159 } else {
160 "Unknown".to_string()
161 }
162}
163
164fn detect_cpu_model() -> String {
166 use sysinfo::System;
167
168 let mut sys = System::new();
169 sys.refresh_cpu_all();
170
171 sys.cpus()
173 .first()
174 .map(|cpu| cpu.brand().to_string())
175 .unwrap_or_else(|| "Unknown".to_string())
176}
177
178fn 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 GpuCapabilities {
202 available: false,
203 devices: Vec::new(),
204 primary_device: None,
205 }
206}
207
208#[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 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
255fn detect_memory_capabilities() -> MemoryCapabilities {
257 use sysinfo::System;
258
259 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
273fn detect_memory_bandwidth() -> Option<f32> {
275 #[cfg(target_os = "linux")]
276 {
277 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 for line in text.lines() {
286 if line.contains("Speed:") && line.contains("MT/s") {
287 if let Some(speed_str) = line.split_whitespace().nth(1) {
289 if let Ok(speed_mts) = speed_str.parse::<f32>() {
290 let bandwidth_gbps = (speed_mts * 8.0) / 1000.0;
293 return Some(bandwidth_gbps);
294 }
295 }
296 }
297 }
298 }
299 }
300 }
301
302 Some(25.0) }
306
307 #[cfg(target_os = "macos")]
308 {
309 if let Ok(output) = std::process::Command::new("sysctl")
311 .arg("hw.memsize")
312 .output()
313 {
314 if output.status.success() {
315 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); }
328 return Some(30.0); }
330 }
331 Some(30.0)
332 }
333
334 #[cfg(target_os = "windows")]
335 {
336 Some(25.0)
339 }
340
341 #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
342 {
343 None
344 }
345}
346
347fn detect_numa_nodes() -> usize {
349 #[cfg(target_os = "linux")]
350 {
351 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 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 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 1
389 }
390
391 #[cfg(target_os = "macos")]
392 {
393 1
396 }
397
398 #[cfg(target_os = "windows")]
399 {
400 1
405 }
406
407 #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
408 {
409 1
410 }
411}
412
413fn 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
425fn detect_platform_type() -> PlatformType {
427 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 if cfg!(target_os = "android") || cfg!(target_os = "ios") {
439 return PlatformType::Mobile;
440 }
441
442 let logical_cores = num_cpus::get();
444 let physical_cores = num_cpus::get_physical();
445
446 use sysinfo::System;
447 let mut sys = System::new();
450 sys.refresh_memory();
451 let total_memory_gb = sys.total_memory() / (1024 * 1024 * 1024);
452
453 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 PlatformType::Embedded
473 } else {
474 PlatformType::Desktop
475 }
476}
477
478const 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
512const 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 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 let caps = detect_gpu_capabilities();
577
578 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 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 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 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}