1#[cfg(feature = "cuda")]
27use std::sync::Arc;
28
29#[cfg(feature = "cuda")]
30use trueno_gpu::driver::{cuda_available, CudaContext, CudaStream, GpuBuffer};
31
32use super::cuda_tensor::{CudaTensorError, Result};
33#[cfg(feature = "cuda")]
34use provable_contracts_macros::requires;
35
36#[cfg(feature = "cuda")]
37use super::cuda_backward::{gemm_backward_a, gemm_backward_b, init_kernel_cache};
38#[cfg(feature = "cuda")]
39use super::cuda_forward::{gemm_forward, init_forward_kernel_cache};
40#[cfg(feature = "cuda")]
41use super::cuda_optim::{adamw_step_cuda, gradient_clip_cuda, init_optim_kernel_cache};
42
43#[cfg(feature = "cuda")]
47pub struct CudaTrainer {
48 ctx: Arc<CudaContext>,
49 stream: CudaStream,
50 step: u32,
51}
52
53#[cfg(feature = "cuda")]
54impl CudaTrainer {
55 pub fn new() -> Result<Self> {
57 Self::with_device(0)
58 }
59
60 pub fn with_device(device_id: i32) -> Result<Self> {
62 if !cuda_available() {
63 return Err(CudaTensorError::CudaNotAvailable("No CUDA driver found".into()));
64 }
65
66 let ctx = Arc::new(
67 CudaContext::new(device_id)
68 .map_err(|e| CudaTensorError::CudaNotAvailable(format!("{e:?}")))?,
69 );
70 let stream = CudaStream::new(&ctx)
71 .map_err(|e| CudaTensorError::AllocationFailed(format!("{e:?}")))?;
72
73 init_forward_kernel_cache(ctx.clone())?;
75 init_kernel_cache(ctx.clone())?;
76 init_optim_kernel_cache(ctx.clone())?;
77
78 Ok(Self { ctx, stream, step: 0 })
85 }
86
87 pub fn context(&self) -> &Arc<CudaContext> {
89 &self.ctx
90 }
91
92 pub fn stream(&self) -> &CudaStream {
94 &self.stream
95 }
96
97 pub fn synchronize(&self) -> Result<()> {
99 self.stream.synchronize().map_err(|e| CudaTensorError::KernelError(format!("{e:?}")))
100 }
101
102 pub fn upload(&self, data: &[f32]) -> Result<GpuBuffer<f32>> {
104 let mut buf = GpuBuffer::from_host(&self.ctx, data)
105 .map_err(|e| CudaTensorError::AllocationFailed(format!("{e:?}")))?;
106 buf.set_context(&self.ctx);
108 Ok(buf)
109 }
110
111 pub fn zeros(&self, len: usize) -> Result<GpuBuffer<f32>> {
113 let data = vec![0.0f32; len];
114 self.upload(&data)
115 }
116
117 pub fn free_memory_mb(&self) -> Option<u64> {
120 self.ctx.memory_info().map(|(free, _total)| (free / (1024 * 1024)) as u64).ok()
121 }
122
123 pub fn download(&self, buffer: &GpuBuffer<f32>) -> Result<Vec<f32>> {
125 let mut result = vec![0.0f32; buffer.len()];
126 buffer
127 .copy_to_host(&mut result)
128 .map_err(|e| CudaTensorError::TransferFailed(format!("{e:?}")))?;
129 Ok(result)
130 }
131
132 pub fn matmul_forward(
140 &self,
141 a: &GpuBuffer<f32>,
142 b: &GpuBuffer<f32>,
143 c: &mut GpuBuffer<f32>,
144 m: u32,
145 k: u32,
146 n: u32,
147 ) -> Result<()> {
148 gemm_forward(a, b, c, m, k, n, &self.stream)
149 }
150
151 #[requires(m > 0 && k > 0 && n > 0)]
158 pub fn matmul_backward(
159 &self,
160 a: &GpuBuffer<f32>,
161 b: &GpuBuffer<f32>,
162 grad_c: &GpuBuffer<f32>,
163 grad_a: &mut GpuBuffer<f32>,
164 grad_b: &mut GpuBuffer<f32>,
165 m: u32,
166 k: u32,
167 n: u32,
168 ) -> Result<()> {
169 gemm_backward_a(grad_c, b, grad_a, m, k, n, &self.stream)?;
170 gemm_backward_b(a, grad_c, grad_b, m, k, n, &self.stream)?;
171 Ok(())
172 }
173
174 pub fn adamw_step(
178 &mut self,
179 params: &mut GpuBuffer<f32>,
180 grads: &GpuBuffer<f32>,
181 m_state: &mut GpuBuffer<f32>,
182 v_state: &mut GpuBuffer<f32>,
183 lr: f32,
184 beta1: f32,
185 beta2: f32,
186 eps: f32,
187 weight_decay: f32,
188 ) -> Result<()> {
189 self.step += 1;
190 let n = params.len() as u32;
191 adamw_step_cuda(
192 params,
193 grads,
194 m_state,
195 v_state,
196 lr,
197 beta1,
198 beta2,
199 eps,
200 weight_decay,
201 self.step,
202 n,
203 &self.stream,
204 )
205 }
206
207 pub fn clip_gradients(&self, grads: &mut GpuBuffer<f32>, max_norm: f32) -> Result<()> {
209 let grad_data = self.download(grads)?;
211 let grad_norm: f32 = grad_data.iter().map(|x| x * x).sum::<f32>().sqrt();
212
213 let scale = if grad_norm > max_norm { max_norm / grad_norm } else { 1.0 };
215
216 gradient_clip_cuda(grads, scale, grads.len() as u32, &self.stream)
218 }
219
220 pub fn step_count(&self) -> u32 {
222 self.step
223 }
224
225 pub fn reset_step(&mut self) {
227 self.step = 0;
228 }
229
230 pub fn device_name(&self) -> String {
232 self.ctx.device_name().unwrap_or_else(|_err| "Unknown GPU".to_string())
233 }
234
235 pub fn total_memory(&self) -> usize {
237 self.ctx.total_memory().unwrap_or(0)
238 }
239}
240
241#[cfg(feature = "cuda")]
242#[allow(clippy::missing_fields_in_debug)]
243impl std::fmt::Debug for CudaTrainer {
244 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
245 f.debug_struct("CudaTrainer")
246 .field("device", &self.device_name())
247 .field("memory_gb", &(self.total_memory() as f64 / 1e9))
248 .field("step", &self.step)
249 .finish()
250 }
251}
252
253#[cfg(not(feature = "cuda"))]
255pub struct CudaTrainer;
256
257#[cfg(not(feature = "cuda"))]
258impl CudaTrainer {
259 pub fn new() -> Result<Self> {
260 Err(CudaTensorError::CudaNotAvailable("Compiled without CUDA support".into()))
261 }
262}
263
264pub fn cuda_training_available() -> bool {
266 #[cfg(feature = "cuda")]
267 {
268 trueno_gpu::driver::cuda_available()
269 }
270 #[cfg(not(feature = "cuda"))]
271 {
272 false
273 }
274}
275
276#[cfg(test)]
277mod tests {
278 use super::*;
279
280 #[test]
281 fn test_cuda_training_available() {
282 let _ = cuda_training_available();
284 }
285
286 #[test]
287 #[cfg(feature = "cuda")]
288 fn test_cuda_trainer_creation() {
289 if !cuda_training_available() {
290 return;
291 }
292
293 let trainer = CudaTrainer::new();
294 assert!(trainer.is_ok());
295
296 let trainer = trainer.expect("operation should succeed");
297 assert!(!trainer.device_name().is_empty());
298 assert!(trainer.total_memory() > 0);
299 }
300
301 #[test]
325 #[cfg(feature = "cuda")]
326 fn falsify_cuda_nf4_forward_nan_001_cublas_stream_ordering() {
327 use crate::autograd::cuda_forward::{gemm_forward, residual_add_forward};
328
329 if !cuda_training_available() {
330 return;
331 }
332 let trainer = CudaTrainer::new().expect("CUDA trainer");
333 let stream = trainer.stream();
334
335 const DIM: usize = 4096;
336 const STEPS: usize = 100; let n = DIM * DIM;
338
339 let mut buf_x = trainer.upload(&vec![1.0f32; n]).expect("upload x");
340 let mut buf_y = trainer.zeros(n).expect("alloc y");
341 let delta = trainer.upload(&vec![1.0f32; n]).expect("upload delta");
342 let ones_col = trainer.upload(&vec![1.0f32; DIM]).expect("upload ones");
343 let mut out = trainer.zeros(DIM).expect("alloc out");
344
345 gemm_forward(&buf_x, &ones_col, &mut out, DIM as u32, DIM as u32, 1, stream)
351 .expect("cuBLAS warm-up gemm");
352 residual_add_forward(&buf_x, &delta, &mut buf_y, n as u32, stream).expect("warm-up add");
353 trainer.synchronize().expect("trainer stream quiesce");
354 let _ = trainer.download(&out).expect("NULL stream quiesce");
355 buf_x.copy_from_host(&vec![1.0f32; n]).expect("reset x");
358
359 for _ in 0..STEPS {
361 residual_add_forward(&buf_x, &delta, &mut buf_y, n as u32, stream)
362 .expect("residual add");
363 std::mem::swap(&mut buf_x, &mut buf_y);
364 }
365
366 gemm_forward(&buf_x, &ones_col, &mut out, DIM as u32, DIM as u32, 1, stream)
370 .expect("cuBLAS gemm");
371
372 trainer.synchronize().expect("stream sync");
373 let result = trainer.download(&out).expect("download out");
374
375 let expected = (1.0 + STEPS as f32) * DIM as f32; let worst = result.iter().fold(0.0f32, |acc, &v| acc.max((v - expected).abs()));
377 assert!(
378 worst <= expected * 1e-3,
379 "FALSIFY-CUDA-NF4-FORWARD-NAN-001: cuBLAS GEMM read the activation \
380 matrix while the producer stream was still writing it \
381 (worst |Δ|={worst}, expected {expected}). The cuBLAS handle is not \
382 bound to the caller's CU_STREAM_NON_BLOCKING stream — restore the \
383 per-call bind_cublas_stream() at the cuBLAS dispatch sites \
384 (cuda_forward::matmul / matmul_f16, cuda_backward::gemm)."
385 );
386 }
387
388 #[test]
389 #[cfg(feature = "cuda")]
390 fn test_cuda_trainer_upload_download() {
391 if !cuda_training_available() {
392 return;
393 }
394
395 let trainer = CudaTrainer::new().expect("operation should succeed");
396 let data = vec![1.0, 2.0, 3.0, 4.0];
397
398 let gpu_buffer = trainer.upload(&data).expect("load should succeed");
399 let result = trainer.download(&gpu_buffer).expect("load should succeed");
400
401 assert_eq!(data, result);
402 }
403
404 #[test]
405 #[cfg(feature = "cuda")]
406 fn test_cuda_trainer_zeros() {
407 if !cuda_training_available() {
408 return;
409 }
410
411 let trainer = CudaTrainer::new().expect("operation should succeed");
412 let gpu_buffer = trainer.zeros(100).expect("operation should succeed");
413 let result = trainer.download(&gpu_buffer).expect("load should succeed");
414
415 assert_eq!(result.len(), 100);
416 assert!(result.iter().all(|&x| x == 0.0));
417 }
418
419 #[test]
420 #[cfg(feature = "cuda")]
421 fn test_cuda_trainer_synchronize() {
422 if !cuda_training_available() {
423 return;
424 }
425
426 let trainer = CudaTrainer::new().expect("operation should succeed");
427 assert!(trainer.synchronize().is_ok());
429 }
430
431 #[test]
432 #[cfg(feature = "cuda")]
433 fn test_cuda_trainer_context_and_stream() {
434 if !cuda_training_available() {
435 return;
436 }
437
438 let trainer = CudaTrainer::new().expect("operation should succeed");
439 let _ctx = trainer.context();
441 let _stream = trainer.stream();
442 }
443
444 #[test]
445 #[cfg(feature = "cuda")]
446 fn test_cuda_trainer_step_count() {
447 if !cuda_training_available() {
448 return;
449 }
450
451 let mut trainer = CudaTrainer::new().expect("operation should succeed");
452 assert_eq!(trainer.step_count(), 0);
453
454 let mut params = trainer.upload(&[1.0, 2.0, 3.0]).expect("load should succeed");
456 let grads = trainer.upload(&[0.1, 0.1, 0.1]).expect("load should succeed");
457 let mut m_state = trainer.zeros(3).expect("operation should succeed");
458 let mut v_state = trainer.zeros(3).expect("operation should succeed");
459
460 trainer
461 .adamw_step(
462 &mut params,
463 &grads,
464 &mut m_state,
465 &mut v_state,
466 0.001,
467 0.9,
468 0.999,
469 1e-8,
470 0.0,
471 )
472 .expect("operation should succeed");
473
474 assert_eq!(trainer.step_count(), 1);
475
476 trainer.reset_step();
477 assert_eq!(trainer.step_count(), 0);
478 }
479
480 #[test]
481 #[cfg(feature = "cuda")]
482 fn test_cuda_trainer_matmul_forward() {
483 if !cuda_training_available() {
484 return;
485 }
486
487 let trainer = CudaTrainer::new().expect("operation should succeed");
488
489 let a_data: Vec<f32> = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]; let b_data: Vec<f32> = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]; let c_data: Vec<f32> = vec![0.0; 4]; let a = trainer.upload(&a_data).expect("load should succeed");
495 let b = trainer.upload(&b_data).expect("load should succeed");
496 let mut c = trainer.upload(&c_data).expect("load should succeed");
497
498 trainer.matmul_forward(&a, &b, &mut c, 2, 3, 2).expect("operation should succeed");
499 trainer.synchronize().expect("operation should succeed");
500
501 let result = trainer.download(&c).expect("load should succeed");
502 assert!(!result.iter().all(|&x| x == 0.0));
504 }
505
506 #[test]
507 #[cfg(feature = "cuda")]
508 fn test_cuda_trainer_clip_gradients() {
509 if !cuda_training_available() {
510 return;
511 }
512
513 let trainer = CudaTrainer::new().expect("operation should succeed");
514
515 let grad_data: Vec<f32> = vec![10.0, 10.0, 10.0, 10.0]; let mut grads = trainer.upload(&grad_data).expect("load should succeed");
518
519 trainer.clip_gradients(&mut grads, 1.0).expect("operation should succeed");
521 trainer.synchronize().expect("operation should succeed");
522
523 let result = trainer.download(&grads).expect("load should succeed");
524 let norm: f32 = result.iter().map(|x| x * x).sum::<f32>().sqrt();
526 assert!(norm <= 1.1, "Gradient norm should be clipped to ~1.0, got {norm}");
527 }
528
529 #[test]
530 #[cfg(feature = "cuda")]
531 fn test_cuda_trainer_debug_impl() {
532 if !cuda_training_available() {
533 return;
534 }
535
536 let trainer = CudaTrainer::new().expect("operation should succeed");
537 let debug_str = format!("{trainer:?}");
538 assert!(debug_str.contains("CudaTrainer"));
539 assert!(debug_str.contains("device"));
540 assert!(debug_str.contains("step"));
541 }
542}