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