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