use crate::EngineError;
use libloading::{Library, Symbol};
use std::collections::HashMap;
use std::ffi::{c_char, c_int, c_void, CStr};
#[cfg(unix)]
use std::path::Path;
use std::sync::Mutex;
const CUDA_SUCCESS: c_int = 0;
const CUBLAS_SUCCESS: c_int = 0;
const CUDA_MEMCPY_H2D: c_int = 1;
const CUDA_MEMCPY_D2H: c_int = 2;
const CUBLAS_OP_N: c_int = 0;
const CUBLAS_OP_T: c_int = 1;
type CudaGetDeviceCount = unsafe extern "C" fn(*mut c_int) -> c_int;
type CudaMalloc = unsafe extern "C" fn(*mut *mut c_void, usize) -> c_int;
type CudaFree = unsafe extern "C" fn(*mut c_void) -> c_int;
type CudaMemcpy = unsafe extern "C" fn(*mut c_void, *const c_void, usize, c_int) -> c_int;
type CudaGetErrorString = unsafe extern "C" fn(c_int) -> *const c_char;
type CublasCreate = unsafe extern "C" fn(*mut *mut c_void) -> c_int;
type CublasDestroy = unsafe extern "C" fn(*mut c_void) -> c_int;
type CublasSgemm = unsafe extern "C" fn(
*mut c_void,
c_int,
c_int,
c_int,
c_int,
c_int,
*const f32,
*const f32,
c_int,
*const f32,
c_int,
*const f32,
*mut f32,
c_int,
) -> c_int;
fn open_first(candidates: &[&str]) -> Result<Library, EngineError> {
let mut last = String::from("no candidate loaded");
for c in candidates {
match unsafe { Library::new(c) } {
Ok(lib) => return Ok(lib),
Err(e) => last = format!("{c}: {e}"),
}
#[cfg(unix)]
if let Ok(lib) = unsafe { Library::new(Path::new(c)) } {
return Ok(lib);
}
}
Err(EngineError::Unsupported(format!("libloading: {last}")))
}
fn nvidia_smi_name() -> Option<String> {
let out = std::process::Command::new("nvidia-smi")
.args(["--query-gpu=name", "--format=csv,noheader"])
.output()
.ok()?;
if !out.status.success() {
return None;
}
let s = String::from_utf8_lossy(&out.stdout);
let line = s.lines().next()?.trim();
if line.is_empty() {
None
} else {
Some(line.to_string())
}
}
pub fn device_info() -> Result<String, EngineError> {
let cudart = open_first(&[
"libcudart.so.12",
"libcudart.so",
"nvcudart.dll",
"libcudart.dylib",
])?;
let cublas = open_first(&[
"libcublas.so.12",
"libcublas.so",
"cublas64_12.dll",
"libcublas.dylib",
])?;
let get_count: Symbol<CudaGetDeviceCount> = unsafe {
cudart
.get(b"cudaGetDeviceCount")
.map_err(|e| EngineError::Unsupported(e.to_string()))?
};
let mut n: c_int = 0;
let st = unsafe { get_count(&mut n) };
if st != CUDA_SUCCESS || n <= 0 {
return Err(EngineError::Unsupported(format!(
"cudaGetDeviceCount status={st} count={n}"
)));
}
let _create: Symbol<CublasCreate> = unsafe {
cublas
.get(b"cublasCreate_v2")
.map_err(|e| EngineError::Unsupported(e.to_string()))?
};
let name = nvidia_smi_name().unwrap_or_else(|| format!("{n} device(s)"));
Ok(name)
}
struct Fns {
malloc: CudaMalloc,
free: CudaFree,
memcpy: CudaMemcpy,
errstr: CudaGetErrorString,
sgemm: CublasSgemm,
}
fn cuda_err(errstr: CudaGetErrorString, st: c_int, what: &str) -> EngineError {
let msg = unsafe {
let p = errstr(st);
if p.is_null() {
format!("{what} cuda status={st}")
} else {
format!("{what}: {}", CStr::from_ptr(p).to_string_lossy())
}
};
EngineError::Unsupported(msg)
}
pub struct CudaContext {
_cudart: Library,
_cublas: Library,
handle: *mut c_void,
fns: Fns,
weights: Mutex<HashMap<usize, (*mut f32, usize)>>,
}
unsafe impl Send for CudaContext {}
unsafe impl Sync for CudaContext {}
impl CudaContext {
pub fn new() -> Result<Self, EngineError> {
let cudart = open_first(&[
"libcudart.so.12",
"libcudart.so",
"nvcudart.dll",
"libcudart.dylib",
])?;
let cublas = open_first(&[
"libcublas.so.12",
"libcublas.so",
"cublas64_12.dll",
"libcublas.dylib",
])?;
let malloc: Symbol<CudaMalloc> = unsafe {
cudart
.get(b"cudaMalloc")
.map_err(|e| EngineError::Unsupported(e.to_string()))?
};
let free: Symbol<CudaFree> = unsafe {
cudart
.get(b"cudaFree")
.map_err(|e| EngineError::Unsupported(e.to_string()))?
};
let memcpy: Symbol<CudaMemcpy> = unsafe {
cudart
.get(b"cudaMemcpy")
.map_err(|e| EngineError::Unsupported(e.to_string()))?
};
let errstr: Symbol<CudaGetErrorString> = unsafe {
cudart
.get(b"cudaGetErrorString")
.map_err(|e| EngineError::Unsupported(e.to_string()))?
};
let create: Symbol<CublasCreate> = unsafe {
cublas
.get(b"cublasCreate_v2")
.map_err(|e| EngineError::Unsupported(e.to_string()))?
};
let sgemm: Symbol<CublasSgemm> = unsafe {
cublas
.get(b"cublasSgemm_v2")
.map_err(|e| EngineError::Unsupported(e.to_string()))?
};
let fns = Fns {
malloc: *malloc,
free: *free,
memcpy: *memcpy,
errstr: *errstr,
sgemm: *sgemm,
};
let mut handle: *mut c_void = std::ptr::null_mut();
let st = unsafe { create(&mut handle) };
if st != CUBLAS_SUCCESS || handle.is_null() {
return Err(EngineError::Unsupported(format!(
"cublasCreate_v2 status={st}"
)));
}
Ok(Self {
_cudart: cudart,
_cublas: cublas,
handle,
fns,
weights: Mutex::new(HashMap::new()),
})
}
pub fn upload(&self, host: &[f32]) -> Result<(), EngineError> {
if host.is_empty() {
return Ok(());
}
let key = host.as_ptr() as usize;
{
let map = self
.weights
.lock()
.map_err(|e| EngineError::Unsupported(e.to_string()))?;
if let Some((_, n)) = map.get(&key) {
if *n == host.len() {
return Ok(());
}
}
}
let bytes = host.len() * 4;
let mut dev: *mut c_void = std::ptr::null_mut();
let st = unsafe { (self.fns.malloc)(&mut dev, bytes) };
if st != CUDA_SUCCESS {
return Err(cuda_err(self.fns.errstr, st, "cudaMalloc"));
}
let st = unsafe { (self.fns.memcpy)(dev, host.as_ptr().cast(), bytes, CUDA_MEMCPY_H2D) };
if st != CUDA_SUCCESS {
unsafe {
(self.fns.free)(dev);
}
return Err(cuda_err(self.fns.errstr, st, "cudaMemcpy H2D"));
}
let mut map = self
.weights
.lock()
.map_err(|e| EngineError::Unsupported(e.to_string()))?;
if let Some((old, _)) = map.insert(key, (dev.cast(), host.len())) {
unsafe {
(self.fns.free)(old.cast());
}
}
Ok(())
}
pub fn linear(
&self,
x: &[f32],
w: &[f32],
out_f: usize,
in_f: usize,
) -> Result<Vec<f32>, EngineError> {
if in_f == 0 || !x.len().is_multiple_of(in_f) {
return Err(EngineError::ShapeMismatch("cuda linear x".into()));
}
if w.len() != out_f * in_f {
return Err(EngineError::ShapeMismatch("cuda linear w".into()));
}
self.upload(w)?;
let batch = x.len() / in_f;
let key = w.as_ptr() as usize;
let map = self
.weights
.lock()
.map_err(|e| EngineError::Unsupported(e.to_string()))?;
let (w_dev, n) = map
.get(&key)
.copied()
.ok_or_else(|| EngineError::Unsupported("cuda weight not uploaded".into()))?;
if n != w.len() {
return Err(EngineError::Unsupported("cuda weight size drift".into()));
}
drop(map);
let x_bytes = x.len() * 4;
let y_bytes = batch * out_f * 4;
let mut x_dev: *mut c_void = std::ptr::null_mut();
let mut y_dev: *mut c_void = std::ptr::null_mut();
let st = unsafe { (self.fns.malloc)(&mut x_dev, x_bytes) };
if st != CUDA_SUCCESS {
return Err(cuda_err(self.fns.errstr, st, "cudaMalloc x"));
}
let st = unsafe { (self.fns.malloc)(&mut y_dev, y_bytes) };
if st != CUDA_SUCCESS {
unsafe {
(self.fns.free)(x_dev);
}
return Err(cuda_err(self.fns.errstr, st, "cudaMalloc y"));
}
let st = unsafe { (self.fns.memcpy)(x_dev, x.as_ptr().cast(), x_bytes, CUDA_MEMCPY_H2D) };
if st != CUDA_SUCCESS {
unsafe {
(self.fns.free)(x_dev);
(self.fns.free)(y_dev);
}
return Err(cuda_err(self.fns.errstr, st, "cudaMemcpy x"));
}
let alpha = 1.0f32;
let beta = 0.0f32;
let m = out_f as c_int;
let n_b = batch as c_int;
let k = in_f as c_int;
let st = unsafe {
(self.fns.sgemm)(
self.handle,
CUBLAS_OP_T,
CUBLAS_OP_N,
m,
n_b,
k,
&alpha,
w_dev,
k,
x_dev.cast(),
k,
&beta,
y_dev.cast(),
m,
)
};
if st != CUBLAS_SUCCESS {
unsafe {
(self.fns.free)(x_dev);
(self.fns.free)(y_dev);
}
return Err(EngineError::Unsupported(format!(
"cublasSgemm_v2 status={st}"
)));
}
let mut y = vec![0.0f32; batch * out_f];
let st = unsafe { (self.fns.memcpy)(y.as_mut_ptr().cast(), y_dev, y_bytes, CUDA_MEMCPY_D2H) };
unsafe {
(self.fns.free)(x_dev);
(self.fns.free)(y_dev);
}
if st != CUDA_SUCCESS {
return Err(cuda_err(self.fns.errstr, st, "cudaMemcpy y"));
}
let _ = CUBLAS_OP_N; Ok(y)
}
}
impl Drop for CudaContext {
fn drop(&mut self) {
if let Ok(mut map) = self.weights.lock() {
for (_, (ptr, _)) in map.drain() {
unsafe {
(self.fns.free)(ptr.cast());
}
}
}
if let Ok(destroy) = unsafe { self._cublas.get::<CublasDestroy>(b"cublasDestroy_v2") } {
if !self.handle.is_null() {
unsafe {
destroy(self.handle);
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::linear;
#[test]
fn cuda_linear_matches_cpu_if_available() {
let Ok(ctx) = CudaContext::new() else {
return;
};
let out_f = 4usize;
let in_f = 3usize;
let w: Vec<f32> = (0..out_f * in_f).map(|i| i as f32 * 0.1 - 0.2).collect();
let x: Vec<f32> = (0..in_f).map(|i| i as f32 * 0.25).collect();
let cpu = linear(&x, &w, out_f, in_f).unwrap();
let gpu = ctx.linear(&x, &w, out_f, in_f).unwrap();
for (a, b) in cpu.iter().zip(gpu.iter()) {
assert!((a - b).abs() < 1e-3, "{a} vs {b}");
}
let xb: Vec<f32> = (0..in_f * 3).map(|i| i as f32 * 0.1).collect();
let cpu_b = linear(&xb, &w, out_f, in_f).unwrap();
let gpu_b = ctx.linear(&xb, &w, out_f, in_f).unwrap();
for (a, b) in cpu_b.iter().zip(gpu_b.iter()) {
assert!((a - b).abs() < 1e-3, "batch {a} vs {b}");
}
}
}