1use libloading::Library;
12#[cfg(target_os = "linux")]
13use libloading::os::unix::{Library as UnixLibrary, RTLD_GLOBAL, RTLD_NOW};
14use ndarray::{Array2, ArrayBase, Data, Ix2};
15use std::borrow::Cow;
16use std::error::Error as StdError;
17use std::path::Path;
18#[cfg(target_os = "linux")]
19use std::path::PathBuf;
20use std::sync::OnceLock;
21
22use super::gpu_error::GpuError;
23
24pub type CuResult = i32;
25#[inline]
36pub fn check_cuda(result: CuResult, name: &str) -> Result<(), GpuError> {
37 if result == 0 {
38 Ok(())
39 } else {
40 Err(GpuError::DriverCallFailed {
41 reason: format!("{name} failed with CUDA driver error {result}"),
42 })
43 }
44}
45
46#[cfg(target_os = "linux")]
60fn already_resident_cuda_driver() -> Option<Library> {
61 const RTLD_NOLOAD: std::os::raw::c_int = 0x4;
62 for soname in ["libcuda.so.1", "libcuda.so"] {
63 if let Ok(library) = unsafe { UnixLibrary::open(Some(soname), RTLD_NOW | RTLD_NOLOAD) } {
66 return Some(library.into());
67 }
68 }
69 None
70}
71
72fn load_library_names(candidates: &[String]) -> Result<Library, GpuError> {
73 #[cfg(target_os = "linux")]
74 if let Some(resident) = already_resident_cuda_driver() {
75 return Ok(resident);
76 }
77 let mut load_faults = Vec::new();
78 for candidate in candidates {
79 match unsafe { Library::new(candidate) } {
83 Ok(library) => return Ok(library),
84 Err(error) => {
85 let detail = library_load_error_detail(&error);
86 let candidate_present = Path::new(candidate).components().count() > 1
87 && std::fs::symlink_metadata(candidate).is_ok();
88 if !load_failure_is_candidate_absence(candidate, candidate_present, &detail) {
89 load_faults.push(format!("{candidate}: {detail}"));
90 }
91 }
92 }
93 }
94 if !load_faults.is_empty() {
95 return Err(GpuError::DriverLibraryLoadFailed {
96 reason: format!(
97 "CUDA library candidates were found but failed to load: {}",
98 load_faults.join("; ")
99 ),
100 });
101 }
102 Err(GpuError::DriverLibraryUnavailable {
103 reason: format!("could not load any of: {}", candidates.join(", ")),
104 })
105}
106
107fn library_load_error_detail(error: &libloading::Error) -> String {
115 let mut detail = error.to_string();
116 let mut source = StdError::source(error);
117 while let Some(cause) = source {
118 detail = cause.to_string();
119 source = cause.source();
120 }
121 detail
122}
123
124fn load_failure_is_candidate_absence(
131 candidate: &str,
132 candidate_present: bool,
133 message: &str,
134) -> bool {
135 if Path::new(candidate).components().count() > 1 {
136 return !candidate_present;
137 }
138 let missing_object = message.starts_with(candidate)
139 && (message.contains("No such file or directory")
140 || message.contains("cannot open shared object file")
141 || message.contains("image not found"));
142 missing_object
143}
144
145fn load_static_cuda_driver_library() -> Result<&'static Library, GpuError> {
146 static LIBRARY: OnceLock<Result<Library, GpuError>> = OnceLock::new();
147 LIBRARY
148 .get_or_init(|| load_library_names(&cuda_library_candidate_names()))
149 .as_ref()
150 .map_err(Clone::clone)
151}
152
153pub fn preload_cuda_driver() -> Result<(), GpuError> {
154 static PRELOAD: OnceLock<Result<(), GpuError>> = OnceLock::new();
155 PRELOAD
156 .get_or_init(|| load_static_cuda_driver_library().map(|_| ()))
157 .clone()
158}
159
160pub fn cuda_driver_available() -> Result<bool, GpuError> {
163 match preload_cuda_driver() {
164 Ok(()) => Ok(true),
165 Err(GpuError::DriverLibraryUnavailable { .. }) => Ok(false),
166 Err(error) => Err(error),
167 }
168}
169
170#[cfg(test)]
171mod loader_classification_tests {
172 #[cfg(target_os = "linux")]
173 use super::library_load_error_detail;
174 use super::load_failure_is_candidate_absence;
175
176 #[cfg(target_os = "linux")]
177 #[test]
178 fn libloading_source_preserves_the_missing_bare_soname() {
179 const SONAME: &str = "libgamfit_cuda_driver_absence_probe.so.2411";
180 let error = match unsafe { libloading::Library::new(SONAME) } {
183 Ok(_) => panic!("the CUDA absence-probe soname unexpectedly exists"),
184 Err(error) => error,
185 };
186 let detail = library_load_error_detail(&error);
187 assert!(
188 load_failure_is_candidate_absence(SONAME, false, &detail),
189 "missing-soname detail was not classified as absence: {detail}"
190 );
191 }
192
193 #[test]
194 fn missing_bare_soname_is_absence() {
195 assert!(load_failure_is_candidate_absence(
196 "libcuda.so.1",
197 false,
198 "libcuda.so.1: cannot open shared object file: No such file or directory",
199 ));
200 }
201
202 #[test]
203 fn missing_transitive_dependency_is_a_load_fault() {
204 assert!(!load_failure_is_candidate_absence(
205 "libcuda.so.1",
206 false,
207 "libnvidia-fatbinaryloader.so.555: cannot open shared object file: No such file or directory",
208 ));
209 }
210
211 #[test]
212 fn present_but_invalid_absolute_candidate_is_a_load_fault() {
213 assert!(!load_failure_is_candidate_absence(
214 "/opt/cuda/libcuda.so.1",
215 true,
216 "/opt/cuda/libcuda.so.1: invalid ELF header",
217 ));
218 }
219
220 #[test]
221 fn absent_absolute_candidate_is_absence() {
222 assert!(load_failure_is_candidate_absence(
223 "/opt/cuda/libcuda.so.1",
224 false,
225 "/opt/cuda/libcuda.so.1: cannot open shared object file: No such file or directory",
226 ));
227 }
228}
229
230#[cfg(target_os = "linux")]
231fn preload_cuda_userspace_libraries() -> Result<(), String> {
232 static PRELOAD: OnceLock<Result<Vec<UnixLibrary>, String>> = OnceLock::new();
233 PRELOAD
234 .get_or_init(|| {
235 let paths = cuda_userspace_preload_paths()?;
236 if paths.is_empty() {
237 return Ok(Vec::new());
238 }
239 let mut loaded = Vec::new();
240 for path in paths {
241 match unsafe { UnixLibrary::open(Some(&path), RTLD_NOW | RTLD_GLOBAL) } {
247 Ok(library) => loaded.push(library),
248 Err(err) => {
249 return Err(format!(
250 "could not preload CUDA userspace library {}: {err}",
251 path.display()
252 ));
253 }
254 }
255 }
256 Ok(loaded)
257 })
258 .as_ref()
259 .map(|_| ())
260 .map_err(Clone::clone)
261}
262
263pub fn require_cuda_compute_library(stem: &str) -> Result<(), String> {
277 static PROBED: OnceLock<
285 std::sync::Mutex<std::collections::HashMap<String, Result<(), String>>>,
286 > = OnceLock::new();
287 static KEEP_ALIVE: OnceLock<std::sync::Mutex<Vec<Library>>> = OnceLock::new();
288 let probed = PROBED.get_or_init(|| std::sync::Mutex::new(std::collections::HashMap::new()));
289 if let Ok(cache) = probed.lock() {
290 if let Some(outcome) = cache.get(stem) {
291 return outcome.clone();
292 }
293 }
294 #[cfg(target_os = "linux")]
295 preload_cuda_userspace_libraries()?;
296 let outcome = match load_library_names(&cuda_compute_library_candidate_names(stem)) {
297 Ok(library) => {
298 if let Ok(mut keep) = KEEP_ALIVE
299 .get_or_init(|| std::sync::Mutex::new(Vec::new()))
300 .lock()
301 {
302 keep.push(library);
303 }
304 Ok(())
305 }
306 Err(error) => Err(error.to_string()),
307 };
308 if let Ok(mut cache) = probed.lock() {
309 cache.insert(stem.to_string(), outcome.clone());
310 }
311 outcome
312}
313
314#[cfg(target_os = "linux")]
315fn cuda_userspace_preload_paths() -> Result<Vec<PathBuf>, String> {
316 let mapped = mapped_cuda_userspace_libraries()?;
323 if !mapped.is_empty() {
324 return complete_mapped_cuda_stack(&mapped);
325 }
326
327 let system_dirs = cuda_system_library_dirs();
328 for dir in &system_dirs {
329 if let Some(stack) = complete_system_cuda_stack(dir) {
330 return Ok(dedup_paths(stack));
331 }
332 if let Some(stack) = system_cuda_stack_with_packaged_nvjitlink(dir) {
333 return Ok(dedup_paths(stack));
334 }
335 }
336 for root in nvidia_package_roots() {
337 if let Some(stack) = complete_nvidia_cuda_stack(&root) {
338 return Ok(dedup_paths(stack));
339 }
340 }
341 Ok(Vec::new())
342}
343
344#[cfg(target_os = "linux")]
349fn cuda_library_component(path: &Path) -> Option<String> {
350 let name = path.file_name()?.to_str()?;
351 let stem = name.strip_prefix("lib")?.split(".so").next()?;
352 if stem.is_empty() {
353 None
354 } else {
355 Some(stem.to_string())
356 }
357}
358
359#[cfg(target_os = "linux")]
366fn is_cuda_compute_component(component: &str) -> bool {
367 matches!(component, "cublas" | "cublasLt" | "cusolver" | "cusparse")
368}
369
370#[cfg(target_os = "linux")]
371fn complete_mapped_cuda_stack(mapped: &[PathBuf]) -> Result<Vec<PathBuf>, String> {
372 let canonical = |p: &Path| p.canonicalize().unwrap_or_else(|_| p.to_path_buf());
373
374 let mut candidates = Vec::new();
375 for path in mapped {
376 if let Some(root) = nvidia_package_root_for_library(path)
377 && let Some(stack) = complete_nvidia_cuda_stack(&root)
378 {
379 candidates.push(stack);
380 }
381 if let Some(parent) = path.parent()
382 && let Some(stack) = complete_system_cuda_stack_path(parent)
383 {
384 candidates.push(stack);
385 }
386 }
387
388 let mapped_meta: Vec<(PathBuf, Option<String>)> = mapped
390 .iter()
391 .map(|m| (canonical(m), cuda_library_component(m)))
392 .collect();
393
394 for stack in &candidates {
398 let stack = dedup_paths(stack.clone());
399 let stack_canon: Vec<PathBuf> = stack.iter().map(|c| canonical(c)).collect();
400 if mapped_meta
401 .iter()
402 .all(|(m, _)| stack_canon.iter().any(|c| c == m))
403 {
404 return Ok(stack);
405 }
406 }
407
408 for stack in candidates {
428 let stack = dedup_paths(stack);
429 let stack_canon: Vec<PathBuf> = stack.iter().map(|c| canonical(c)).collect();
430 let fully_mapped = stack_canon
431 .iter()
432 .all(|s| mapped_meta.iter().any(|(m, _)| m == s));
433 if !fully_mapped {
434 continue;
435 }
436 let consistent = mapped_meta.iter().all(|(m, component)| {
437 if stack_canon.iter().any(|s| s == m) {
438 return true;
439 }
440 match component {
441 Some(component) if !is_cuda_compute_component(component) => stack
442 .iter()
443 .any(|s| cuda_library_component(s).as_deref() == Some(component)),
444 _ => false,
445 }
446 });
447 if consistent {
448 return Ok(stack);
449 }
450 }
451
452 Err(format!(
453 "CUDA userspace is already mapped from no single complete stack: {}",
454 mapped
455 .iter()
456 .map(|path| path.display().to_string())
457 .collect::<Vec<_>>()
458 .join(", ")
459 ))
460}
461
462#[cfg(target_os = "linux")]
463fn mapped_cuda_userspace_libraries() -> Result<Vec<PathBuf>, String> {
464 let maps = std::fs::read_to_string("/proc/self/maps")
465 .map_err(|error| format!("cannot inspect mapped CUDA userspace libraries: {error}"))?;
466 let mut mapped = Vec::new();
467 for line in maps.lines() {
468 let Some(raw_path) = line.split_whitespace().last() else {
469 continue;
470 };
471 if !raw_path.starts_with('/') {
472 continue;
473 }
474 let path = PathBuf::from(raw_path);
475 let Some(name) = path.file_name().and_then(|name| name.to_str()) else {
476 continue;
477 };
478 if [
479 "libcudart.so",
480 "libnvJitLink.so",
481 "libcublasLt.so",
482 "libcublas.so",
483 "libcusparse.so",
484 "libcusolver.so",
485 ]
486 .iter()
487 .any(|prefix| name.starts_with(prefix))
488 {
489 mapped.push(path);
490 }
491 }
492 Ok(dedup_paths(mapped))
493}
494
495#[cfg(target_os = "linux")]
496fn nvidia_package_root_for_library(path: &Path) -> Option<PathBuf> {
497 path.ancestors()
498 .find(|ancestor| ancestor.file_name().and_then(|name| name.to_str()) == Some("nvidia"))
499 .map(Path::to_path_buf)
500}
501
502fn cuda_compute_library_candidate_names(stem: &str) -> Vec<String> {
503 let base = format!("lib{stem}");
504 let mut out: Vec<String> = Vec::new();
505 out.push(format!("{base}.so"));
508 out.push(format!("{base}.so.1"));
509 for major in (9..=13).rev() {
512 out.push(format!("{base}.so.{major}"));
513 }
514 #[cfg(target_os = "linux")]
515 {
516 for dir in cuda_system_library_dirs() {
517 out.push(format!("{dir}/{base}.so"));
518 for major in (9..=13).rev() {
519 out.push(format!("{dir}/{base}.so.{major}"));
520 }
521 append_versioned_linux_so_candidates(&mut out, Path::new(dir), &base);
522 }
523 for root in nvidia_package_roots() {
524 let lib_dir = root.join(nvidia_component_for_stem(stem)).join("lib");
525 out.push(format!("{}/{}.so", lib_dir.display(), base));
526 for major in (9..=13).rev() {
527 out.push(format!("{}/{}.so.{major}", lib_dir.display(), base));
528 }
529 append_versioned_linux_so_candidates(&mut out, &lib_dir, &base);
530 }
531 }
532 out
533}
534
535#[cfg(target_os = "linux")]
536fn cuda_system_library_dirs() -> Vec<&'static str> {
537 vec![
538 "/usr/local/cuda/lib64",
539 "/usr/local/cuda/lib",
540 "/usr/local/cuda/targets/x86_64-linux/lib",
541 "/usr/lib/x86_64-linux-gnu",
542 "/usr/lib64",
543 "/usr/lib/wsl/lib",
544 "/opt/cuda/lib64",
545 ]
546}
547
548#[cfg(target_os = "linux")]
549fn complete_system_cuda_stack(dir: &str) -> Option<Vec<PathBuf>> {
550 complete_system_cuda_stack_path(Path::new(dir))
551}
552
553#[cfg(target_os = "linux")]
554fn complete_system_cuda_stack_path(dir: &Path) -> Option<Vec<PathBuf>> {
555 let stack = vec![
556 first_existing(dir, &["libcudart.so.13", "libcudart.so.12", "libcudart.so"])?,
557 first_existing(
558 dir,
559 &[
560 "libnvJitLink.so.13",
561 "libnvJitLink.so.12",
562 "libnvJitLink.so",
563 ],
564 )?,
565 first_existing(
566 dir,
567 &["libcublasLt.so.13", "libcublasLt.so.12", "libcublasLt.so"],
568 )?,
569 first_existing(dir, &["libcublas.so.13", "libcublas.so.12", "libcublas.so"])?,
570 first_existing(
571 dir,
572 &["libcusparse.so.13", "libcusparse.so.12", "libcusparse.so"],
573 )?,
574 first_existing(
575 dir,
576 &[
577 "libcusolver.so.13",
578 "libcusolver.so.12",
579 "libcusolver.so.11",
580 "libcusolver.so",
581 ],
582 )?,
583 ];
584 Some(stack)
585}
586
587#[cfg(target_os = "linux")]
588fn system_cuda_stack_with_packaged_nvjitlink(dir: &str) -> Option<Vec<PathBuf>> {
589 let dir = Path::new(dir);
590 let nvjitlink = packaged_nvjitlink_library()?;
591 let stack = vec![
592 first_existing(dir, &["libcudart.so.13", "libcudart.so.12", "libcudart.so"])?,
593 nvjitlink,
594 first_existing(
595 dir,
596 &["libcublasLt.so.13", "libcublasLt.so.12", "libcublasLt.so"],
597 )?,
598 first_existing(dir, &["libcublas.so.13", "libcublas.so.12", "libcublas.so"])?,
599 first_existing(
600 dir,
601 &["libcusparse.so.13", "libcusparse.so.12", "libcusparse.so"],
602 )?,
603 first_existing(
604 dir,
605 &[
606 "libcusolver.so.13",
607 "libcusolver.so.12",
608 "libcusolver.so.11",
609 "libcusolver.so",
610 ],
611 )?,
612 ];
613 Some(stack)
614}
615
616#[cfg(target_os = "linux")]
617fn complete_nvidia_cuda_stack(root: &Path) -> Option<Vec<PathBuf>> {
618 let stack = vec![
619 first_existing(
620 &root.join("cuda_runtime").join("lib"),
621 &["libcudart.so.13", "libcudart.so.12", "libcudart.so"],
622 )?,
623 first_existing(
624 &root.join("nvjitlink").join("lib"),
625 &[
626 "libnvJitLink.so.13",
627 "libnvJitLink.so.12",
628 "libnvJitLink.so",
629 ],
630 )?,
631 first_existing(
632 &root.join("cublas").join("lib"),
633 &["libcublasLt.so.13", "libcublasLt.so.12", "libcublasLt.so"],
634 )?,
635 first_existing(
636 &root.join("cublas").join("lib"),
637 &["libcublas.so.13", "libcublas.so.12", "libcublas.so"],
638 )?,
639 first_existing(
640 &root.join("cusparse").join("lib"),
641 &["libcusparse.so.13", "libcusparse.so.12", "libcusparse.so"],
642 )?,
643 first_existing(
644 &root.join("cusolver").join("lib"),
645 &[
646 "libcusolver.so.13",
647 "libcusolver.so.12",
648 "libcusolver.so.11",
649 "libcusolver.so",
650 ],
651 )?,
652 ];
653 Some(stack)
654}
655
656#[cfg(target_os = "linux")]
657fn packaged_nvjitlink_library() -> Option<PathBuf> {
658 for root in nvidia_package_roots() {
659 let lib_dir = root.join("nvjitlink").join("lib");
660 if let Some(path) = first_existing(
661 &lib_dir,
662 &[
663 "libnvJitLink.so.13",
664 "libnvJitLink.so.12",
665 "libnvJitLink.so",
666 ],
667 ) {
668 return Some(path);
669 }
670 }
671 None
672}
673
674#[cfg(target_os = "linux")]
675fn nvidia_component_for_stem(stem: &str) -> String {
676 match stem {
677 "cublas" => "cublas".to_string(),
678 "cusolver" => "cusolver".to_string(),
679 "cusparse" => "cusparse".to_string(),
680 "nvJitLink" | "nvjitlink" => "nvjitlink".to_string(),
681 "cudart" | "cuda_runtime" => "cuda_runtime".to_string(),
682 _ => stem.to_string(),
683 }
684}
685
686#[cfg(target_os = "linux")]
687fn nvidia_package_roots() -> Vec<PathBuf> {
688 let mut roots = Vec::new();
689 if let Some(home) = current_user_home_dir() {
690 collect_python_nvidia_roots(home.join(".local/lib"), &mut roots);
691 }
692 collect_python_nvidia_roots(Path::new("/usr/local/lib").to_path_buf(), &mut roots);
693 collect_python_nvidia_roots(Path::new("/usr/lib").to_path_buf(), &mut roots);
694 dedup_paths(roots)
695}
696
697#[cfg(target_os = "linux")]
698fn current_user_home_dir() -> Option<PathBuf> {
699 let status = std::fs::read_to_string("/proc/self/status").ok()?;
700 let uid = status
701 .lines()
702 .find_map(|line| line.strip_prefix("Uid:"))?
703 .split_whitespace()
704 .next()?;
705 let passwd = std::fs::read_to_string("/etc/passwd").ok()?;
706 for line in passwd.lines() {
707 let mut fields = line.split(':');
708 fields.next()?;
709 fields.next()?;
710 if fields.next()? != uid {
711 continue;
712 }
713 fields.next()?;
714 fields.next()?;
715 return Some(PathBuf::from(fields.next()?));
716 }
717 None
718}
719
720#[cfg(target_os = "linux")]
721fn collect_python_nvidia_roots(base: PathBuf, out: &mut Vec<PathBuf>) {
722 let Ok(entries) = std::fs::read_dir(base) else {
723 return;
724 };
725 for entry in entries.flatten() {
726 let path = entry.path();
727 let Some(name) = path.file_name().and_then(|name| name.to_str()) else {
728 continue;
729 };
730 if !name.starts_with("python") {
731 continue;
732 }
733 for site_dir in ["site-packages", "dist-packages"] {
734 let root = path.join(site_dir).join("nvidia");
735 if root.exists() {
736 out.push(root);
737 }
738 }
739 }
740}
741
742#[cfg(target_os = "linux")]
743fn first_existing(dir: &Path, names: &[&str]) -> Option<PathBuf> {
744 for name in names {
745 let path = dir.join(name);
746 if path.exists() {
747 return Some(path);
748 }
749 }
750 None
751}
752
753#[cfg(target_os = "linux")]
754fn dedup_paths(paths: Vec<PathBuf>) -> Vec<PathBuf> {
755 let mut out = Vec::new();
756 for path in paths {
757 let canonical = path.canonicalize().unwrap_or(path);
758 if !out.iter().any(|existing| existing == &canonical) {
759 out.push(canonical);
760 }
761 }
762 out
763}
764
765#[cfg(target_os = "linux")]
766fn append_versioned_linux_so_candidates(out: &mut Vec<String>, dir: &Path, base: &str) {
767 let Ok(entries) = std::fs::read_dir(dir) else {
768 return;
769 };
770 let prefix = format!("{base}.so.");
771 let mut versioned = Vec::new();
772 for entry in entries.flatten() {
773 let path = entry.path();
774 let Some(name) = path.file_name().and_then(|name| name.to_str()) else {
775 continue;
776 };
777 if name.starts_with(&prefix) {
778 versioned.push(path);
779 }
780 }
781 versioned.sort();
782 for path in versioned {
783 let candidate = path.to_string_lossy().into_owned();
784 if !out.iter().any(|existing| existing == &candidate) {
785 out.push(candidate);
786 }
787 }
788}
789
790fn cuda_library_candidate_names() -> Vec<String> {
791 let mut out: Vec<String> = cuda_library_candidates()
792 .iter()
793 .map(|candidate| (*candidate).to_string())
794 .collect();
795 if cfg!(target_os = "linux") {
796 for dir in [
797 "/usr/local/nvidia/lib64",
798 "/usr/local/nvidia/lib",
799 "/usr/local/cuda/compat",
800 "/usr/lib/x86_64-linux-gnu",
801 "/usr/lib64",
802 "/usr/lib/wsl/lib",
803 ] {
804 append_versioned_linux_libcuda_candidates(&mut out, Path::new(dir));
805 }
806 }
807 out
808}
809
810fn append_versioned_linux_libcuda_candidates(out: &mut Vec<String>, dir: &Path) {
811 let Ok(entries) = std::fs::read_dir(dir) else {
812 return;
813 };
814 let mut versioned = Vec::new();
815 for entry in entries.flatten() {
816 let path = entry.path();
817 let Some(name) = path.file_name().and_then(|name| name.to_str()) else {
818 continue;
819 };
820 if name.starts_with("libcuda.so.") && name != "libcuda.so.1" {
821 versioned.push(path);
822 }
823 }
824 versioned.sort();
825 for path in versioned {
826 let candidate = path.to_string_lossy().into_owned();
827 if !out.iter().any(|existing| existing == &candidate) {
828 out.push(candidate);
829 }
830 }
831}
832
833pub fn cuda_library_candidates() -> &'static [&'static str] {
834 if cfg!(target_os = "windows") {
835 &["nvcuda.dll"]
836 } else if cfg!(target_os = "macos") {
837 &["/usr/local/cuda/lib/libcuda.dylib", "libcuda.dylib"]
838 } else {
839 &[
840 "/usr/local/nvidia/lib64/libcuda.so.1",
841 "/usr/local/nvidia/lib64/libcuda.so",
842 "/usr/local/nvidia/lib/libcuda.so.1",
843 "/usr/local/nvidia/lib/libcuda.so",
844 "/usr/local/cuda/compat/libcuda.so.1",
845 "/usr/local/cuda/compat/libcuda.so",
846 "/usr/lib/x86_64-linux-gnu/libcuda.so.1",
847 "/usr/lib/x86_64-linux-gnu/libcuda.so",
848 "/usr/lib64/libcuda.so.1",
849 "/usr/lib64/libcuda.so",
850 "/usr/lib/wsl/lib/libcuda.so.1",
851 "/usr/lib/wsl/lib/libcuda.so",
852 "libcuda.so.1",
853 "libcuda.so",
854 ]
855 }
856}
857
858#[inline]
859pub fn to_i32(value: usize) -> Option<i32> {
860 i32::try_from(value).ok()
861}
862
863pub fn to_col_major<'a, S: Data<Elem = f64>>(a: &'a ArrayBase<S, Ix2>) -> Cow<'a, [f64]> {
875 let (rows, cols) = a.dim();
876 let strides = a.strides();
877 if rows > 0
880 && cols > 0
881 && strides[0] == 1
882 && strides[1] == rows as isize
883 && let Some(slice) = a.as_slice_memory_order()
884 {
885 return Cow::Borrowed(slice);
886 }
887 let mut out: Vec<f64> = Vec::with_capacity(rows.saturating_mul(cols));
888 for col in 0..cols {
889 out.extend(a.column(col).iter().copied());
890 }
891 Cow::Owned(out)
892}
893
894pub fn to_row_major<'a, S: Data<Elem = f64>>(a: &'a ArrayBase<S, Ix2>) -> Cow<'a, [f64]> {
908 let (rows, cols) = a.dim();
909 let strides = a.strides();
910 if rows > 0
912 && cols > 0
913 && strides[1] == 1
914 && strides[0] == cols as isize
915 && let Some(slice) = a.as_slice_memory_order()
916 {
917 return Cow::Borrowed(slice);
918 }
919 let mut out: Vec<f64> = Vec::with_capacity(rows.saturating_mul(cols));
920 for row in 0..rows {
921 out.extend(a.row(row).iter().copied());
922 }
923 Cow::Owned(out)
924}
925
926pub fn array_from_row_major(values: Vec<f64>, rows: usize, cols: usize) -> Option<Array2<f64>> {
930 if values.len() != rows.checked_mul(cols)? {
931 return None;
932 }
933 Array2::from_shape_vec((rows, cols), values).ok()
934}
935
936pub fn from_col_major_inplace(values: &[f64], out: &mut Array2<f64>) -> Option<()> {
938 let (rows, cols) = out.dim();
939 if values.len() != rows.checked_mul(cols)? {
940 return None;
941 }
942 for col in 0..cols {
943 let src = ndarray::ArrayView1::from(&values[col * rows..(col + 1) * rows]);
944 out.column_mut(col).assign(&src);
945 }
946 Some(())
947}
948
949pub fn from_col_major(values: &[f64], rows: usize, cols: usize) -> Option<Array2<f64>> {
950 let mut out = Array2::<f64>::zeros((rows, cols));
951 from_col_major_inplace(values, &mut out)?;
952 Some(out)
953}
954
955#[cfg(test)]
956mod tests {
957 use super::*;
958 use ndarray::array;
959
960 #[cfg(target_os = "linux")]
961 fn fake_nvidia_stack(root: &Path) -> Vec<PathBuf> {
962 let libraries = [
963 ("cuda_runtime", "libcudart.so.12"),
964 ("nvjitlink", "libnvJitLink.so.12"),
965 ("cublas", "libcublasLt.so.12"),
966 ("cublas", "libcublas.so.12"),
967 ("cusparse", "libcusparse.so.12"),
968 ("cusolver", "libcusolver.so.11"),
969 ];
970 libraries
971 .into_iter()
972 .map(|(component, name)| {
973 let path = root.join(component).join("lib").join(name);
974 std::fs::create_dir_all(path.parent().expect("library parent"))
975 .expect("create fake CUDA component directory");
976 std::fs::write(&path, []).expect("create fake CUDA library");
977 path
978 })
979 .collect()
980 }
981
982 #[test]
983 fn to_i32_fits_small_value() {
984 assert_eq!(to_i32(0), Some(0));
985 assert_eq!(to_i32(42), Some(42));
986 assert_eq!(to_i32(i32::MAX as usize), Some(i32::MAX));
987 }
988
989 #[test]
990 fn to_i32_overflows_returns_none() {
991 assert_eq!(to_i32(i32::MAX as usize + 1), None);
992 }
993
994 #[test]
995 fn to_col_major_2x3_row_major() {
996 let a = array![[1.0_f64, 2.0, 3.0], [4.0, 5.0, 6.0]];
998 let col = to_col_major(&a);
999 assert_eq!(&*col, &[1.0, 4.0, 2.0, 5.0, 3.0, 6.0]);
1000 }
1001
1002 #[test]
1003 fn to_col_major_identity_roundtrip() {
1004 let a = array![[1.0_f64, 0.0], [0.0, 1.0]];
1005 let col = to_col_major(&a);
1006 assert_eq!(&*col, &[1.0, 0.0, 0.0, 1.0]);
1007 }
1008
1009 #[test]
1010 fn from_col_major_2x3_roundtrip() {
1011 let original = array![[1.0_f64, 2.0, 3.0], [4.0, 5.0, 6.0]];
1012 let col = to_col_major(&original);
1013 let recovered = from_col_major(&col, 2, 3).expect("should succeed");
1014 assert_eq!(recovered, original);
1015 }
1016
1017 #[test]
1018 fn from_col_major_wrong_length_returns_none() {
1019 assert!(from_col_major(&[1.0, 2.0, 3.0, 4.0, 5.0], 2, 3).is_none());
1021 }
1022
1023 #[test]
1024 fn from_col_major_inplace_mismatched_buffer_returns_none() {
1025 let mut out = Array2::<f64>::zeros((3, 3));
1026 let short = vec![1.0_f64; 8]; assert!(from_col_major_inplace(&short, &mut out).is_none());
1028 }
1029
1030 #[test]
1031 fn from_col_major_single_element() {
1032 let result = from_col_major(&[7.0], 1, 1).expect("should succeed");
1033 assert_eq!(result[[0, 0]], 7.0);
1034 }
1035
1036 #[cfg(target_os = "linux")]
1037 #[test]
1038 fn mapped_pytorch_stack_is_continued_as_one_complete_stack() {
1039 let temp = tempfile::tempdir().expect("temporary CUDA tree");
1040 let root = temp.path().join("site-packages").join("nvidia");
1041 let stack = fake_nvidia_stack(&root);
1042 let mapped = vec![stack[0].clone(), stack[3].clone()];
1043
1044 let selected = complete_mapped_cuda_stack(&mapped).expect("one mapped NVIDIA root");
1045
1046 assert_eq!(selected.len(), stack.len());
1047 assert!(mapped.iter().all(|path| {
1048 let canonical = path.canonicalize().expect("canonical fake library");
1049 selected.contains(&canonical)
1050 }));
1051 }
1052
1053 #[cfg(target_os = "linux")]
1054 #[test]
1055 fn mapped_mixed_cuda_stacks_are_refused() {
1056 let temp = tempfile::tempdir().expect("temporary CUDA tree");
1057 let first = fake_nvidia_stack(&temp.path().join("first").join("nvidia"));
1058 let second = fake_nvidia_stack(&temp.path().join("second").join("nvidia"));
1059 let mapped = vec![first[0].clone(), second[3].clone()];
1060
1061 let error = complete_mapped_cuda_stack(&mapped)
1062 .expect_err("libraries from two mapped roots must not be mixed");
1063
1064 assert!(error.contains("no single complete stack"));
1065 }
1066
1067 #[cfg(target_os = "linux")]
1068 #[test]
1069 fn mapped_pytorch_stack_with_redundant_system_cudart_is_continued() {
1070 let temp = tempfile::tempdir().expect("temporary CUDA tree");
1076 let pip = fake_nvidia_stack(&temp.path().join("site-packages").join("nvidia"));
1077
1078 let sys_dir = temp.path().join("usr").join("local").join("cuda").join("lib");
1081 std::fs::create_dir_all(&sys_dir).expect("system lib dir");
1082 let sys_cudart = sys_dir.join("libcudart.so.12");
1083 std::fs::write(&sys_cudart, []).expect("system cudart");
1084
1085 let mut mapped = pip.clone();
1087 mapped.push(sys_cudart);
1088
1089 let selected =
1090 complete_mapped_cuda_stack(&mapped).expect("continue the fully-mapped pip stack");
1091
1092 assert_eq!(selected.len(), pip.len());
1093 assert!(pip.iter().all(|path| {
1094 let canonical = path.canonicalize().expect("canonical fake library");
1095 selected.contains(&canonical)
1096 }));
1097 }
1098}