Skip to main content

tract_cuda/
utils.rs

1use std::ffi::c_int;
2use std::sync::OnceLock;
3
4use anyhow::Context;
5use libloading::{Library, Symbol};
6
7use tract_core::internal::*;
8use tract_core::tract_linalg::block_quant::*;
9use tract_gpu::tensor::DeviceTensor;
10
11use crate::Q40_ROW_PADDING;
12use crate::ops::GgmlQuantQ81Fact;
13
14static CULIBS_MISSING: OnceLock<Option<&'static str>> = OnceLock::new();
15static DEPENDENCIES_OK: OnceLock<()> = OnceLock::new();
16
17/// CUDA Driver API version this build of tract-cuda is bound against. Used both
18/// for the runtime driver-version check (in `ensure_cuda_driver_compatible`)
19/// and for the per-version cubin cache path. Derived at compile time from the
20/// active `cuda-XXXXX` feature; cudarc binds to that same enum/struct layout,
21/// so the two must agree. Adding a new minor here means adding the matching
22/// cargo feature too.
23#[cfg(feature = "cuda-12000")]
24pub const REQUIRED_CUDA_API: i32 = 12000;
25#[cfg(feature = "cuda-12010")]
26pub const REQUIRED_CUDA_API: i32 = 12010;
27#[cfg(feature = "cuda-12020")]
28pub const REQUIRED_CUDA_API: i32 = 12020;
29#[cfg(feature = "cuda-12030")]
30pub const REQUIRED_CUDA_API: i32 = 12030;
31#[cfg(feature = "cuda-12040")]
32pub const REQUIRED_CUDA_API: i32 = 12040;
33#[cfg(feature = "cuda-12050")]
34pub const REQUIRED_CUDA_API: i32 = 12050;
35#[cfg(feature = "cuda-12060")]
36pub const REQUIRED_CUDA_API: i32 = 12060;
37#[cfg(feature = "cuda-12080")]
38pub const REQUIRED_CUDA_API: i32 = 12080;
39#[cfg(feature = "cuda-12090")]
40pub const REQUIRED_CUDA_API: i32 = 12090;
41#[cfg(feature = "cuda-13000")]
42pub const REQUIRED_CUDA_API: i32 = 13000;
43#[cfg(feature = "cuda-13010")]
44pub const REQUIRED_CUDA_API: i32 = 13010;
45#[cfg(feature = "cuda-13020")]
46pub const REQUIRED_CUDA_API: i32 = 13020;
47
48// Exactly one cuda-XXXXX feature must be enabled. cudarc itself panics at its
49// build script if zero are enabled, but it does not catch the case of two
50// being enabled simultaneously — and a mismatch between REQUIRED_CUDA_API and
51// cudarc's actual binding would be a silent ABI hazard. Enumerate the
52// supported set explicitly so a duplicate triggers `REQUIRED_CUDA_API` re-def
53// at compile time.
54#[cfg(not(any(
55    feature = "cuda-12000",
56    feature = "cuda-12010",
57    feature = "cuda-12020",
58    feature = "cuda-12030",
59    feature = "cuda-12040",
60    feature = "cuda-12050",
61    feature = "cuda-12060",
62    feature = "cuda-12080",
63    feature = "cuda-12090",
64    feature = "cuda-13000",
65    feature = "cuda-13010",
66    feature = "cuda-13020",
67)))]
68compile_error!(
69    "Tract CUDA backend requires exactly one of the cuda-XXXXX features \
70     (cuda-12000..cuda-13020) to be enabled. Enable it in Cargo features.",
71);
72
73/// CUDA Driver API status code type (CUresult is an enum, but it's ABI-compatible with int).
74type CuResult = c_int;
75
76type CuInitFn = unsafe extern "C" fn(flags: u32) -> CuResult;
77type CuDriverGetVersionFn = unsafe extern "C" fn(version: *mut c_int) -> CuResult;
78
79#[cfg(target_os = "linux")]
80const CUDA_DRIVER_LIB_CANDIDATES: [&str; 2] = ["libcuda.so.1", "libcuda.so"];
81
82#[cfg(target_os = "windows")]
83const CUDA_DRIVER_LIB_CANDIDATES: [&str; 1] = ["nvcuda.dll"];
84
85#[cfg(not(any(target_os = "linux", target_os = "windows")))]
86const CUDA_DRIVER_LIB_CANDIDATES: [&str; 0] = [];
87
88fn format_cuda_version(v: i32) -> (i32, i32) {
89    (v / 1000, (v % 1000) / 10)
90}
91
92fn load_first_found_cuda_lib() -> TractResult<(&'static str, Library)> {
93    for name in CUDA_DRIVER_LIB_CANDIDATES {
94        match unsafe { Library::new(name) } {
95            Ok(lib) => {
96                log::debug!("tract-cuda: driver candidate {name:?} loaded");
97                return Ok((name, lib));
98            }
99            Err(e) => log::debug!("tract-cuda: driver candidate {name:?} not loadable: {e}"),
100        }
101    }
102
103    bail!(
104        "CUDA driver library not found. Tried: {:?}. \
105         Is an NVIDIA driver installed \
106         (and, in containers, did you run with GPU passthrough)?",
107        CUDA_DRIVER_LIB_CANDIDATES
108    )
109}
110
111/// Checks that the installed CUDA driver is present and new enough to satisfy REQUIRED_CUDA_API.
112///
113/// IMPORTANT: call this before touching `cudarc::driver::sys` or any cudarc context creation,
114/// otherwise cudarc may panic while eagerly binding symbols.
115fn ensure_cuda_driver_compatible() -> TractResult<()> {
116    // Load driver library without involving cudarc.
117    let (lib_name, lib) = load_first_found_cuda_lib()?;
118    let (req_major, req_minor) = format_cuda_version(REQUIRED_CUDA_API);
119
120    unsafe {
121        // Resolve symbols we need. If these are missing, the driver install is broken or not NVIDIA.
122        let cu_init: Symbol<CuInitFn> = lib.get(b"cuInit\0").map_err(|e| {
123            format_err!(
124                "CUDA driver library loaded ({lib_name}), but symbol cuInit is missing. \
125                 This does not look like a functional NVIDIA driver installation. Details: {e}"
126            )
127        })?;
128
129        let cu_driver_get_version: Symbol<CuDriverGetVersionFn> =
130            lib.get(b"cuDriverGetVersion\0").map_err(|e| {
131                format_err!(
132                    "CUDA driver library loaded ({lib_name}), but symbol cuDriverGetVersion is missing. \
133                     Driver is too old or installation is corrupted. Details: {e}"
134                )
135            })?;
136
137        // Initialize the driver. This also surfaces "no device / permission / container" issues early.
138        let init_res = cu_init(0);
139        if init_res != 0 {
140            // Don't assume specific numeric codes here; just report the CUresult.
141            bail!(
142                "CUDA driver initialization failed (cuInit returned {}, via {lib_name}). \
143                 Possible causes: no CUDA-capable device exposed to this process, \
144                 missing /dev/nvidia* nodes (container), insufficient permissions, \
145                 or a broken driver install.",
146                init_res
147            );
148        }
149
150        // Query driver API version.
151        let mut version: c_int = 0;
152        let ver_res = cu_driver_get_version(&mut version as *mut _);
153        if ver_res != 0 {
154            bail!(
155                "cuDriverGetVersion failed (returned {}, via {lib_name}). \
156                 NVIDIA driver may be corrupted or not functioning properly.",
157                ver_res
158            );
159        }
160
161        let (found_major, found_minor) = format_cuda_version(version);
162
163        // Compare against required API based on feature gate.
164        if version < REQUIRED_CUDA_API {
165            bail!(
166                "CUDA driver too old.\n\
167                 Driver library: {lib_name}\n\
168                 Built with cudarc feature cuda-{} (requires driver API >= {}.{}).\n\
169                 Found driver API {}.{}.\n\
170                 Fix: upgrade the NVIDIA driver, or rebuild tract-cuda with a lower cuda-XXXXX gate.",
171                REQUIRED_CUDA_API,
172                req_major,
173                req_minor,
174                found_major,
175                found_minor
176            );
177        }
178
179        log::info!(
180            "tract-cuda: CUDA driver {lib_name} OK; API version {found_major}.{found_minor} (built for >= {req_major}.{req_minor})"
181        );
182    }
183
184    Ok(())
185}
186
187/// Probe each cudarc-backed sub-library we rely on and return the name of the first one
188/// that fails to load, or `None` if all are present. Cached across calls.
189fn first_missing_cudarc_culib() -> Option<&'static str> {
190    *CULIBS_MISSING.get_or_init(|| {
191        // Names match the short labels cudarc uses in its dynamic-loading candidates.
192        // SAFETY: cudarc's `is_culib_present` functions are documented dlopen probes with no
193        // side effects beyond the library handle cache.
194        let probe = |name: &'static str, ok: bool| -> Option<&'static str> {
195            if ok {
196                log::debug!("tract-cuda: cudarc probe for {name:?} succeeded");
197                None
198            } else {
199                log::debug!("tract-cuda: cudarc probe for {name:?} failed");
200                Some(name)
201            }
202        };
203        probe("cuda (driver)", unsafe { cudarc::driver::sys::is_culib_present() })
204            .or_else(|| {
205                probe("cudart (runtime)", unsafe { cudarc::runtime::sys::is_culib_present() })
206            })
207            .or_else(|| probe("nvrtc", unsafe { cudarc::nvrtc::sys::is_culib_present() }))
208            .or_else(|| probe("cublas", unsafe { cudarc::cublas::sys::is_culib_present() }))
209            .or_else(|| probe("cudnn", unsafe { cudarc::cudnn::sys::is_culib_present() }))
210    })
211}
212
213pub fn ensure_cuda_runtime_dependencies(context_msg: &'static str) -> TractResult<()> {
214    // Fast path: if a previous call already validated the full chain, skip the whole probe.
215    // CudaRuntime wires this into both `check()` and `prepare_with_options()`, so the second
216    // caller would otherwise re-dlopen libcuda and re-log the init narrative.
217    if DEPENDENCIES_OK.get().is_some() {
218        return Ok(());
219    }
220
221    ensure_cuda_driver_compatible()
222        .context("CUDA driver validation failed")
223        .context(context_msg)?;
224
225    if let Some(missing) = first_missing_cudarc_culib() {
226        bail!(
227            "{context_msg}: CUDA runtime sub-library {missing:?} could not be dlopen'd. \
228             cudarc searches standard library names derived from the build-time CUDA \
229             version; install the matching package or set LD_LIBRARY_PATH so the \
230             loader can find it. Enable RUST_LOG=tract_cuda=debug to see each probe."
231        );
232    }
233
234    log::info!(
235        "tract-cuda: all required cudarc sub-libraries present (driver, cudart, nvrtc, cublas, cudnn)"
236    );
237
238    let _ = DEPENDENCIES_OK.set(());
239    Ok(())
240}
241
242pub fn get_ggml_q81_fact(t: &DeviceTensor) -> Option<GgmlQuantQ81Fact> {
243    if let DeviceTensor::Owned(t) = t {
244        t.exotic_fact().and_then(|of| of.downcast_ref::<GgmlQuantQ81Fact>()).cloned()
245    } else if let DeviceTensor::ArenaView(t) = t {
246        t.exotic_fact().and_then(|of| of.downcast_ref::<GgmlQuantQ81Fact>()).cloned()
247    } else {
248        None
249    }
250}
251
252pub fn pad_q40(bqs: &BlockQuantStorage, m: usize, k: usize) -> TractResult<BlockQuantStorage> {
253    ensure!(k % 32 == 0);
254
255    let to_pad = k.next_multiple_of(Q40_ROW_PADDING) - k;
256    if to_pad == 0 {
257        return Ok(bqs.clone()); // No padding needed
258    }
259
260    let row_bytes = k * Q4_0.block_bytes() / Q4_0.block_len();
261
262    let pad_quant = Q4_0.quant_f32(&vec![0f32; to_pad])?;
263    let pad_bytes = pad_quant.len();
264
265    let mut new_data = Vec::with_capacity(m * (row_bytes + pad_bytes));
266    let old_bytes = bqs.value().as_bytes();
267
268    for row in 0..m {
269        let start = row * row_bytes;
270        new_data.extend_from_slice(&old_bytes[start..start + row_bytes]);
271        new_data.extend_from_slice(&pad_quant);
272    }
273
274    BlockQuantStorage::new(
275        tract_core::dyn_clone::clone_box(bqs.format()),
276        m,
277        k + to_pad,
278        Arc::new(Blob::from_bytes(&new_data)?),
279    )
280}