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