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(|| {
157 load_static_cuda_driver_library()?;
158 Ok(())
159 })
160 .clone()
161}
162
163pub fn cuda_driver_available() -> Result<bool, GpuError> {
166 match preload_cuda_driver() {
167 Ok(()) => Ok(true),
168 Err(GpuError::DriverLibraryUnavailable { .. }) => Ok(false),
169 Err(error) => Err(error),
170 }
171}
172
173#[cfg(test)]
174mod loader_classification_tests {
175 #[cfg(target_os = "linux")]
176 use super::library_load_error_detail;
177 use super::load_failure_is_candidate_absence;
178
179 #[cfg(target_os = "linux")]
180 #[test]
181 fn libloading_source_preserves_the_missing_bare_soname() {
182 const SONAME: &str = "libgamfit_cuda_driver_absence_probe.so.2411";
183 let error = match unsafe { libloading::Library::new(SONAME) } {
186 Ok(_) => panic!("the CUDA absence-probe soname unexpectedly exists"),
187 Err(error) => error,
188 };
189 let detail = library_load_error_detail(&error);
190 assert!(
191 load_failure_is_candidate_absence(SONAME, false, &detail),
192 "missing-soname detail was not classified as absence: {detail}"
193 );
194 }
195
196 #[test]
197 fn missing_bare_soname_is_absence() {
198 assert!(load_failure_is_candidate_absence(
199 "libcuda.so.1",
200 false,
201 "libcuda.so.1: cannot open shared object file: No such file or directory",
202 ));
203 }
204
205 #[test]
206 fn missing_transitive_dependency_is_a_load_fault() {
207 assert!(!load_failure_is_candidate_absence(
208 "libcuda.so.1",
209 false,
210 "libnvidia-fatbinaryloader.so.555: cannot open shared object file: No such file or directory",
211 ));
212 }
213
214 #[test]
215 fn present_but_invalid_absolute_candidate_is_a_load_fault() {
216 assert!(!load_failure_is_candidate_absence(
217 "/opt/cuda/libcuda.so.1",
218 true,
219 "/opt/cuda/libcuda.so.1: invalid ELF header",
220 ));
221 }
222
223 #[test]
224 fn absent_absolute_candidate_is_absence() {
225 assert!(load_failure_is_candidate_absence(
226 "/opt/cuda/libcuda.so.1",
227 false,
228 "/opt/cuda/libcuda.so.1: cannot open shared object file: No such file or directory",
229 ));
230 }
231}
232
233#[cfg(target_os = "linux")]
234fn preload_cuda_userspace_libraries() -> Result<(), String> {
235 static PRELOAD: OnceLock<Result<Vec<UnixLibrary>, String>> = OnceLock::new();
236 PRELOAD
237 .get_or_init(|| {
238 let paths = cuda_userspace_preload_paths()?;
239 if paths.is_empty() {
240 return Ok(Vec::new());
241 }
242 let mut loaded = Vec::new();
243 for path in paths {
244 match unsafe { UnixLibrary::open(Some(&path), RTLD_NOW | RTLD_GLOBAL) } {
250 Ok(library) => loaded.push(library),
251 Err(err) => {
252 return Err(format!(
253 "could not preload CUDA userspace library {}: {err}",
254 path.display()
255 ));
256 }
257 }
258 }
259 Ok(loaded)
260 })
261 .as_ref()
262 .map_err(Clone::clone)?;
263 Ok(())
266}
267
268pub fn require_cuda_compute_library(stem: &str) -> Result<(), String> {
282 static PROBED: OnceLock<
290 std::sync::Mutex<std::collections::HashMap<String, Result<(), String>>>,
291 > = OnceLock::new();
292 static KEEP_ALIVE: OnceLock<std::sync::Mutex<Vec<Library>>> = OnceLock::new();
293 let probed = PROBED.get_or_init(|| std::sync::Mutex::new(std::collections::HashMap::new()));
294 if let Ok(cache) = probed.lock() {
295 if let Some(outcome) = cache.get(stem) {
296 return outcome.clone();
297 }
298 }
299 #[cfg(target_os = "linux")]
300 preload_cuda_userspace_libraries()?;
301 let outcome = match load_library_names(&cuda_compute_library_candidate_names(stem)) {
302 Ok(library) => {
303 if let Ok(mut keep) = KEEP_ALIVE
304 .get_or_init(|| std::sync::Mutex::new(Vec::new()))
305 .lock()
306 {
307 keep.push(library);
308 }
309 Ok(())
310 }
311 Err(error) => Err(error.to_string()),
312 };
313 if let Ok(mut cache) = probed.lock() {
314 cache.insert(stem.to_string(), outcome.clone());
315 }
316 outcome
317}
318
319#[cfg(target_os = "linux")]
320fn cuda_userspace_preload_paths() -> Result<Vec<PathBuf>, String> {
321 let mapped = mapped_cuda_userspace_libraries()?;
328 if !mapped.is_empty() {
329 return complete_mapped_cuda_stack(&mapped);
330 }
331
332 let system_dirs = cuda_system_library_dirs();
333 for dir in &system_dirs {
334 if let Some(stack) = complete_system_cuda_stack(dir) {
335 return Ok(dedup_paths(stack));
336 }
337 if let Some(stack) = system_cuda_stack_with_packaged_nvjitlink(dir) {
338 return Ok(dedup_paths(stack));
339 }
340 }
341 for root in nvidia_package_roots() {
342 if let Some(stack) = complete_nvidia_cuda_stack(&root) {
343 return Ok(dedup_paths(stack));
344 }
345 }
346 Ok(Vec::new())
347}
348
349#[cfg(target_os = "linux")]
354fn cuda_library_component(path: &Path) -> Option<String> {
355 let name = path.file_name()?.to_str()?;
356 let stem = name.strip_prefix("lib")?.split(".so").next()?;
357 if stem.is_empty() {
358 None
359 } else {
360 Some(stem.to_string())
361 }
362}
363
364#[cfg(target_os = "linux")]
371fn is_cuda_compute_component(component: &str) -> bool {
372 matches!(component, "cublas" | "cublasLt" | "cusolver" | "cusparse")
373}
374
375#[cfg(target_os = "linux")]
376fn complete_mapped_cuda_stack(mapped: &[PathBuf]) -> Result<Vec<PathBuf>, String> {
377 let canonical = |p: &Path| {
378 p.canonicalize().unwrap_or_else(|error| {
379 log::debug!(
380 "cuda stack: cannot canonicalize {}: {error}; \
381 comparing the path as given",
382 p.display()
383 );
384 p.to_path_buf()
385 })
386 };
387
388 let mut candidates = Vec::new();
389 for path in mapped {
390 if let Some(root) = nvidia_package_root_for_library(path)
391 && let Some(stack) = complete_nvidia_cuda_stack(&root)
392 {
393 candidates.push(stack);
394 }
395 if let Some(parent) = path.parent()
396 && let Some(stack) = complete_system_cuda_stack_path(parent)
397 {
398 candidates.push(stack);
399 }
400 }
401
402 let mapped_meta: Vec<(PathBuf, Option<String>)> = mapped
404 .iter()
405 .map(|m| (canonical(m), cuda_library_component(m)))
406 .collect();
407
408 for stack in &candidates {
412 let stack = dedup_paths(stack.clone());
413 let stack_canon: Vec<PathBuf> = stack.iter().map(|c| canonical(c)).collect();
414 if mapped_meta
415 .iter()
416 .all(|(m, _)| stack_canon.iter().any(|c| c == m))
417 {
418 return Ok(stack);
419 }
420 }
421
422 for stack in candidates {
442 let stack = dedup_paths(stack);
443 let stack_canon: Vec<PathBuf> = stack.iter().map(|c| canonical(c)).collect();
444 let fully_mapped = stack_canon
445 .iter()
446 .all(|s| mapped_meta.iter().any(|(m, _)| m == s));
447 if !fully_mapped {
448 continue;
449 }
450 let consistent = mapped_meta.iter().all(|(m, component)| {
451 if stack_canon.iter().any(|s| s == m) {
452 return true;
453 }
454 match component {
455 Some(component) if !is_cuda_compute_component(component) => stack
456 .iter()
457 .any(|s| cuda_library_component(s).as_deref() == Some(component)),
458 _ => false,
459 }
460 });
461 if consistent {
462 return Ok(stack);
463 }
464 }
465
466 Err(format!(
467 "CUDA userspace is already mapped from no single complete stack: {}",
468 mapped
469 .iter()
470 .map(|path| path.display().to_string())
471 .collect::<Vec<_>>()
472 .join(", ")
473 ))
474}
475
476#[cfg(target_os = "linux")]
477fn mapped_cuda_userspace_libraries() -> Result<Vec<PathBuf>, String> {
478 let maps = std::fs::read_to_string("/proc/self/maps")
479 .map_err(|error| format!("cannot inspect mapped CUDA userspace libraries: {error}"))?;
480 let mut mapped = Vec::new();
481 for line in maps.lines() {
482 let Some(raw_path) = line.split_whitespace().last() else {
483 continue;
484 };
485 if !raw_path.starts_with('/') {
486 continue;
487 }
488 let path = PathBuf::from(raw_path);
489 let Some(name) = path.file_name().and_then(|name| name.to_str()) else {
490 continue;
491 };
492 if [
493 "libcudart.so",
494 "libnvJitLink.so",
495 "libcublasLt.so",
496 "libcublas.so",
497 "libcusparse.so",
498 "libcusolver.so",
499 ]
500 .iter()
501 .any(|prefix| name.starts_with(prefix))
502 {
503 mapped.push(path);
504 }
505 }
506 Ok(dedup_paths(mapped))
507}
508
509#[cfg(target_os = "linux")]
510fn nvidia_package_root_for_library(path: &Path) -> Option<PathBuf> {
511 path.ancestors()
512 .find(|ancestor| ancestor.file_name().and_then(|name| name.to_str()) == Some("nvidia"))
513 .map(Path::to_path_buf)
514}
515
516fn cuda_compute_library_candidate_names(stem: &str) -> Vec<String> {
517 let base = format!("lib{stem}");
518 let mut out: Vec<String> = Vec::new();
519 out.push(format!("{base}.so"));
522 out.push(format!("{base}.so.1"));
523 for major in (9..=13).rev() {
526 out.push(format!("{base}.so.{major}"));
527 }
528 #[cfg(target_os = "linux")]
529 {
530 for dir in cuda_system_library_dirs() {
531 out.push(format!("{dir}/{base}.so"));
532 for major in (9..=13).rev() {
533 out.push(format!("{dir}/{base}.so.{major}"));
534 }
535 append_versioned_linux_so_candidates(&mut out, Path::new(dir), &base);
536 }
537 for root in nvidia_package_roots() {
538 let lib_dir = root.join(nvidia_component_for_stem(stem)).join("lib");
539 out.push(format!("{}/{}.so", lib_dir.display(), base));
540 for major in (9..=13).rev() {
541 out.push(format!("{}/{}.so.{major}", lib_dir.display(), base));
542 }
543 append_versioned_linux_so_candidates(&mut out, &lib_dir, &base);
544 }
545 }
546 out
547}
548
549#[cfg(target_os = "linux")]
550fn cuda_system_library_dirs() -> Vec<&'static str> {
551 vec![
552 "/usr/local/cuda/lib64",
553 "/usr/local/cuda/lib",
554 "/usr/local/cuda/targets/x86_64-linux/lib",
555 "/usr/lib/x86_64-linux-gnu",
556 "/usr/lib64",
557 "/usr/lib/wsl/lib",
558 "/opt/cuda/lib64",
559 ]
560}
561
562#[cfg(target_os = "linux")]
563fn complete_system_cuda_stack(dir: &str) -> Option<Vec<PathBuf>> {
564 complete_system_cuda_stack_path(Path::new(dir))
565}
566
567#[cfg(target_os = "linux")]
568fn complete_system_cuda_stack_path(dir: &Path) -> Option<Vec<PathBuf>> {
569 let stack = vec![
570 first_existing(dir, &["libcudart.so.13", "libcudart.so.12", "libcudart.so"])?,
571 first_existing(
572 dir,
573 &[
574 "libnvJitLink.so.13",
575 "libnvJitLink.so.12",
576 "libnvJitLink.so",
577 ],
578 )?,
579 first_existing(
580 dir,
581 &["libcublasLt.so.13", "libcublasLt.so.12", "libcublasLt.so"],
582 )?,
583 first_existing(dir, &["libcublas.so.13", "libcublas.so.12", "libcublas.so"])?,
584 first_existing(
585 dir,
586 &["libcusparse.so.13", "libcusparse.so.12", "libcusparse.so"],
587 )?,
588 first_existing(
589 dir,
590 &[
591 "libcusolver.so.13",
592 "libcusolver.so.12",
593 "libcusolver.so.11",
594 "libcusolver.so",
595 ],
596 )?,
597 ];
598 Some(stack)
599}
600
601#[cfg(target_os = "linux")]
602fn system_cuda_stack_with_packaged_nvjitlink(dir: &str) -> Option<Vec<PathBuf>> {
603 let dir = Path::new(dir);
604 let nvjitlink = packaged_nvjitlink_library()?;
605 let stack = vec![
606 first_existing(dir, &["libcudart.so.13", "libcudart.so.12", "libcudart.so"])?,
607 nvjitlink,
608 first_existing(
609 dir,
610 &["libcublasLt.so.13", "libcublasLt.so.12", "libcublasLt.so"],
611 )?,
612 first_existing(dir, &["libcublas.so.13", "libcublas.so.12", "libcublas.so"])?,
613 first_existing(
614 dir,
615 &["libcusparse.so.13", "libcusparse.so.12", "libcusparse.so"],
616 )?,
617 first_existing(
618 dir,
619 &[
620 "libcusolver.so.13",
621 "libcusolver.so.12",
622 "libcusolver.so.11",
623 "libcusolver.so",
624 ],
625 )?,
626 ];
627 Some(stack)
628}
629
630#[cfg(target_os = "linux")]
631fn complete_nvidia_cuda_stack(root: &Path) -> Option<Vec<PathBuf>> {
632 let stack = vec![
633 first_existing(
634 &root.join("cuda_runtime").join("lib"),
635 &["libcudart.so.13", "libcudart.so.12", "libcudart.so"],
636 )?,
637 first_existing(
638 &root.join("nvjitlink").join("lib"),
639 &[
640 "libnvJitLink.so.13",
641 "libnvJitLink.so.12",
642 "libnvJitLink.so",
643 ],
644 )?,
645 first_existing(
646 &root.join("cublas").join("lib"),
647 &["libcublasLt.so.13", "libcublasLt.so.12", "libcublasLt.so"],
648 )?,
649 first_existing(
650 &root.join("cublas").join("lib"),
651 &["libcublas.so.13", "libcublas.so.12", "libcublas.so"],
652 )?,
653 first_existing(
654 &root.join("cusparse").join("lib"),
655 &["libcusparse.so.13", "libcusparse.so.12", "libcusparse.so"],
656 )?,
657 first_existing(
658 &root.join("cusolver").join("lib"),
659 &[
660 "libcusolver.so.13",
661 "libcusolver.so.12",
662 "libcusolver.so.11",
663 "libcusolver.so",
664 ],
665 )?,
666 ];
667 Some(stack)
668}
669
670#[cfg(target_os = "linux")]
671fn packaged_nvjitlink_library() -> Option<PathBuf> {
672 for root in nvidia_package_roots() {
673 let lib_dir = root.join("nvjitlink").join("lib");
674 if let Some(path) = first_existing(
675 &lib_dir,
676 &[
677 "libnvJitLink.so.13",
678 "libnvJitLink.so.12",
679 "libnvJitLink.so",
680 ],
681 ) {
682 return Some(path);
683 }
684 }
685 None
686}
687
688#[cfg(target_os = "linux")]
689fn nvidia_component_for_stem(stem: &str) -> String {
690 match stem {
691 "cublas" => "cublas".to_string(),
692 "cusolver" => "cusolver".to_string(),
693 "cusparse" => "cusparse".to_string(),
694 "nvJitLink" | "nvjitlink" => "nvjitlink".to_string(),
695 "cudart" | "cuda_runtime" => "cuda_runtime".to_string(),
696 _ => stem.to_string(),
697 }
698}
699
700#[cfg(target_os = "linux")]
701fn nvidia_package_roots() -> Vec<PathBuf> {
702 let mut roots = Vec::new();
703 if let Some(home) = current_user_home_dir() {
704 collect_python_nvidia_roots(home.join(".local/lib"), &mut roots);
705 }
706 collect_python_nvidia_roots(Path::new("/usr/local/lib").to_path_buf(), &mut roots);
707 collect_python_nvidia_roots(Path::new("/usr/lib").to_path_buf(), &mut roots);
708 dedup_paths(roots)
709}
710
711#[cfg(target_os = "linux")]
712fn current_user_home_dir() -> Option<PathBuf> {
713 let status = std::fs::read_to_string("/proc/self/status").ok()?;
714 let uid = status
715 .lines()
716 .find_map(|line| line.strip_prefix("Uid:"))?
717 .split_whitespace()
718 .next()?;
719 let passwd = std::fs::read_to_string("/etc/passwd").ok()?;
720 for line in passwd.lines() {
721 let mut fields = line.split(':');
722 fields.next()?;
723 fields.next()?;
724 if fields.next()? != uid {
725 continue;
726 }
727 fields.next()?;
728 fields.next()?;
729 return Some(PathBuf::from(fields.next()?));
730 }
731 None
732}
733
734#[cfg(target_os = "linux")]
735fn collect_python_nvidia_roots(base: PathBuf, out: &mut Vec<PathBuf>) {
736 let Ok(entries) = std::fs::read_dir(base) else {
737 return;
738 };
739 for entry in entries.flatten() {
740 let path = entry.path();
741 let Some(name) = path.file_name().and_then(|name| name.to_str()) else {
742 continue;
743 };
744 if !name.starts_with("python") {
745 continue;
746 }
747 for site_dir in ["site-packages", "dist-packages"] {
748 let root = path.join(site_dir).join("nvidia");
749 if root.exists() {
750 out.push(root);
751 }
752 }
753 }
754}
755
756#[cfg(target_os = "linux")]
757fn first_existing(dir: &Path, names: &[&str]) -> Option<PathBuf> {
758 for name in names {
759 let path = dir.join(name);
760 if path.exists() {
761 return Some(path);
762 }
763 }
764 None
765}
766
767#[cfg(target_os = "linux")]
768fn dedup_paths(paths: Vec<PathBuf>) -> Vec<PathBuf> {
769 let mut out = Vec::new();
770 for path in paths {
771 let canonical = path.canonicalize().unwrap_or(path);
772 if !out.iter().any(|existing| existing == &canonical) {
773 out.push(canonical);
774 }
775 }
776 out
777}
778
779#[cfg(target_os = "linux")]
780fn append_versioned_linux_so_candidates(out: &mut Vec<String>, dir: &Path, base: &str) {
781 let Ok(entries) = std::fs::read_dir(dir) else {
782 return;
783 };
784 let prefix = format!("{base}.so.");
785 let mut versioned = Vec::new();
786 for entry in entries.flatten() {
787 let path = entry.path();
788 let Some(name) = path.file_name().and_then(|name| name.to_str()) else {
789 continue;
790 };
791 if name.starts_with(&prefix) {
792 versioned.push(path);
793 }
794 }
795 versioned.sort();
796 for path in versioned {
797 let candidate = path.to_string_lossy().into_owned();
798 if !out.iter().any(|existing| existing == &candidate) {
799 out.push(candidate);
800 }
801 }
802}
803
804fn cuda_library_candidate_names() -> Vec<String> {
805 let mut out: Vec<String> = cuda_library_candidates()
806 .iter()
807 .map(|candidate| (*candidate).to_string())
808 .collect();
809 if cfg!(target_os = "linux") {
810 for dir in [
811 "/usr/local/nvidia/lib64",
812 "/usr/local/nvidia/lib",
813 "/usr/local/cuda/compat",
814 "/usr/lib/x86_64-linux-gnu",
815 "/usr/lib64",
816 "/usr/lib/wsl/lib",
817 ] {
818 append_versioned_linux_libcuda_candidates(&mut out, Path::new(dir));
819 }
820 }
821 out
822}
823
824fn append_versioned_linux_libcuda_candidates(out: &mut Vec<String>, dir: &Path) {
825 let Ok(entries) = std::fs::read_dir(dir) else {
826 return;
827 };
828 let mut versioned = Vec::new();
829 for entry in entries.flatten() {
830 let path = entry.path();
831 let Some(name) = path.file_name().and_then(|name| name.to_str()) else {
832 continue;
833 };
834 if name.starts_with("libcuda.so.") && name != "libcuda.so.1" {
835 versioned.push(path);
836 }
837 }
838 versioned.sort();
839 for path in versioned {
840 let candidate = path.to_string_lossy().into_owned();
841 if !out.iter().any(|existing| existing == &candidate) {
842 out.push(candidate);
843 }
844 }
845}
846
847pub fn cuda_library_candidates() -> &'static [&'static str] {
848 if cfg!(target_os = "windows") {
849 &["nvcuda.dll"]
850 } else if cfg!(target_os = "macos") {
851 &["/usr/local/cuda/lib/libcuda.dylib", "libcuda.dylib"]
852 } else {
853 &[
854 "/usr/local/nvidia/lib64/libcuda.so.1",
855 "/usr/local/nvidia/lib64/libcuda.so",
856 "/usr/local/nvidia/lib/libcuda.so.1",
857 "/usr/local/nvidia/lib/libcuda.so",
858 "/usr/local/cuda/compat/libcuda.so.1",
859 "/usr/local/cuda/compat/libcuda.so",
860 "/usr/lib/x86_64-linux-gnu/libcuda.so.1",
861 "/usr/lib/x86_64-linux-gnu/libcuda.so",
862 "/usr/lib64/libcuda.so.1",
863 "/usr/lib64/libcuda.so",
864 "/usr/lib/wsl/lib/libcuda.so.1",
865 "/usr/lib/wsl/lib/libcuda.so",
866 "libcuda.so.1",
867 "libcuda.so",
868 ]
869 }
870}
871
872#[inline]
873pub fn to_i32(value: usize) -> Option<i32> {
874 i32::try_from(value).ok()
875}
876
877pub fn to_col_major<'a, S: Data<Elem = f64>>(a: &'a ArrayBase<S, Ix2>) -> Cow<'a, [f64]> {
889 let (rows, cols) = a.dim();
890 let strides = a.strides();
891 if rows > 0
894 && cols > 0
895 && strides[0] == 1
896 && strides[1] == rows as isize
897 && let Some(slice) = a.as_slice_memory_order()
898 {
899 return Cow::Borrowed(slice);
900 }
901 let mut out: Vec<f64> = Vec::with_capacity(rows.saturating_mul(cols));
902 for col in 0..cols {
903 out.extend(a.column(col).iter().copied());
904 }
905 Cow::Owned(out)
906}
907
908pub fn to_row_major<'a, S: Data<Elem = f64>>(a: &'a ArrayBase<S, Ix2>) -> Cow<'a, [f64]> {
922 let (rows, cols) = a.dim();
923 let strides = a.strides();
924 if rows > 0
926 && cols > 0
927 && strides[1] == 1
928 && strides[0] == cols as isize
929 && let Some(slice) = a.as_slice_memory_order()
930 {
931 return Cow::Borrowed(slice);
932 }
933 let mut out: Vec<f64> = Vec::with_capacity(rows.saturating_mul(cols));
934 for row in 0..rows {
935 out.extend(a.row(row).iter().copied());
936 }
937 Cow::Owned(out)
938}
939
940pub fn array_from_row_major(values: Vec<f64>, rows: usize, cols: usize) -> Option<Array2<f64>> {
944 if values.len() != rows.checked_mul(cols)? {
945 return None;
946 }
947 Array2::from_shape_vec((rows, cols), values).ok()
948}
949
950pub fn from_col_major_inplace(values: &[f64], out: &mut Array2<f64>) -> Option<()> {
952 let (rows, cols) = out.dim();
953 if values.len() != rows.checked_mul(cols)? {
954 return None;
955 }
956 for col in 0..cols {
957 let src = ndarray::ArrayView1::from(&values[col * rows..(col + 1) * rows]);
958 out.column_mut(col).assign(&src);
959 }
960 Some(())
961}
962
963pub fn from_col_major(values: &[f64], rows: usize, cols: usize) -> Option<Array2<f64>> {
964 let mut out = Array2::<f64>::zeros((rows, cols));
965 from_col_major_inplace(values, &mut out)?;
966 Some(out)
967}
968
969#[cfg(test)]
970mod tests {
971 use super::*;
972 use ndarray::array;
973
974 #[cfg(target_os = "linux")]
975 fn fake_nvidia_stack(root: &Path) -> Vec<PathBuf> {
976 let libraries = [
977 ("cuda_runtime", "libcudart.so.12"),
978 ("nvjitlink", "libnvJitLink.so.12"),
979 ("cublas", "libcublasLt.so.12"),
980 ("cublas", "libcublas.so.12"),
981 ("cusparse", "libcusparse.so.12"),
982 ("cusolver", "libcusolver.so.11"),
983 ];
984 libraries
985 .into_iter()
986 .map(|(component, name)| {
987 let path = root.join(component).join("lib").join(name);
988 std::fs::create_dir_all(path.parent().expect("library parent"))
989 .expect("create fake CUDA component directory");
990 std::fs::write(&path, []).expect("create fake CUDA library");
991 path
992 })
993 .collect()
994 }
995
996 #[test]
997 fn to_i32_fits_small_value() {
998 assert_eq!(to_i32(0), Some(0));
999 assert_eq!(to_i32(42), Some(42));
1000 assert_eq!(to_i32(i32::MAX as usize), Some(i32::MAX));
1001 }
1002
1003 #[test]
1004 fn to_i32_overflows_returns_none() {
1005 assert_eq!(to_i32(i32::MAX as usize + 1), None);
1006 }
1007
1008 #[test]
1009 fn to_col_major_2x3_row_major() {
1010 let a = array![[1.0_f64, 2.0, 3.0], [4.0, 5.0, 6.0]];
1012 let col = to_col_major(&a);
1013 assert_eq!(&*col, &[1.0, 4.0, 2.0, 5.0, 3.0, 6.0]);
1014 }
1015
1016 #[test]
1017 fn to_col_major_identity_roundtrip() {
1018 let a = array![[1.0_f64, 0.0], [0.0, 1.0]];
1019 let col = to_col_major(&a);
1020 assert_eq!(&*col, &[1.0, 0.0, 0.0, 1.0]);
1021 }
1022
1023 #[test]
1024 fn from_col_major_2x3_roundtrip() {
1025 let original = array![[1.0_f64, 2.0, 3.0], [4.0, 5.0, 6.0]];
1026 let col = to_col_major(&original);
1027 let recovered = from_col_major(&col, 2, 3).expect("should succeed");
1028 assert_eq!(recovered, original);
1029 }
1030
1031 #[test]
1032 fn from_col_major_wrong_length_returns_none() {
1033 assert!(from_col_major(&[1.0, 2.0, 3.0, 4.0, 5.0], 2, 3).is_none());
1035 }
1036
1037 #[test]
1038 fn from_col_major_inplace_mismatched_buffer_returns_none() {
1039 let mut out = Array2::<f64>::zeros((3, 3));
1040 let short = vec![1.0_f64; 8]; assert!(from_col_major_inplace(&short, &mut out).is_none());
1042 }
1043
1044 #[test]
1045 fn from_col_major_single_element() {
1046 let result = from_col_major(&[7.0], 1, 1).expect("should succeed");
1047 assert_eq!(result[[0, 0]], 7.0);
1048 }
1049
1050 #[cfg(target_os = "linux")]
1051 #[test]
1052 fn mapped_pytorch_stack_is_continued_as_one_complete_stack() {
1053 let temp = tempfile::tempdir().expect("temporary CUDA tree");
1054 let root = temp.path().join("site-packages").join("nvidia");
1055 let stack = fake_nvidia_stack(&root);
1056 let mapped = vec![stack[0].clone(), stack[3].clone()];
1057
1058 let selected = complete_mapped_cuda_stack(&mapped).expect("one mapped NVIDIA root");
1059
1060 assert_eq!(selected.len(), stack.len());
1061 assert!(mapped.iter().all(|path| {
1062 let canonical = path.canonicalize().expect("canonical fake library");
1063 selected.contains(&canonical)
1064 }));
1065 }
1066
1067 #[cfg(target_os = "linux")]
1068 #[test]
1069 fn mapped_mixed_cuda_stacks_are_refused() {
1070 let temp = tempfile::tempdir().expect("temporary CUDA tree");
1071 let first = fake_nvidia_stack(&temp.path().join("first").join("nvidia"));
1072 let second = fake_nvidia_stack(&temp.path().join("second").join("nvidia"));
1073 let mapped = vec![first[0].clone(), second[3].clone()];
1074
1075 let error = complete_mapped_cuda_stack(&mapped)
1076 .expect_err("libraries from two mapped roots must not be mixed");
1077
1078 assert!(error.contains("no single complete stack"));
1079 }
1080
1081 #[cfg(target_os = "linux")]
1082 #[test]
1083 fn mapped_pytorch_stack_with_redundant_system_cudart_is_continued() {
1084 let temp = tempfile::tempdir().expect("temporary CUDA tree");
1090 let pip = fake_nvidia_stack(&temp.path().join("site-packages").join("nvidia"));
1091
1092 let sys_dir = temp.path().join("usr").join("local").join("cuda").join("lib");
1095 std::fs::create_dir_all(&sys_dir).expect("system lib dir");
1096 let sys_cudart = sys_dir.join("libcudart.so.12");
1097 std::fs::write(&sys_cudart, []).expect("system cudart");
1098
1099 let mut mapped = pip.clone();
1101 mapped.push(sys_cudart);
1102
1103 let selected =
1104 complete_mapped_cuda_stack(&mapped).expect("continue the fully-mapped pip stack");
1105
1106 assert_eq!(selected.len(), pip.len());
1107 assert!(pip.iter().all(|path| {
1108 let canonical = path.canonicalize().expect("canonical fake library");
1109 selected.contains(&canonical)
1110 }));
1111 }
1112}