1use crate::EngineError;
4use libloading::{Library, Symbol};
5use std::collections::HashMap;
6use std::ffi::{c_char, c_int, c_void, CStr};
7#[cfg(unix)]
8use std::path::Path;
9use std::sync::Mutex;
10
11const CUDA_SUCCESS: c_int = 0;
12const CUBLAS_SUCCESS: c_int = 0;
13const CUDA_MEMCPY_H2D: c_int = 1;
14const CUDA_MEMCPY_D2H: c_int = 2;
15const CUBLAS_OP_N: c_int = 0;
16const CUBLAS_OP_T: c_int = 1;
17
18type CudaGetDeviceCount = unsafe extern "C" fn(*mut c_int) -> c_int;
19type CudaMalloc = unsafe extern "C" fn(*mut *mut c_void, usize) -> c_int;
20type CudaFree = unsafe extern "C" fn(*mut c_void) -> c_int;
21type CudaMemcpy = unsafe extern "C" fn(*mut c_void, *const c_void, usize, c_int) -> c_int;
22type CudaGetErrorString = unsafe extern "C" fn(c_int) -> *const c_char;
23type CublasCreate = unsafe extern "C" fn(*mut *mut c_void) -> c_int;
24type CublasDestroy = unsafe extern "C" fn(*mut c_void) -> c_int;
25type CublasSgemm = unsafe extern "C" fn(
26 *mut c_void,
27 c_int,
28 c_int,
29 c_int,
30 c_int,
31 c_int,
32 *const f32,
33 *const f32,
34 c_int,
35 *const f32,
36 c_int,
37 *const f32,
38 *mut f32,
39 c_int,
40) -> c_int;
41
42fn open_first(candidates: &[&str]) -> Result<Library, EngineError> {
43 let mut last = String::from("no candidate loaded");
44 for c in candidates {
45 match unsafe { Library::new(c) } {
46 Ok(lib) => return Ok(lib),
47 Err(e) => last = format!("{c}: {e}"),
48 }
49 #[cfg(unix)]
50 if let Ok(lib) = unsafe { Library::new(Path::new(c)) } {
51 return Ok(lib);
52 }
53 }
54 Err(EngineError::Unsupported(format!("libloading: {last}")))
55}
56
57fn nvidia_smi_name() -> Option<String> {
58 let out = std::process::Command::new("nvidia-smi")
59 .args(["--query-gpu=name", "--format=csv,noheader"])
60 .output()
61 .ok()?;
62 if !out.status.success() {
63 return None;
64 }
65 let s = String::from_utf8_lossy(&out.stdout);
66 let line = s.lines().next()?.trim();
67 if line.is_empty() {
68 None
69 } else {
70 Some(line.to_string())
71 }
72}
73
74pub fn device_info() -> Result<String, EngineError> {
76 let cudart = open_first(&[
77 "libcudart.so.12",
78 "libcudart.so",
79 "nvcudart.dll",
80 "libcudart.dylib",
81 ])?;
82 let cublas = open_first(&[
83 "libcublas.so.12",
84 "libcublas.so",
85 "cublas64_12.dll",
86 "libcublas.dylib",
87 ])?;
88 let get_count: Symbol<CudaGetDeviceCount> = unsafe {
89 cudart
90 .get(b"cudaGetDeviceCount")
91 .map_err(|e| EngineError::Unsupported(e.to_string()))?
92 };
93 let mut n: c_int = 0;
94 let st = unsafe { get_count(&mut n) };
95 if st != CUDA_SUCCESS || n <= 0 {
96 return Err(EngineError::Unsupported(format!(
97 "cudaGetDeviceCount status={st} count={n}"
98 )));
99 }
100 let _create: Symbol<CublasCreate> = unsafe {
102 cublas
103 .get(b"cublasCreate_v2")
104 .map_err(|e| EngineError::Unsupported(e.to_string()))?
105 };
106 let name = nvidia_smi_name().unwrap_or_else(|| format!("{n} device(s)"));
107 Ok(name)
108}
109
110struct Fns {
111 malloc: CudaMalloc,
112 free: CudaFree,
113 memcpy: CudaMemcpy,
114 errstr: CudaGetErrorString,
115 sgemm: CublasSgemm,
116}
117
118fn cuda_err(errstr: CudaGetErrorString, st: c_int, what: &str) -> EngineError {
119 let msg = unsafe {
120 let p = errstr(st);
121 if p.is_null() {
122 format!("{what} cuda status={st}")
123 } else {
124 format!("{what}: {}", CStr::from_ptr(p).to_string_lossy())
125 }
126 };
127 EngineError::Unsupported(msg)
128}
129
130pub struct CudaContext {
132 _cudart: Library,
133 _cublas: Library,
134 handle: *mut c_void,
135 fns: Fns,
136 weights: Mutex<HashMap<usize, (*mut f32, usize)>>,
137}
138
139unsafe impl Send for CudaContext {}
140unsafe impl Sync for CudaContext {}
141
142impl CudaContext {
143 pub fn new() -> Result<Self, EngineError> {
144 let cudart = open_first(&[
145 "libcudart.so.12",
146 "libcudart.so",
147 "nvcudart.dll",
148 "libcudart.dylib",
149 ])?;
150 let cublas = open_first(&[
151 "libcublas.so.12",
152 "libcublas.so",
153 "cublas64_12.dll",
154 "libcublas.dylib",
155 ])?;
156 let malloc: Symbol<CudaMalloc> = unsafe {
157 cudart
158 .get(b"cudaMalloc")
159 .map_err(|e| EngineError::Unsupported(e.to_string()))?
160 };
161 let free: Symbol<CudaFree> = unsafe {
162 cudart
163 .get(b"cudaFree")
164 .map_err(|e| EngineError::Unsupported(e.to_string()))?
165 };
166 let memcpy: Symbol<CudaMemcpy> = unsafe {
167 cudart
168 .get(b"cudaMemcpy")
169 .map_err(|e| EngineError::Unsupported(e.to_string()))?
170 };
171 let errstr: Symbol<CudaGetErrorString> = unsafe {
172 cudart
173 .get(b"cudaGetErrorString")
174 .map_err(|e| EngineError::Unsupported(e.to_string()))?
175 };
176 let create: Symbol<CublasCreate> = unsafe {
177 cublas
178 .get(b"cublasCreate_v2")
179 .map_err(|e| EngineError::Unsupported(e.to_string()))?
180 };
181 let sgemm: Symbol<CublasSgemm> = unsafe {
182 cublas
183 .get(b"cublasSgemm_v2")
184 .map_err(|e| EngineError::Unsupported(e.to_string()))?
185 };
186 let fns = Fns {
187 malloc: *malloc,
188 free: *free,
189 memcpy: *memcpy,
190 errstr: *errstr,
191 sgemm: *sgemm,
192 };
193 let mut handle: *mut c_void = std::ptr::null_mut();
194 let st = unsafe { create(&mut handle) };
195 if st != CUBLAS_SUCCESS || handle.is_null() {
196 return Err(EngineError::Unsupported(format!(
197 "cublasCreate_v2 status={st}"
198 )));
199 }
200 Ok(Self {
201 _cudart: cudart,
202 _cublas: cublas,
203 handle,
204 fns,
205 weights: Mutex::new(HashMap::new()),
206 })
207 }
208
209 pub fn upload(&self, host: &[f32]) -> Result<(), EngineError> {
210 if host.is_empty() {
211 return Ok(());
212 }
213 let key = host.as_ptr() as usize;
214 {
215 let map = self
216 .weights
217 .lock()
218 .map_err(|e| EngineError::Unsupported(e.to_string()))?;
219 if let Some((_, n)) = map.get(&key) {
220 if *n == host.len() {
221 return Ok(());
222 }
223 }
224 }
225 let bytes = host.len() * 4;
226 let mut dev: *mut c_void = std::ptr::null_mut();
227 let st = unsafe { (self.fns.malloc)(&mut dev, bytes) };
228 if st != CUDA_SUCCESS {
229 return Err(cuda_err(self.fns.errstr, st, "cudaMalloc"));
230 }
231 let st = unsafe { (self.fns.memcpy)(dev, host.as_ptr().cast(), bytes, CUDA_MEMCPY_H2D) };
232 if st != CUDA_SUCCESS {
233 unsafe {
234 (self.fns.free)(dev);
235 }
236 return Err(cuda_err(self.fns.errstr, st, "cudaMemcpy H2D"));
237 }
238 let mut map = self
239 .weights
240 .lock()
241 .map_err(|e| EngineError::Unsupported(e.to_string()))?;
242 if let Some((old, _)) = map.insert(key, (dev.cast(), host.len())) {
243 unsafe {
244 (self.fns.free)(old.cast());
245 }
246 }
247 Ok(())
248 }
249
250 pub fn linear(
252 &self,
253 x: &[f32],
254 w: &[f32],
255 out_f: usize,
256 in_f: usize,
257 ) -> Result<Vec<f32>, EngineError> {
258 if in_f == 0 || !x.len().is_multiple_of(in_f) {
259 return Err(EngineError::ShapeMismatch("cuda linear x".into()));
260 }
261 if w.len() != out_f * in_f {
262 return Err(EngineError::ShapeMismatch("cuda linear w".into()));
263 }
264 self.upload(w)?;
265 let batch = x.len() / in_f;
266 let key = w.as_ptr() as usize;
267 let map = self
268 .weights
269 .lock()
270 .map_err(|e| EngineError::Unsupported(e.to_string()))?;
271 let (w_dev, n) = map
272 .get(&key)
273 .copied()
274 .ok_or_else(|| EngineError::Unsupported("cuda weight not uploaded".into()))?;
275 if n != w.len() {
276 return Err(EngineError::Unsupported("cuda weight size drift".into()));
277 }
278 drop(map);
279
280 let x_bytes = x.len() * 4;
281 let y_bytes = batch * out_f * 4;
282 let mut x_dev: *mut c_void = std::ptr::null_mut();
283 let mut y_dev: *mut c_void = std::ptr::null_mut();
284 let st = unsafe { (self.fns.malloc)(&mut x_dev, x_bytes) };
285 if st != CUDA_SUCCESS {
286 return Err(cuda_err(self.fns.errstr, st, "cudaMalloc x"));
287 }
288 let st = unsafe { (self.fns.malloc)(&mut y_dev, y_bytes) };
289 if st != CUDA_SUCCESS {
290 unsafe {
291 (self.fns.free)(x_dev);
292 }
293 return Err(cuda_err(self.fns.errstr, st, "cudaMalloc y"));
294 }
295 let st = unsafe { (self.fns.memcpy)(x_dev, x.as_ptr().cast(), x_bytes, CUDA_MEMCPY_H2D) };
296 if st != CUDA_SUCCESS {
297 unsafe {
298 (self.fns.free)(x_dev);
299 (self.fns.free)(y_dev);
300 }
301 return Err(cuda_err(self.fns.errstr, st, "cudaMemcpy x"));
302 }
303
304 let alpha = 1.0f32;
305 let beta = 0.0f32;
306 let m = out_f as c_int;
308 let n_b = batch as c_int;
309 let k = in_f as c_int;
310 let st = unsafe {
311 (self.fns.sgemm)(
312 self.handle,
313 CUBLAS_OP_T,
314 CUBLAS_OP_N,
315 m,
316 n_b,
317 k,
318 &alpha,
319 w_dev,
320 k,
321 x_dev.cast(),
322 k,
323 &beta,
324 y_dev.cast(),
325 m,
326 )
327 };
328 if st != CUBLAS_SUCCESS {
329 unsafe {
330 (self.fns.free)(x_dev);
331 (self.fns.free)(y_dev);
332 }
333 return Err(EngineError::Unsupported(format!(
334 "cublasSgemm_v2 status={st}"
335 )));
336 }
337 let mut y = vec![0.0f32; batch * out_f];
338 let st = unsafe { (self.fns.memcpy)(y.as_mut_ptr().cast(), y_dev, y_bytes, CUDA_MEMCPY_D2H) };
339 unsafe {
340 (self.fns.free)(x_dev);
341 (self.fns.free)(y_dev);
342 }
343 if st != CUDA_SUCCESS {
344 return Err(cuda_err(self.fns.errstr, st, "cudaMemcpy y"));
345 }
346 let _ = CUBLAS_OP_N; Ok(y)
348 }
349}
350
351impl Drop for CudaContext {
352 fn drop(&mut self) {
353 if let Ok(mut map) = self.weights.lock() {
354 for (_, (ptr, _)) in map.drain() {
355 unsafe {
356 (self.fns.free)(ptr.cast());
357 }
358 }
359 }
360 if let Ok(destroy) = unsafe { self._cublas.get::<CublasDestroy>(b"cublasDestroy_v2") } {
361 if !self.handle.is_null() {
362 unsafe {
363 destroy(self.handle);
364 }
365 }
366 }
367 }
368}
369
370#[cfg(test)]
371mod tests {
372 use super::*;
373 use crate::linear;
374
375 #[test]
376 fn cuda_linear_matches_cpu_if_available() {
377 let Ok(ctx) = CudaContext::new() else {
378 return;
379 };
380 let out_f = 4usize;
381 let in_f = 3usize;
382 let w: Vec<f32> = (0..out_f * in_f).map(|i| i as f32 * 0.1 - 0.2).collect();
383 let x: Vec<f32> = (0..in_f).map(|i| i as f32 * 0.25).collect();
384 let cpu = linear(&x, &w, out_f, in_f).unwrap();
385 let gpu = ctx.linear(&x, &w, out_f, in_f).unwrap();
386 for (a, b) in cpu.iter().zip(gpu.iter()) {
387 assert!((a - b).abs() < 1e-3, "{a} vs {b}");
388 }
389 let xb: Vec<f32> = (0..in_f * 3).map(|i| i as f32 * 0.1).collect();
390 let cpu_b = linear(&xb, &w, out_f, in_f).unwrap();
391 let gpu_b = ctx.linear(&xb, &w, out_f, in_f).unwrap();
392 for (a, b) in cpu_b.iter().zip(gpu_b.iter()) {
393 assert!((a - b).abs() < 1e-3, "batch {a} vs {b}");
394 }
395 }
396}