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::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// NOTE (#1017): the `DriverApi` / `CudaWorkingState` / `DeviceAllocation` cluster
26// that lived here was REMOVED. It created a SEPARATE user CUDA context via
27// `cuCtxCreate` — distinct from cudarc's device PRIMARY context (cuDevicePrimaryCtxRetain)
28// that the live GPU path actually uses — which is the documented cause of the
29// cuBLAS/cuSOLVER NOT_INITIALIZED handle failures (handles bind to whichever
30// context is current). The cluster had ZERO consumers once the runtime routed
31// through `cuda_context_for` (the primary context) in `device_runtime.rs`, so it
32// was dead dual-context code. Keep ONE context model: the cudarc primary context.
33// Do not reintroduce `cuCtxCreate` for issuing work.
34
35#[inline]
36pub fn check_cuda(result: CuResult, name: &str) -> Result<(), GpuError> {
37    if result == 0 {
38        Ok(())
39    } else {
40        Err(GpuError::DriverCallFailed {
41            reason: format!("{name} failed with CUDA driver error {result}"),
42        })
43    }
44}
45
46/// Bind to a CUDA driver that is ALREADY RESIDENT in this process, if any.
47///
48/// `RTLD_NOLOAD` makes `dlopen` return a handle only when some other
49/// component (e.g. torch) has already mapped the library — it never loads a
50/// new copy. Inside a torch process, walking the candidate list below can
51/// dlopen a SECOND driver instance (system driver vs the CUDA-toolkit compat
52/// driver at `/usr/local/cuda*/compat/`): CUDA contexts created by torch's
53/// instance are invisible to ours, which is the measured dual-stack failure
54/// "no CUDA context for ordinal 0" (and steering the loader at the compat
55/// driver instead breaks torch with error 803) — gam#2259. Binding to the
56/// resident copy first guarantees ONE driver instance per process, so gam's
57/// runtime shares torch's contexts; a standalone process has nothing
58/// resident and falls through to the candidate walk unchanged.
59#[cfg(target_os = "linux")]
60fn already_resident_cuda_driver() -> Option<Library> {
61    const RTLD_NOLOAD: std::os::raw::c_int = 0x4;
62    for soname in ["libcuda.so.1", "libcuda.so"] {
63        // SAFETY: RTLD_NOLOAD never runs a new loader initializer — it only
64        // binds to a library some other component already loaded.
65        if let Ok(library) = unsafe { UnixLibrary::open(Some(soname), RTLD_NOW | RTLD_NOLOAD) } {
66            return Some(library.into());
67        }
68    }
69    None
70}
71
72fn load_library_names(candidates: &[String]) -> Result<Library, GpuError> {
73    #[cfg(target_os = "linux")]
74    if let Some(resident) = already_resident_cuda_driver() {
75        return Ok(resident);
76    }
77    let mut load_faults = Vec::new();
78    for candidate in candidates {
79        // SAFETY: Library::new runs the library's loader initializer; we
80        // only pass CUDA driver candidates discovered from fixed NVIDIA
81        // driver directories or canonical libcuda sonames.
82        match unsafe { Library::new(candidate) } {
83            Ok(library) => return Ok(library),
84            Err(error) => {
85                let detail = library_load_error_detail(&error);
86                let candidate_present = Path::new(candidate).components().count() > 1
87                    && std::fs::symlink_metadata(candidate).is_ok();
88                if !load_failure_is_candidate_absence(candidate, candidate_present, &detail) {
89                    load_faults.push(format!("{candidate}: {detail}"));
90                }
91            }
92        }
93    }
94    if !load_faults.is_empty() {
95        return Err(GpuError::DriverLibraryLoadFailed {
96            reason: format!(
97                "CUDA library candidates were found but failed to load: {}",
98                load_faults.join("; ")
99            ),
100        });
101    }
102    Err(GpuError::DriverLibraryUnavailable {
103        reason: format!("could not load any of: {}", candidates.join(", ")),
104    })
105}
106
107/// Recover the platform loader's actual diagnostic from `libloading`.
108///
109/// `libloading 0.9` deliberately made its top-level `Display` stable and
110/// generic (`"dlopen failed"` / `"LoadLibraryExW failed"`); the `dlerror()` or
111/// Windows loader detail now lives in the standard error source chain. CUDA
112/// admission needs that detail to distinguish a genuinely absent driver from
113/// a present library with an ABI or transitive-dependency fault.
114fn library_load_error_detail(error: &libloading::Error) -> String {
115    let mut detail = error.to_string();
116    let mut source = StdError::source(error);
117    while let Some(cause) = source {
118        detail = cause.to_string();
119        source = cause.source();
120    }
121    detail
122}
123
124/// True only when a failed loader attempt proves that the requested candidate
125/// itself is absent. A named object that exists but is corrupt, ABI-incompatible,
126/// or missing a transitive dependency is a load fault. For bare sonames, glibc's
127/// missing-object diagnostic begins with the requested soname; a missing
128/// transitive dependency begins with that dependency instead and is therefore
129/// deliberately not classified as absence.
130fn load_failure_is_candidate_absence(
131    candidate: &str,
132    candidate_present: bool,
133    message: &str,
134) -> bool {
135    if Path::new(candidate).components().count() > 1 {
136        return !candidate_present;
137    }
138    let missing_object = message.starts_with(candidate)
139        && (message.contains("No such file or directory")
140            || message.contains("cannot open shared object file")
141            || message.contains("image not found"));
142    missing_object
143}
144
145fn load_static_cuda_driver_library() -> Result<&'static Library, GpuError> {
146    static LIBRARY: OnceLock<Result<Library, GpuError>> = OnceLock::new();
147    LIBRARY
148        .get_or_init(|| load_library_names(&cuda_library_candidate_names()))
149        .as_ref()
150        .map_err(Clone::clone)
151}
152
153pub fn preload_cuda_driver() -> Result<(), GpuError> {
154    static PRELOAD: OnceLock<Result<(), GpuError>> = OnceLock::new();
155    PRELOAD
156        .get_or_init(|| {
157            load_static_cuda_driver_library()?;
158            Ok(())
159        })
160        .clone()
161}
162
163/// Lossless CUDA-driver presence probe. `Ok(false)` means every candidate was
164/// genuinely absent; loader/ABI/transitive-dependency faults remain `Err`.
165pub fn cuda_driver_available() -> Result<bool, GpuError> {
166    match preload_cuda_driver() {
167        Ok(()) => Ok(true),
168        Err(GpuError::DriverLibraryUnavailable { .. }) => Ok(false),
169        Err(error) => Err(error),
170    }
171}
172
173#[cfg(test)]
174mod loader_classification_tests {
175    #[cfg(target_os = "linux")]
176    use super::library_load_error_detail;
177    use super::load_failure_is_candidate_absence;
178
179    #[cfg(target_os = "linux")]
180    #[test]
181    fn libloading_source_preserves_the_missing_bare_soname() {
182        const SONAME: &str = "libgamfit_cuda_driver_absence_probe.so.2411";
183        // SAFETY: this deliberately attempts to open a unique nonexistent
184        // soname and retains no symbols or library handle.
185        let error = match unsafe { libloading::Library::new(SONAME) } {
186            Ok(_) => panic!("the CUDA absence-probe soname unexpectedly exists"),
187            Err(error) => error,
188        };
189        let detail = library_load_error_detail(&error);
190        assert!(
191            load_failure_is_candidate_absence(SONAME, false, &detail),
192            "missing-soname detail was not classified as absence: {detail}"
193        );
194    }
195
196    #[test]
197    fn missing_bare_soname_is_absence() {
198        assert!(load_failure_is_candidate_absence(
199            "libcuda.so.1",
200            false,
201            "libcuda.so.1: cannot open shared object file: No such file or directory",
202        ));
203    }
204
205    #[test]
206    fn missing_transitive_dependency_is_a_load_fault() {
207        assert!(!load_failure_is_candidate_absence(
208            "libcuda.so.1",
209            false,
210            "libnvidia-fatbinaryloader.so.555: cannot open shared object file: No such file or directory",
211        ));
212    }
213
214    #[test]
215    fn present_but_invalid_absolute_candidate_is_a_load_fault() {
216        assert!(!load_failure_is_candidate_absence(
217            "/opt/cuda/libcuda.so.1",
218            true,
219            "/opt/cuda/libcuda.so.1: invalid ELF header",
220        ));
221    }
222
223    #[test]
224    fn absent_absolute_candidate_is_absence() {
225        assert!(load_failure_is_candidate_absence(
226            "/opt/cuda/libcuda.so.1",
227            false,
228            "/opt/cuda/libcuda.so.1: cannot open shared object file: No such file or directory",
229        ));
230    }
231}
232
233#[cfg(target_os = "linux")]
234fn preload_cuda_userspace_libraries() -> Result<(), String> {
235    static PRELOAD: OnceLock<Result<Vec<UnixLibrary>, String>> = OnceLock::new();
236    PRELOAD
237        .get_or_init(|| {
238            let paths = cuda_userspace_preload_paths()?;
239            if paths.is_empty() {
240                return Ok(Vec::new());
241            }
242            let mut loaded = Vec::new();
243            for path in paths {
244                // SAFETY: these candidates are CUDA userspace libraries found
245                // in canonical toolkit directories or pip's nvidia-*-cu12
246                // wheel layout. RTLD_GLOBAL is required so transitive deps
247                // such as libcusolver -> libnvJitLink resolve without an
248                // LD_LIBRARY_PATH mutation.
249                match unsafe { UnixLibrary::open(Some(&path), RTLD_NOW | RTLD_GLOBAL) } {
250                    Ok(library) => loaded.push(library),
251                    Err(err) => {
252                        return Err(format!(
253                            "could not preload CUDA userspace library {}: {err}",
254                            path.display()
255                        ));
256                    }
257                }
258            }
259            Ok(loaded)
260        })
261        .as_ref()
262        .map_err(Clone::clone)?;
263    // The loaded handles stay resident in `PRELOAD` for the process lifetime,
264    // which is the whole point of the preload; callers need only the verdict.
265    Ok(())
266}
267
268/// Require that the platform loader can open the named CUDA compute library
269/// (`cublas`, `cusolver`, `cusparse`) from the one selected userspace stack.
270///
271/// cudarc 0.19 attempts to lazy-load these via its own generated
272/// `panic_no_lib_found` helpers the first time `CudaBlas::new` /
273/// `DnHandle::new` / cuSPARSE handle creation is invoked. On a host that
274/// has only the CUDA *driver* (e.g. large-scale workbench images expose
275/// `libcuda.so.1` but no cuBLAS at all), those calls panic out of the
276/// PyO3 FFI boundary instead of returning a typed error.
277///
278/// `GpuRuntime::probe()` calls this for every compute library it depends on;
279/// failure retains the exact stack-selection or loader error in the typed GPU
280/// refusal and keeps cudarc's panic completely off the call path.
281pub fn require_cuda_compute_library(stem: &str) -> Result<(), String> {
282    // Cache the probe per stem and KEEP the loaded handle alive for the process
283    // lifetime. Dropping the `Library` here dlclose's it; that dlopen+dlclose
284    // cycle tears down the compute library's global init state, after which
285    // cudarc's own cublasCreate / cusolverDnCreate fail
286    // CUBLAS/CUSOLVER_STATUS_NOT_INITIALIZED on the next handle creation (the GPU
287    // then silently declines and falls back to CPU). Holding the handle keeps the
288    // library mapped and initialized so cudarc reuses it intact.
289    static PROBED: OnceLock<
290        std::sync::Mutex<std::collections::HashMap<String, Result<(), String>>>,
291    > = OnceLock::new();
292    static KEEP_ALIVE: OnceLock<std::sync::Mutex<Vec<Library>>> = OnceLock::new();
293    let probed = PROBED.get_or_init(|| std::sync::Mutex::new(std::collections::HashMap::new()));
294    if let Ok(cache) = probed.lock() {
295        if let Some(outcome) = cache.get(stem) {
296            return outcome.clone();
297        }
298    }
299    #[cfg(target_os = "linux")]
300    preload_cuda_userspace_libraries()?;
301    let outcome = match load_library_names(&cuda_compute_library_candidate_names(stem)) {
302        Ok(library) => {
303            if let Ok(mut keep) = KEEP_ALIVE
304                .get_or_init(|| std::sync::Mutex::new(Vec::new()))
305                .lock()
306            {
307                keep.push(library);
308            }
309            Ok(())
310        }
311        Err(error) => Err(error.to_string()),
312    };
313    if let Ok(mut cache) = probed.lock() {
314        cache.insert(stem.to_string(), outcome.clone());
315    }
316    outcome
317}
318
319#[cfg(target_os = "linux")]
320fn cuda_userspace_preload_paths() -> Result<Vec<PathBuf>, String> {
321    // A host package such as PyTorch may already own the process's CUDA
322    // userspace stack. Loading gam's system-first stack on top of that maps a
323    // second cudart/cuBLAS implementation and splits context/handle ownership.
324    // Continue the already-mapped stack instead: pip NVIDIA wheels are spread
325    // across component directories under one `nvidia/` root, while a system
326    // toolkit keeps this preload set in one directory.
327    let mapped = mapped_cuda_userspace_libraries()?;
328    if !mapped.is_empty() {
329        return complete_mapped_cuda_stack(&mapped);
330    }
331
332    let system_dirs = cuda_system_library_dirs();
333    for dir in &system_dirs {
334        if let Some(stack) = complete_system_cuda_stack(dir) {
335            return Ok(dedup_paths(stack));
336        }
337        if let Some(stack) = system_cuda_stack_with_packaged_nvjitlink(dir) {
338            return Ok(dedup_paths(stack));
339        }
340    }
341    for root in nvidia_package_roots() {
342        if let Some(stack) = complete_nvidia_cuda_stack(&root) {
343            return Ok(dedup_paths(stack));
344        }
345    }
346    Ok(Vec::new())
347}
348
349/// The CUDA component a userspace library path belongs to, taken from its
350/// SONAME stem (`libcublas.so.12` -> `cublas`, `libnvJitLink.so.12` ->
351/// `nvJitLink`). Used to reason about which mapped libraries are the same
352/// component sourced from different roots.
353#[cfg(target_os = "linux")]
354fn cuda_library_component(path: &Path) -> Option<String> {
355    let name = path.file_name()?.to_str()?;
356    let stem = name.strip_prefix("lib")?.split(".so").next()?;
357    if stem.is_empty() {
358        None
359    } else {
360        Some(stem.to_string())
361    }
362}
363
364/// The CUDA *compute* libraries. These are handle-based and share context /
365/// workspace state with whatever copy the process already initialised, so a
366/// second copy must never be mapped from a different root — that split is the
367/// double-free / `NOT_INITIALIZED` hazard the mapped-stack continuation exists
368/// to avoid. `cudart` / `nvJitLink` are runtime/driver-adjacent and tolerate a
369/// redundant duplicate (dlopen-by-SONAME binds one deterministically).
370#[cfg(target_os = "linux")]
371fn is_cuda_compute_component(component: &str) -> bool {
372    matches!(component, "cublas" | "cublasLt" | "cusolver" | "cusparse")
373}
374
375#[cfg(target_os = "linux")]
376fn complete_mapped_cuda_stack(mapped: &[PathBuf]) -> Result<Vec<PathBuf>, String> {
377    let canonical = |p: &Path| {
378        p.canonicalize().unwrap_or_else(|error| {
379            log::debug!(
380                "cuda stack: cannot canonicalize {}: {error}; \
381                 comparing the path as given",
382                p.display()
383            );
384            p.to_path_buf()
385        })
386    };
387
388    let mut candidates = Vec::new();
389    for path in mapped {
390        if let Some(root) = nvidia_package_root_for_library(path)
391            && let Some(stack) = complete_nvidia_cuda_stack(&root)
392        {
393            candidates.push(stack);
394        }
395        if let Some(parent) = path.parent()
396            && let Some(stack) = complete_system_cuda_stack_path(parent)
397        {
398            candidates.push(stack);
399        }
400    }
401
402    // Canonical path + component for every mapped library, computed once.
403    let mapped_meta: Vec<(PathBuf, Option<String>)> = mapped
404        .iter()
405        .map(|m| (canonical(m), cuda_library_component(m)))
406        .collect();
407
408    // Fast path: a single complete stack that contains EVERY mapped library.
409    // This is the unchanged behaviour for a process whose CUDA userspace all
410    // comes from one root (a pure system toolkit or a pure pip-wheel stack).
411    for stack in &candidates {
412        let stack = dedup_paths(stack.clone());
413        let stack_canon: Vec<PathBuf> = stack.iter().map(|c| canonical(c)).collect();
414        if mapped_meta
415            .iter()
416            .all(|(m, _)| stack_canon.iter().any(|c| c == m))
417        {
418            return Ok(stack);
419        }
420    }
421
422    // Split-stack path: a process can legitimately map a system `libcudart`
423    // (pulled onto the default loader path by ldconfig) alongside a host
424    // framework's pip-wheel stack (e.g. PyTorch's `nvidia-*-cu12` wheels), so
425    // NO single complete stack contains every mapped path. Continue a stack
426    // only when the process is genuinely using ALL of it, and only tolerate a
427    // benign duplicate — never a split of the handle-based compute libraries:
428    //
429    //   (a) every library in the candidate stack is already mapped — we
430    //       CONTINUE a stack the process runs on, we never introduce a library
431    //       from a partially-present root; and
432    //   (b) every mapped library is either part of that stack or a redundant
433    //       duplicate of a NON-compute component (`cudart` / `nvJitLink`) the
434    //       stack already provides. A mapped compute library (`cuBLAS` /
435    //       `cuSOLVER` / `cuSPARSE`) from another root is a genuine split and
436    //       disqualifies the candidate — mapping a second copy is the
437    //       double-free / `NOT_INITIALIZED` hazard this continuation avoids.
438    //
439    // Libraries mixed across two partially-mapped roots satisfy neither, so the
440    // conservative refusal below still fires for them.
441    for stack in candidates {
442        let stack = dedup_paths(stack);
443        let stack_canon: Vec<PathBuf> = stack.iter().map(|c| canonical(c)).collect();
444        let fully_mapped = stack_canon
445            .iter()
446            .all(|s| mapped_meta.iter().any(|(m, _)| m == s));
447        if !fully_mapped {
448            continue;
449        }
450        let consistent = mapped_meta.iter().all(|(m, component)| {
451            if stack_canon.iter().any(|s| s == m) {
452                return true;
453            }
454            match component {
455                Some(component) if !is_cuda_compute_component(component) => stack
456                    .iter()
457                    .any(|s| cuda_library_component(s).as_deref() == Some(component)),
458                _ => false,
459            }
460        });
461        if consistent {
462            return Ok(stack);
463        }
464    }
465
466    Err(format!(
467        "CUDA userspace is already mapped from no single complete stack: {}",
468        mapped
469            .iter()
470            .map(|path| path.display().to_string())
471            .collect::<Vec<_>>()
472            .join(", ")
473    ))
474}
475
476#[cfg(target_os = "linux")]
477fn mapped_cuda_userspace_libraries() -> Result<Vec<PathBuf>, String> {
478    let maps = std::fs::read_to_string("/proc/self/maps")
479        .map_err(|error| format!("cannot inspect mapped CUDA userspace libraries: {error}"))?;
480    let mut mapped = Vec::new();
481    for line in maps.lines() {
482        let Some(raw_path) = line.split_whitespace().last() else {
483            continue;
484        };
485        if !raw_path.starts_with('/') {
486            continue;
487        }
488        let path = PathBuf::from(raw_path);
489        let Some(name) = path.file_name().and_then(|name| name.to_str()) else {
490            continue;
491        };
492        if [
493            "libcudart.so",
494            "libnvJitLink.so",
495            "libcublasLt.so",
496            "libcublas.so",
497            "libcusparse.so",
498            "libcusolver.so",
499        ]
500        .iter()
501        .any(|prefix| name.starts_with(prefix))
502        {
503            mapped.push(path);
504        }
505    }
506    Ok(dedup_paths(mapped))
507}
508
509#[cfg(target_os = "linux")]
510fn nvidia_package_root_for_library(path: &Path) -> Option<PathBuf> {
511    path.ancestors()
512        .find(|ancestor| ancestor.file_name().and_then(|name| name.to_str()) == Some("nvidia"))
513        .map(Path::to_path_buf)
514}
515
516fn cuda_compute_library_candidate_names(stem: &str) -> Vec<String> {
517    let base = format!("lib{stem}");
518    let mut out: Vec<String> = Vec::new();
519    // Bare soname forms — exercised by the platform loader against
520    // LD_LIBRARY_PATH and the default search dirs.
521    out.push(format!("{base}.so"));
522    out.push(format!("{base}.so.1"));
523    // Major-version walk mirroring cudarc's own candidate list so the
524    // preflight agrees with whatever cudarc would have tried next.
525    for major in (9..=13).rev() {
526        out.push(format!("{base}.so.{major}"));
527    }
528    #[cfg(target_os = "linux")]
529    {
530        for dir in cuda_system_library_dirs() {
531            out.push(format!("{dir}/{base}.so"));
532            for major in (9..=13).rev() {
533                out.push(format!("{dir}/{base}.so.{major}"));
534            }
535            append_versioned_linux_so_candidates(&mut out, Path::new(dir), &base);
536        }
537        for root in nvidia_package_roots() {
538            let lib_dir = root.join(nvidia_component_for_stem(stem)).join("lib");
539            out.push(format!("{}/{}.so", lib_dir.display(), base));
540            for major in (9..=13).rev() {
541                out.push(format!("{}/{}.so.{major}", lib_dir.display(), base));
542            }
543            append_versioned_linux_so_candidates(&mut out, &lib_dir, &base);
544        }
545    }
546    out
547}
548
549#[cfg(target_os = "linux")]
550fn cuda_system_library_dirs() -> Vec<&'static str> {
551    vec![
552        "/usr/local/cuda/lib64",
553        "/usr/local/cuda/lib",
554        "/usr/local/cuda/targets/x86_64-linux/lib",
555        "/usr/lib/x86_64-linux-gnu",
556        "/usr/lib64",
557        "/usr/lib/wsl/lib",
558        "/opt/cuda/lib64",
559    ]
560}
561
562#[cfg(target_os = "linux")]
563fn complete_system_cuda_stack(dir: &str) -> Option<Vec<PathBuf>> {
564    complete_system_cuda_stack_path(Path::new(dir))
565}
566
567#[cfg(target_os = "linux")]
568fn complete_system_cuda_stack_path(dir: &Path) -> Option<Vec<PathBuf>> {
569    let stack = vec![
570        first_existing(dir, &["libcudart.so.13", "libcudart.so.12", "libcudart.so"])?,
571        first_existing(
572            dir,
573            &[
574                "libnvJitLink.so.13",
575                "libnvJitLink.so.12",
576                "libnvJitLink.so",
577            ],
578        )?,
579        first_existing(
580            dir,
581            &["libcublasLt.so.13", "libcublasLt.so.12", "libcublasLt.so"],
582        )?,
583        first_existing(dir, &["libcublas.so.13", "libcublas.so.12", "libcublas.so"])?,
584        first_existing(
585            dir,
586            &["libcusparse.so.13", "libcusparse.so.12", "libcusparse.so"],
587        )?,
588        first_existing(
589            dir,
590            &[
591                "libcusolver.so.13",
592                "libcusolver.so.12",
593                "libcusolver.so.11",
594                "libcusolver.so",
595            ],
596        )?,
597    ];
598    Some(stack)
599}
600
601#[cfg(target_os = "linux")]
602fn system_cuda_stack_with_packaged_nvjitlink(dir: &str) -> Option<Vec<PathBuf>> {
603    let dir = Path::new(dir);
604    let nvjitlink = packaged_nvjitlink_library()?;
605    let stack = vec![
606        first_existing(dir, &["libcudart.so.13", "libcudart.so.12", "libcudart.so"])?,
607        nvjitlink,
608        first_existing(
609            dir,
610            &["libcublasLt.so.13", "libcublasLt.so.12", "libcublasLt.so"],
611        )?,
612        first_existing(dir, &["libcublas.so.13", "libcublas.so.12", "libcublas.so"])?,
613        first_existing(
614            dir,
615            &["libcusparse.so.13", "libcusparse.so.12", "libcusparse.so"],
616        )?,
617        first_existing(
618            dir,
619            &[
620                "libcusolver.so.13",
621                "libcusolver.so.12",
622                "libcusolver.so.11",
623                "libcusolver.so",
624            ],
625        )?,
626    ];
627    Some(stack)
628}
629
630#[cfg(target_os = "linux")]
631fn complete_nvidia_cuda_stack(root: &Path) -> Option<Vec<PathBuf>> {
632    let stack = vec![
633        first_existing(
634            &root.join("cuda_runtime").join("lib"),
635            &["libcudart.so.13", "libcudart.so.12", "libcudart.so"],
636        )?,
637        first_existing(
638            &root.join("nvjitlink").join("lib"),
639            &[
640                "libnvJitLink.so.13",
641                "libnvJitLink.so.12",
642                "libnvJitLink.so",
643            ],
644        )?,
645        first_existing(
646            &root.join("cublas").join("lib"),
647            &["libcublasLt.so.13", "libcublasLt.so.12", "libcublasLt.so"],
648        )?,
649        first_existing(
650            &root.join("cublas").join("lib"),
651            &["libcublas.so.13", "libcublas.so.12", "libcublas.so"],
652        )?,
653        first_existing(
654            &root.join("cusparse").join("lib"),
655            &["libcusparse.so.13", "libcusparse.so.12", "libcusparse.so"],
656        )?,
657        first_existing(
658            &root.join("cusolver").join("lib"),
659            &[
660                "libcusolver.so.13",
661                "libcusolver.so.12",
662                "libcusolver.so.11",
663                "libcusolver.so",
664            ],
665        )?,
666    ];
667    Some(stack)
668}
669
670#[cfg(target_os = "linux")]
671fn packaged_nvjitlink_library() -> Option<PathBuf> {
672    for root in nvidia_package_roots() {
673        let lib_dir = root.join("nvjitlink").join("lib");
674        if let Some(path) = first_existing(
675            &lib_dir,
676            &[
677                "libnvJitLink.so.13",
678                "libnvJitLink.so.12",
679                "libnvJitLink.so",
680            ],
681        ) {
682            return Some(path);
683        }
684    }
685    None
686}
687
688#[cfg(target_os = "linux")]
689fn nvidia_component_for_stem(stem: &str) -> String {
690    match stem {
691        "cublas" => "cublas".to_string(),
692        "cusolver" => "cusolver".to_string(),
693        "cusparse" => "cusparse".to_string(),
694        "nvJitLink" | "nvjitlink" => "nvjitlink".to_string(),
695        "cudart" | "cuda_runtime" => "cuda_runtime".to_string(),
696        _ => stem.to_string(),
697    }
698}
699
700#[cfg(target_os = "linux")]
701fn nvidia_package_roots() -> Vec<PathBuf> {
702    let mut roots = Vec::new();
703    if let Some(home) = current_user_home_dir() {
704        collect_python_nvidia_roots(home.join(".local/lib"), &mut roots);
705    }
706    collect_python_nvidia_roots(Path::new("/usr/local/lib").to_path_buf(), &mut roots);
707    collect_python_nvidia_roots(Path::new("/usr/lib").to_path_buf(), &mut roots);
708    dedup_paths(roots)
709}
710
711#[cfg(target_os = "linux")]
712fn current_user_home_dir() -> Option<PathBuf> {
713    let status = std::fs::read_to_string("/proc/self/status").ok()?;
714    let uid = status
715        .lines()
716        .find_map(|line| line.strip_prefix("Uid:"))?
717        .split_whitespace()
718        .next()?;
719    let passwd = std::fs::read_to_string("/etc/passwd").ok()?;
720    for line in passwd.lines() {
721        let mut fields = line.split(':');
722        fields.next()?;
723        fields.next()?;
724        if fields.next()? != uid {
725            continue;
726        }
727        fields.next()?;
728        fields.next()?;
729        return Some(PathBuf::from(fields.next()?));
730    }
731    None
732}
733
734#[cfg(target_os = "linux")]
735fn collect_python_nvidia_roots(base: PathBuf, out: &mut Vec<PathBuf>) {
736    let Ok(entries) = std::fs::read_dir(base) else {
737        return;
738    };
739    for entry in entries.flatten() {
740        let path = entry.path();
741        let Some(name) = path.file_name().and_then(|name| name.to_str()) else {
742            continue;
743        };
744        if !name.starts_with("python") {
745            continue;
746        }
747        for site_dir in ["site-packages", "dist-packages"] {
748            let root = path.join(site_dir).join("nvidia");
749            if root.exists() {
750                out.push(root);
751            }
752        }
753    }
754}
755
756#[cfg(target_os = "linux")]
757fn first_existing(dir: &Path, names: &[&str]) -> Option<PathBuf> {
758    for name in names {
759        let path = dir.join(name);
760        if path.exists() {
761            return Some(path);
762        }
763    }
764    None
765}
766
767#[cfg(target_os = "linux")]
768fn dedup_paths(paths: Vec<PathBuf>) -> Vec<PathBuf> {
769    let mut out = Vec::new();
770    for path in paths {
771        let canonical = path.canonicalize().unwrap_or(path);
772        if !out.iter().any(|existing| existing == &canonical) {
773            out.push(canonical);
774        }
775    }
776    out
777}
778
779#[cfg(target_os = "linux")]
780fn append_versioned_linux_so_candidates(out: &mut Vec<String>, dir: &Path, base: &str) {
781    let Ok(entries) = std::fs::read_dir(dir) else {
782        return;
783    };
784    let prefix = format!("{base}.so.");
785    let mut versioned = Vec::new();
786    for entry in entries.flatten() {
787        let path = entry.path();
788        let Some(name) = path.file_name().and_then(|name| name.to_str()) else {
789            continue;
790        };
791        if name.starts_with(&prefix) {
792            versioned.push(path);
793        }
794    }
795    versioned.sort();
796    for path in versioned {
797        let candidate = path.to_string_lossy().into_owned();
798        if !out.iter().any(|existing| existing == &candidate) {
799            out.push(candidate);
800        }
801    }
802}
803
804fn cuda_library_candidate_names() -> Vec<String> {
805    let mut out: Vec<String> = cuda_library_candidates()
806        .iter()
807        .map(|candidate| (*candidate).to_string())
808        .collect();
809    if cfg!(target_os = "linux") {
810        for dir in [
811            "/usr/local/nvidia/lib64",
812            "/usr/local/nvidia/lib",
813            "/usr/local/cuda/compat",
814            "/usr/lib/x86_64-linux-gnu",
815            "/usr/lib64",
816            "/usr/lib/wsl/lib",
817        ] {
818            append_versioned_linux_libcuda_candidates(&mut out, Path::new(dir));
819        }
820    }
821    out
822}
823
824fn append_versioned_linux_libcuda_candidates(out: &mut Vec<String>, dir: &Path) {
825    let Ok(entries) = std::fs::read_dir(dir) else {
826        return;
827    };
828    let mut versioned = Vec::new();
829    for entry in entries.flatten() {
830        let path = entry.path();
831        let Some(name) = path.file_name().and_then(|name| name.to_str()) else {
832            continue;
833        };
834        if name.starts_with("libcuda.so.") && name != "libcuda.so.1" {
835            versioned.push(path);
836        }
837    }
838    versioned.sort();
839    for path in versioned {
840        let candidate = path.to_string_lossy().into_owned();
841        if !out.iter().any(|existing| existing == &candidate) {
842            out.push(candidate);
843        }
844    }
845}
846
847pub fn cuda_library_candidates() -> &'static [&'static str] {
848    if cfg!(target_os = "windows") {
849        &["nvcuda.dll"]
850    } else if cfg!(target_os = "macos") {
851        &["/usr/local/cuda/lib/libcuda.dylib", "libcuda.dylib"]
852    } else {
853        &[
854            "/usr/local/nvidia/lib64/libcuda.so.1",
855            "/usr/local/nvidia/lib64/libcuda.so",
856            "/usr/local/nvidia/lib/libcuda.so.1",
857            "/usr/local/nvidia/lib/libcuda.so",
858            "/usr/local/cuda/compat/libcuda.so.1",
859            "/usr/local/cuda/compat/libcuda.so",
860            "/usr/lib/x86_64-linux-gnu/libcuda.so.1",
861            "/usr/lib/x86_64-linux-gnu/libcuda.so",
862            "/usr/lib64/libcuda.so.1",
863            "/usr/lib64/libcuda.so",
864            "/usr/lib/wsl/lib/libcuda.so.1",
865            "/usr/lib/wsl/lib/libcuda.so",
866            "libcuda.so.1",
867            "libcuda.so",
868        ]
869    }
870}
871
872#[inline]
873pub fn to_i32(value: usize) -> Option<i32> {
874    i32::try_from(value).ok()
875}
876
877/// Repack a 2D `ndarray::ArrayBase` (row-major) into the column-major
878/// layout expected by every cuBLAS / cuSOLVER entry point.
879///
880/// Walks each column once via ndarray's iter (no per-element bounds checks)
881/// and extends into a pre-sized `Vec`. On large-scale inputs (n≈3×10⁵,
882/// p≈35) this replaces a per-element `a[[row, col]]` indexing loop that
883/// dominated the host side of every GPU dispatch.
884///
885/// Fast path: if the input is already F-order (column-major, contiguous in
886/// memory-order), borrow its raw buffer directly — no allocation, no copy.
887/// Standard row-major ndarrays still go through the permutation path.
888pub fn to_col_major<'a, S: Data<Elem = f64>>(a: &'a ArrayBase<S, Ix2>) -> Cow<'a, [f64]> {
889    let (rows, cols) = a.dim();
890    let strides = a.strides();
891    // F-order contiguous: column stride == 1, row stride == rows.
892    // `as_slice_memory_order` confirms the buffer is contiguous in memory.
893    if rows > 0
894        && cols > 0
895        && strides[0] == 1
896        && strides[1] == rows as isize
897        && let Some(slice) = a.as_slice_memory_order()
898    {
899        return Cow::Borrowed(slice);
900    }
901    let mut out: Vec<f64> = Vec::with_capacity(rows.saturating_mul(cols));
902    for col in 0..cols {
903        out.extend(a.column(col).iter().copied());
904    }
905    Cow::Owned(out)
906}
907
908/// Borrow (or pack) a 2D array's buffer in ROW-major (C) order.
909///
910/// The col-major dual of [`to_col_major`]: when the input is already
911/// C-contiguous its raw buffer IS the row-major flat layout, so this borrows
912/// it with no allocation or copy. Non-contiguous / F-order inputs are packed
913/// row by row.
914///
915/// This is the host-transpose-free upload path. A row-major `(r × c)` buffer,
916/// reinterpreted as a column-major buffer, is exactly the transpose `(c × r)`
917/// of the logical matrix — which is what the swapped-operand cuBLAS GEMM
918/// (`Cᵀ = Bᵀ·Aᵀ`) consumes, letting both the design upload and the result
919/// download skip the O(r·c) scalar permutation that dominated tall-skinny
920/// GEMMs on the host.
921pub fn to_row_major<'a, S: Data<Elem = f64>>(a: &'a ArrayBase<S, Ix2>) -> Cow<'a, [f64]> {
922    let (rows, cols) = a.dim();
923    let strides = a.strides();
924    // C-order contiguous: row stride == cols, column stride == 1.
925    if rows > 0
926        && cols > 0
927        && strides[1] == 1
928        && strides[0] == cols as isize
929        && let Some(slice) = a.as_slice_memory_order()
930    {
931        return Cow::Borrowed(slice);
932    }
933    let mut out: Vec<f64> = Vec::with_capacity(rows.saturating_mul(cols));
934    for row in 0..rows {
935        out.extend(a.row(row).iter().copied());
936    }
937    Cow::Owned(out)
938}
939
940/// Wrap a row-major flat buffer of shape `(rows, cols)` as an `Array2<f64>`
941/// without permutation. The buffer is consumed (no copy when its length
942/// matches). Returns `None` on a length mismatch.
943pub fn array_from_row_major(values: Vec<f64>, rows: usize, cols: usize) -> Option<Array2<f64>> {
944    if values.len() != rows.checked_mul(cols)? {
945        return None;
946    }
947    Array2::from_shape_vec((rows, cols), values).ok()
948}
949
950/// Convert a column-major flat buffer back into row-major `Array2<f64>`.
951pub fn from_col_major_inplace(values: &[f64], out: &mut Array2<f64>) -> Option<()> {
952    let (rows, cols) = out.dim();
953    if values.len() != rows.checked_mul(cols)? {
954        return None;
955    }
956    for col in 0..cols {
957        let src = ndarray::ArrayView1::from(&values[col * rows..(col + 1) * rows]);
958        out.column_mut(col).assign(&src);
959    }
960    Some(())
961}
962
963pub fn from_col_major(values: &[f64], rows: usize, cols: usize) -> Option<Array2<f64>> {
964    let mut out = Array2::<f64>::zeros((rows, cols));
965    from_col_major_inplace(values, &mut out)?;
966    Some(out)
967}
968
969#[cfg(test)]
970mod tests {
971    use super::*;
972    use ndarray::array;
973
974    #[cfg(target_os = "linux")]
975    fn fake_nvidia_stack(root: &Path) -> Vec<PathBuf> {
976        let libraries = [
977            ("cuda_runtime", "libcudart.so.12"),
978            ("nvjitlink", "libnvJitLink.so.12"),
979            ("cublas", "libcublasLt.so.12"),
980            ("cublas", "libcublas.so.12"),
981            ("cusparse", "libcusparse.so.12"),
982            ("cusolver", "libcusolver.so.11"),
983        ];
984        libraries
985            .into_iter()
986            .map(|(component, name)| {
987                let path = root.join(component).join("lib").join(name);
988                std::fs::create_dir_all(path.parent().expect("library parent"))
989                    .expect("create fake CUDA component directory");
990                std::fs::write(&path, []).expect("create fake CUDA library");
991                path
992            })
993            .collect()
994    }
995
996    #[test]
997    fn to_i32_fits_small_value() {
998        assert_eq!(to_i32(0), Some(0));
999        assert_eq!(to_i32(42), Some(42));
1000        assert_eq!(to_i32(i32::MAX as usize), Some(i32::MAX));
1001    }
1002
1003    #[test]
1004    fn to_i32_overflows_returns_none() {
1005        assert_eq!(to_i32(i32::MAX as usize + 1), None);
1006    }
1007
1008    #[test]
1009    fn to_col_major_2x3_row_major() {
1010        // Row-major [[1,2,3],[4,5,6]] → col-major [1,4,2,5,3,6]
1011        let a = array![[1.0_f64, 2.0, 3.0], [4.0, 5.0, 6.0]];
1012        let col = to_col_major(&a);
1013        assert_eq!(&*col, &[1.0, 4.0, 2.0, 5.0, 3.0, 6.0]);
1014    }
1015
1016    #[test]
1017    fn to_col_major_identity_roundtrip() {
1018        let a = array![[1.0_f64, 0.0], [0.0, 1.0]];
1019        let col = to_col_major(&a);
1020        assert_eq!(&*col, &[1.0, 0.0, 0.0, 1.0]);
1021    }
1022
1023    #[test]
1024    fn from_col_major_2x3_roundtrip() {
1025        let original = array![[1.0_f64, 2.0, 3.0], [4.0, 5.0, 6.0]];
1026        let col = to_col_major(&original);
1027        let recovered = from_col_major(&col, 2, 3).expect("should succeed");
1028        assert_eq!(recovered, original);
1029    }
1030
1031    #[test]
1032    fn from_col_major_wrong_length_returns_none() {
1033        // 2x3 = 6 elements, but only 5 provided
1034        assert!(from_col_major(&[1.0, 2.0, 3.0, 4.0, 5.0], 2, 3).is_none());
1035    }
1036
1037    #[test]
1038    fn from_col_major_inplace_mismatched_buffer_returns_none() {
1039        let mut out = Array2::<f64>::zeros((3, 3));
1040        let short = vec![1.0_f64; 8]; // 9 expected, 8 given
1041        assert!(from_col_major_inplace(&short, &mut out).is_none());
1042    }
1043
1044    #[test]
1045    fn from_col_major_single_element() {
1046        let result = from_col_major(&[7.0], 1, 1).expect("should succeed");
1047        assert_eq!(result[[0, 0]], 7.0);
1048    }
1049
1050    #[cfg(target_os = "linux")]
1051    #[test]
1052    fn mapped_pytorch_stack_is_continued_as_one_complete_stack() {
1053        let temp = tempfile::tempdir().expect("temporary CUDA tree");
1054        let root = temp.path().join("site-packages").join("nvidia");
1055        let stack = fake_nvidia_stack(&root);
1056        let mapped = vec![stack[0].clone(), stack[3].clone()];
1057
1058        let selected = complete_mapped_cuda_stack(&mapped).expect("one mapped NVIDIA root");
1059
1060        assert_eq!(selected.len(), stack.len());
1061        assert!(mapped.iter().all(|path| {
1062            let canonical = path.canonicalize().expect("canonical fake library");
1063            selected.contains(&canonical)
1064        }));
1065    }
1066
1067    #[cfg(target_os = "linux")]
1068    #[test]
1069    fn mapped_mixed_cuda_stacks_are_refused() {
1070        let temp = tempfile::tempdir().expect("temporary CUDA tree");
1071        let first = fake_nvidia_stack(&temp.path().join("first").join("nvidia"));
1072        let second = fake_nvidia_stack(&temp.path().join("second").join("nvidia"));
1073        let mapped = vec![first[0].clone(), second[3].clone()];
1074
1075        let error = complete_mapped_cuda_stack(&mapped)
1076            .expect_err("libraries from two mapped roots must not be mixed");
1077
1078        assert!(error.contains("no single complete stack"));
1079    }
1080
1081    #[cfg(target_os = "linux")]
1082    #[test]
1083    fn mapped_pytorch_stack_with_redundant_system_cudart_is_continued() {
1084        // A process can map a system `libcudart` (pulled onto the default
1085        // loader path by ldconfig) alongside PyTorch's complete pip-wheel
1086        // stack. The whole pip stack is mapped; the extra system cudart is a
1087        // redundant duplicate of a non-compute component, so the pip stack is
1088        // continued rather than the GPU being refused (gam issue #2259).
1089        let temp = tempfile::tempdir().expect("temporary CUDA tree");
1090        let pip = fake_nvidia_stack(&temp.path().join("site-packages").join("nvidia"));
1091
1092        // A stray system cudart from an unrelated toolkit directory (no
1093        // complete stack of its own next to it).
1094        let sys_dir = temp.path().join("usr").join("local").join("cuda").join("lib");
1095        std::fs::create_dir_all(&sys_dir).expect("system lib dir");
1096        let sys_cudart = sys_dir.join("libcudart.so.12");
1097        std::fs::write(&sys_cudart, []).expect("system cudart");
1098
1099        // Everything from the pip stack is mapped, plus the redundant cudart.
1100        let mut mapped = pip.clone();
1101        mapped.push(sys_cudart);
1102
1103        let selected =
1104            complete_mapped_cuda_stack(&mapped).expect("continue the fully-mapped pip stack");
1105
1106        assert_eq!(selected.len(), pip.len());
1107        assert!(pip.iter().all(|path| {
1108            let canonical = path.canonicalize().expect("canonical fake library");
1109            selected.contains(&canonical)
1110        }));
1111    }
1112}