Skip to main content

gam_gpu/
driver.rs

1//! Shared CUDA driver presence/loading helpers used by every cuBLAS / cuSPARSE
2//! / cuSOLVER routing module.
3//!
4//! The GPU path uses ONE context model: cudarc's device PRIMARY context
5//! (`cuDevicePrimaryCtxRetain`, bound in `device_runtime::cuda_context_for`).
6//! cuBLAS/cuSOLVER/cuSPARSE handles attach to that current context; there is no
7//! separate user `cuCtxCreate` context (its removal fixed the #1017
8//! NOT_INITIALIZED handle failures). This module keeps only the libcuda
9//! presence probes, byte-size/layout helpers, and the `check_cuda` status wrap.
10
11use 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// NOTE (#1017): the `DriverApi` / `CudaWorkingState` / `DeviceAllocation` cluster
25// that lived here was REMOVED. It created a SEPARATE user CUDA context via
26// `cuCtxCreate` — distinct from cudarc's device PRIMARY context (cuDevicePrimaryCtxRetain)
27// that the live GPU path actually uses — which is the documented cause of the
28// cuBLAS/cuSOLVER NOT_INITIALIZED handle failures (handles bind to whichever
29// context is current). The cluster had ZERO consumers once the runtime routed
30// through `cuda_context_for` (the primary context) in `device_runtime.rs`, so it
31// was dead dual-context code. Keep ONE context model: the cudarc primary context.
32// Do not reintroduce `cuCtxCreate` for issuing work.
33
34#[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/// Bind to a CUDA driver that is ALREADY RESIDENT in this process, if any.
46///
47/// `RTLD_NOLOAD` makes `dlopen` return a handle only when some other
48/// component (e.g. torch) has already mapped the library — it never loads a
49/// new copy. Inside a torch process, walking the candidate list below can
50/// dlopen a SECOND driver instance (system driver vs the CUDA-toolkit compat
51/// driver at `/usr/local/cuda*/compat/`): CUDA contexts created by torch's
52/// instance are invisible to ours, which is the measured dual-stack failure
53/// "no CUDA context for ordinal 0" (and steering the loader at the compat
54/// driver instead breaks torch with error 803) — gam#2259. Binding to the
55/// resident copy first guarantees ONE driver instance per process, so gam's
56/// runtime shares torch's contexts; a standalone process has nothing
57/// resident and falls through to the candidate walk unchanged.
58#[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        // SAFETY: RTLD_NOLOAD never runs a new loader initializer — it only
63        // binds to a library some other component already loaded.
64        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        // SAFETY: Library::new runs the library's loader initializer; we
79        // only pass CUDA driver candidates discovered from fixed NVIDIA
80        // driver directories or canonical libcuda sonames.
81        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
109/// True only when a failed loader attempt proves that the requested candidate
110/// itself is absent. A named object that exists but is corrupt, ABI-incompatible,
111/// or missing a transitive dependency is a load fault. For bare sonames, glibc's
112/// missing-object diagnostic begins with the requested soname; a missing
113/// transitive dependency begins with that dependency instead and is therefore
114/// deliberately not classified as absence.
115fn 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
145/// Lossless CUDA-driver presence probe. `Ok(false)` means every candidate was
146/// genuinely absent; loader/ABI/transitive-dependency faults remain `Err`.
147pub 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                // SAFETY: these candidates are CUDA userspace libraries found
208                // in canonical toolkit directories or pip's nvidia-*-cu12
209                // wheel layout. RTLD_GLOBAL is required so transitive deps
210                // such as libcusolver -> libnvJitLink resolve without an
211                // LD_LIBRARY_PATH mutation.
212                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
229/// Require that the platform loader can open the named CUDA compute library
230/// (`cublas`, `cusolver`, `cusparse`) from the one selected userspace stack.
231///
232/// cudarc 0.19 attempts to lazy-load these via its own generated
233/// `panic_no_lib_found` helpers the first time `CudaBlas::new` /
234/// `DnHandle::new` / cuSPARSE handle creation is invoked. On a host that
235/// has only the CUDA *driver* (e.g. large-scale workbench images expose
236/// `libcuda.so.1` but no cuBLAS at all), those calls panic out of the
237/// PyO3 FFI boundary instead of returning a typed error.
238///
239/// `GpuRuntime::probe()` calls this for every compute library it depends on;
240/// failure retains the exact stack-selection or loader error in the typed GPU
241/// refusal and keeps cudarc's panic completely off the call path.
242pub fn require_cuda_compute_library(stem: &str) -> Result<(), String> {
243    // Cache the probe per stem and KEEP the loaded handle alive for the process
244    // lifetime. Dropping the `Library` here dlclose's it; that dlopen+dlclose
245    // cycle tears down the compute library's global init state, after which
246    // cudarc's own cublasCreate / cusolverDnCreate fail
247    // CUBLAS/CUSOLVER_STATUS_NOT_INITIALIZED on the next handle creation (the GPU
248    // then silently declines and falls back to CPU). Holding the handle keeps the
249    // library mapped and initialized so cudarc reuses it intact.
250    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    // A host package such as PyTorch may already own the process's CUDA
283    // userspace stack. Loading gam's system-first stack on top of that maps a
284    // second cudart/cuBLAS implementation and splits context/handle ownership.
285    // Continue the already-mapped stack instead: pip NVIDIA wheels are spread
286    // across component directories under one `nvidia/` root, while a system
287    // toolkit keeps this preload set in one directory.
288    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/// The CUDA component a userspace library path belongs to, taken from its
311/// SONAME stem (`libcublas.so.12` -> `cublas`, `libnvJitLink.so.12` ->
312/// `nvJitLink`). Used to reason about which mapped libraries are the same
313/// component sourced from different roots.
314#[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/// The CUDA *compute* libraries. These are handle-based and share context /
326/// workspace state with whatever copy the process already initialised, so a
327/// second copy must never be mapped from a different root — that split is the
328/// double-free / `NOT_INITIALIZED` hazard the mapped-stack continuation exists
329/// to avoid. `cudart` / `nvJitLink` are runtime/driver-adjacent and tolerate a
330/// redundant duplicate (dlopen-by-SONAME binds one deterministically).
331#[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    // Canonical path + component for every mapped library, computed once.
355    let mapped_meta: Vec<(PathBuf, Option<String>)> = mapped
356        .iter()
357        .map(|m| (canonical(m), cuda_library_component(m)))
358        .collect();
359
360    // Fast path: a single complete stack that contains EVERY mapped library.
361    // This is the unchanged behaviour for a process whose CUDA userspace all
362    // comes from one root (a pure system toolkit or a pure pip-wheel stack).
363    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    // Split-stack path: a process can legitimately map a system `libcudart`
375    // (pulled onto the default loader path by ldconfig) alongside a host
376    // framework's pip-wheel stack (e.g. PyTorch's `nvidia-*-cu12` wheels), so
377    // NO single complete stack contains every mapped path. Continue a stack
378    // only when the process is genuinely using ALL of it, and only tolerate a
379    // benign duplicate — never a split of the handle-based compute libraries:
380    //
381    //   (a) every library in the candidate stack is already mapped — we
382    //       CONTINUE a stack the process runs on, we never introduce a library
383    //       from a partially-present root; and
384    //   (b) every mapped library is either part of that stack or a redundant
385    //       duplicate of a NON-compute component (`cudart` / `nvJitLink`) the
386    //       stack already provides. A mapped compute library (`cuBLAS` /
387    //       `cuSOLVER` / `cuSPARSE`) from another root is a genuine split and
388    //       disqualifies the candidate — mapping a second copy is the
389    //       double-free / `NOT_INITIALIZED` hazard this continuation avoids.
390    //
391    // Libraries mixed across two partially-mapped roots satisfy neither, so the
392    // conservative refusal below still fires for them.
393    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    // Bare soname forms — exercised by the platform loader against
472    // LD_LIBRARY_PATH and the default search dirs.
473    out.push(format!("{base}.so"));
474    out.push(format!("{base}.so.1"));
475    // Major-version walk mirroring cudarc's own candidate list so the
476    // preflight agrees with whatever cudarc would have tried next.
477    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
829/// Repack a 2D `ndarray::ArrayBase` (row-major) into the column-major
830/// layout expected by every cuBLAS / cuSOLVER entry point.
831///
832/// Walks each column once via ndarray's iter (no per-element bounds checks)
833/// and extends into a pre-sized `Vec`. On large-scale inputs (n≈3×10⁵,
834/// p≈35) this replaces a per-element `a[[row, col]]` indexing loop that
835/// dominated the host side of every GPU dispatch.
836///
837/// Fast path: if the input is already F-order (column-major, contiguous in
838/// memory-order), borrow its raw buffer directly — no allocation, no copy.
839/// Standard row-major ndarrays still go through the permutation path.
840pub 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    // F-order contiguous: column stride == 1, row stride == rows.
844    // `as_slice_memory_order` confirms the buffer is contiguous in memory.
845    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
860/// Borrow (or pack) a 2D array's buffer in ROW-major (C) order.
861///
862/// The col-major dual of [`to_col_major`]: when the input is already
863/// C-contiguous its raw buffer IS the row-major flat layout, so this borrows
864/// it with no allocation or copy. Non-contiguous / F-order inputs are packed
865/// row by row.
866///
867/// This is the host-transpose-free upload path. A row-major `(r × c)` buffer,
868/// reinterpreted as a column-major buffer, is exactly the transpose `(c × r)`
869/// of the logical matrix — which is what the swapped-operand cuBLAS GEMM
870/// (`Cᵀ = Bᵀ·Aᵀ`) consumes, letting both the design upload and the result
871/// download skip the O(r·c) scalar permutation that dominated tall-skinny
872/// GEMMs on the host.
873pub 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    // C-order contiguous: row stride == cols, column stride == 1.
877    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
892/// Wrap a row-major flat buffer of shape `(rows, cols)` as an `Array2<f64>`
893/// without permutation. The buffer is consumed (no copy when its length
894/// matches). Returns `None` on a length mismatch.
895pub 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
902/// Convert a column-major flat buffer back into row-major `Array2<f64>`.
903pub 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        // Row-major [[1,2,3],[4,5,6]] → col-major [1,4,2,5,3,6]
963        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        // 2x3 = 6 elements, but only 5 provided
986        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]; // 9 expected, 8 given
993        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        // A process can map a system `libcudart` (pulled onto the default
1037        // loader path by ldconfig) alongside PyTorch's complete pip-wheel
1038        // stack. The whole pip stack is mapped; the extra system cudart is a
1039        // redundant duplicate of a non-compute component, so the pip stack is
1040        // continued rather than the GPU being refused (gam issue #2259).
1041        let temp = tempfile::tempdir().expect("temporary CUDA tree");
1042        let pip = fake_nvidia_stack(&temp.path().join("site-packages").join("nvidia"));
1043
1044        // A stray system cudart from an unrelated toolkit directory (no
1045        // complete stack of its own next to it).
1046        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        // Everything from the pip stack is mapped, plus the redundant cudart.
1052        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}