Skip to main content

entrenar/autograd/cuda_backward/
gemm.rs

1#![allow(unsafe_code)]
2#![allow(trivial_casts)]
3#![allow(clippy::borrow_as_ptr)]
4#![allow(clippy::ref_as_ptr)]
5
6#[cfg(feature = "cuda")]
7use trueno_gpu::driver::{CudaStream, GpuBuffer, LaunchConfig};
8#[cfg(feature = "cuda")]
9use trueno_gpu::kernels::backward::{GemmBackwardAKernel, GemmBackwardBKernel};
10#[cfg(feature = "cuda")]
11use trueno_gpu::kernels::Kernel;
12
13use super::super::cuda_tensor::{CudaTensorError, Result};
14#[cfg(feature = "cuda")]
15use super::cache::KERNEL_CACHE;
16
17// cuBLAS backward dispatch (ALB-075)
18#[cfg(feature = "cuda")]
19use crate::autograd::cuda_forward::{
20    bind_cublas_stream, cublas_gemm_backward_a, cublas_gemm_backward_b,
21};
22
23/// Tile size for backward GEMM kernels (C-TILE-BWD-001).
24///
25/// Must be divisible by 4 (unroll factor). Shared memory per block = 2 * TILE^2 * 4 bytes.
26/// TILE=16: 2KB smem, 256 threads/block. Safe for all dimensions including LoRA rank=16.
27const BACKWARD_TILE_SIZE: u32 = 16;
28
29/// GEMM backward pass for matrix A on GPU (trueno#109: tiled)
30///
31/// Given C = A @ B, computes: grad_A = grad_C @ B^T
32///
33/// Uses tiled GEMM with shared memory (C-TILE-BWD-001) and 4x unrolled inner loop.
34#[cfg(feature = "cuda")]
35pub fn gemm_backward_a(
36    grad_output: &GpuBuffer<f32>,
37    b: &GpuBuffer<f32>,
38    grad_a: &mut GpuBuffer<f32>,
39    m: u32,
40    k: u32,
41    n: u32,
42    stream: &CudaStream,
43) -> Result<()> {
44    let cache = KERNEL_CACHE.get().ok_or(CudaTensorError::DeviceNotInitialized)?;
45    let mut cache = cache.lock().map_err(|_err| {
46        CudaTensorError::KernelError("Failed to acquire kernel cache lock".to_string())
47    })?;
48
49    // ALB-075: cuBLAS SIMD fast path (6-14x faster than PTX)
50    // ALB-076: Uses CUBLAS_DEFAULT_MATH (no tensor cores) for backward GEMMs.
51    // trueno#170 fixed NaN corruption caused by tensor core algorithms (TF32)
52    // on transposed GEMMs with gradient magnitudes ~1e5. Forward GEMMs remain
53    // on tensor cores since NoTrans/NoTrans is unaffected.
54    if let Some(cublas) = cache.cublas() {
55        bind_cublas_stream(cublas, stream)?;
56        return cublas_gemm_backward_a(cublas, grad_output, b, grad_a, m, k, n);
57    }
58
59    let tile = BACKWARD_TILE_SIZE;
60    // Kernel object needed for name(); cheap struct creation, PTX deferred.
61    let kernel = GemmBackwardAKernel::tiled_unrolled(m, n, k, tile);
62    let kernel_name = kernel.name();
63
64    let key = format!("gemm_backward_a_{m}_{k}_{n}");
65    let module = match cache.get_cached(&key) {
66        Some(m) => m,
67        None => {
68            let ptx = kernel.emit_ptx_for_target(cache.sm_target());
69            cache.get_or_compile(&key, &ptx)?
70        }
71    };
72
73    // Tiled launch: block = (TILE, TILE), grid covers output grad_a[M, K]
74    let smem = 2 * tile * tile * 4; // 2 tiles of f32
75    let config = LaunchConfig {
76        grid: (k.div_ceil(tile), m.div_ceil(tile), 1),
77        block: (tile, tile, 1),
78        shared_mem: smem,
79    };
80
81    let grad_out_ptr = grad_output.as_ptr();
82    let b_ptr = b.as_ptr();
83    let grad_a_ptr = grad_a.as_ptr();
84
85    // PTX kernel signature: (grad_c_ptr, b_ptr, grad_a_ptr, m, n, k)
86    // CRITICAL: must match param declaration order in GemmBackwardAKernel::build_ptx()
87    let mut args: [*mut std::ffi::c_void; 6] = [
88        &grad_out_ptr as *const _ as *mut _,
89        &b_ptr as *const _ as *mut _,
90        &grad_a_ptr as *const _ as *mut _,
91        &m as *const _ as *mut _,
92        &n as *const _ as *mut _,
93        &k as *const _ as *mut _,
94    ];
95
96    // SAFETY: Kernel launch requires FFI. All buffers are valid GPU allocations with
97    // matching sizes, and the kernel parameters match the expected PTX signature.
98    unsafe {
99        stream.launch_kernel(module, kernel_name, &config, &mut args).map_err(|e| {
100            CudaTensorError::KernelError(format!("GEMM backward A launch failed: {e:?}"))
101        })?;
102    }
103
104    Ok(())
105}
106
107/// GEMM backward pass for matrix B on GPU (trueno#109: tiled)
108///
109/// Given C = A @ B, computes: grad_B = A^T @ grad_C
110///
111/// Uses tiled GEMM with shared memory (C-TILE-BWD-002) and 4x unrolled inner loop.
112#[cfg(feature = "cuda")]
113pub fn gemm_backward_b(
114    a: &GpuBuffer<f32>,
115    grad_output: &GpuBuffer<f32>,
116    grad_b: &mut GpuBuffer<f32>,
117    m: u32,
118    k: u32,
119    n: u32,
120    stream: &CudaStream,
121) -> Result<()> {
122    let cache = KERNEL_CACHE.get().ok_or(CudaTensorError::DeviceNotInitialized)?;
123    let mut cache = cache.lock().map_err(|_err| {
124        CudaTensorError::KernelError("Failed to acquire kernel cache lock".to_string())
125    })?;
126
127    // ALB-075: cuBLAS SIMD fast path (6-14x faster than PTX)
128    // ALB-076: Uses CUBLAS_DEFAULT_MATH (no tensor cores) for backward GEMMs.
129    // trueno#170 fixed NaN corruption caused by tensor core algorithms (TF32)
130    // on transposed GEMMs with gradient magnitudes ~1e5. Forward GEMMs remain
131    // on tensor cores since NoTrans/NoTrans is unaffected.
132    if let Some(cublas) = cache.cublas() {
133        bind_cublas_stream(cublas, stream)?;
134        return cublas_gemm_backward_b(cublas, a, grad_output, grad_b, m, k, n);
135    }
136
137    let tile = BACKWARD_TILE_SIZE;
138    // Kernel object needed for name(); cheap struct creation, PTX deferred.
139    let kernel = GemmBackwardBKernel::tiled_unrolled(m, n, k, tile);
140    let kernel_name = kernel.name();
141
142    let key = format!("gemm_backward_b_{m}_{k}_{n}");
143    let module = match cache.get_cached(&key) {
144        Some(m) => m,
145        None => {
146            let ptx = kernel.emit_ptx_for_target(cache.sm_target());
147            cache.get_or_compile(&key, &ptx)?
148        }
149    };
150
151    // Tiled launch: block = (TILE, TILE), grid covers output grad_b[K, N]
152    let smem = 2 * tile * tile * 4;
153    let config = LaunchConfig {
154        grid: (n.div_ceil(tile), k.div_ceil(tile), 1),
155        block: (tile, tile, 1),
156        shared_mem: smem,
157    };
158
159    let a_ptr = a.as_ptr();
160    let grad_out_ptr = grad_output.as_ptr();
161    let grad_b_ptr = grad_b.as_ptr();
162
163    // PTX kernel signature: (a_ptr, grad_c_ptr, grad_b_ptr, m, n, k)
164    // CRITICAL: must match param declaration order in GemmBackwardBKernel::build_ptx()
165    let mut args: [*mut std::ffi::c_void; 6] = [
166        &a_ptr as *const _ as *mut _,
167        &grad_out_ptr as *const _ as *mut _,
168        &grad_b_ptr as *const _ as *mut _,
169        &m as *const _ as *mut _,
170        &n as *const _ as *mut _,
171        &k as *const _ as *mut _,
172    ];
173
174    // SAFETY: Kernel launch requires FFI. All buffers are valid GPU allocations with
175    // matching sizes, and the kernel parameters match the expected PTX signature.
176    unsafe {
177        stream.launch_kernel(module, kernel_name, &config, &mut args).map_err(|e| {
178            CudaTensorError::KernelError(format!("GEMM backward B launch failed: {e:?}"))
179        })?;
180    }
181
182    Ok(())
183}
184
185/// GEMM backward A with accumulation: grad_A += grad_C @ B^T (PMAT-484)
186///
187/// Adds result into grad_a instead of overwriting. Used for fused Gate+Up backward
188/// to eliminate the separate cuda_add_inplace kernel launch.
189#[cfg(feature = "cuda")]
190pub fn gemm_backward_a_accumulate(
191    grad_output: &GpuBuffer<f32>,
192    b: &GpuBuffer<f32>,
193    grad_a: &mut GpuBuffer<f32>,
194    m: u32,
195    k: u32,
196    n: u32,
197    stream: &CudaStream,
198) -> Result<()> {
199    let cache = KERNEL_CACHE.get().ok_or(CudaTensorError::DeviceNotInitialized)?;
200    let cache = cache.lock().map_err(|_err| {
201        CudaTensorError::KernelError("Failed to acquire kernel cache lock".to_string())
202    })?;
203
204    // cuBLAS accumulate path (beta=1.0) — this is the only path that matters
205    // in production since cuBLAS is always initialized for NF4 QLoRA training.
206    if let Some(cublas) = cache.cublas() {
207        bind_cublas_stream(cublas, stream)?;
208        return crate::autograd::cuda_forward::cublas_gemm_backward_a_accumulate(
209            cublas,
210            grad_output,
211            b,
212            grad_a,
213            m,
214            k,
215            n,
216        );
217    }
218
219    // No cuBLAS = no accumulation support. NF4 training requires cuBLAS.
220    Err(CudaTensorError::KernelError(
221        "gemm_backward_a_accumulate requires cuBLAS (NF4 training always has it)".to_string(),
222    ))
223}
224
225/// FP16-aware backward A with accumulation (PMAT-484): grad_A += grad_C @ B^T
226///
227/// Same as gemm_backward_a_fp16_dispatch but accumulates into grad_a.
228/// Used for fused Gate+Up backward to eliminate cuda_add_inplace.
229#[cfg(feature = "cuda")]
230pub fn gemm_backward_a_fp16_dispatch_accumulate(
231    grad_output: &GpuBuffer<f32>,
232    w_fp16: Option<&GpuBuffer<u16>>,
233    w_fp32: &GpuBuffer<f32>,
234    grad_a: &mut GpuBuffer<f32>,
235    m: u32,
236    k: u32,
237    n: u32,
238    stream: &CudaStream,
239    _ctx: &trueno_gpu::driver::CudaContext,
240) -> Result<()> {
241    // For fp16 path: compute into temp then add (cuBLAS fp16 doesn't easily support beta=1 mixed)
242    // For fp32 path: use cuBLAS beta=1.0 directly
243    if w_fp16.is_some() {
244        // FP16: compute into temp, then accumulate
245        let mut temp = GpuBuffer::<f32>::new(_ctx, (m * k) as usize)
246            .map_err(|e| CudaTensorError::AllocationFailed(format!("fp16 accum temp: {e:?}")))?;
247        gemm_backward_a_fp16_dispatch(
248            grad_output,
249            w_fp16,
250            w_fp32,
251            &mut temp,
252            m,
253            k,
254            n,
255            stream,
256            _ctx,
257        )?;
258        crate::transformer::cuda_block::cuda_add_inplace(grad_a, &temp, (m * k) as usize, stream)?;
259        Ok(())
260    } else {
261        gemm_backward_a_accumulate(grad_output, w_fp32, grad_a, m, k, n, stream)
262    }
263}
264
265/// FP16-aware backward A dispatch (PMAT-472): uses fp16 weights when available.
266///
267/// If `w_fp16` is Some, casts grad_output to fp16 and uses tensor core GEMM
268/// (fp16×fp16→fp32). Otherwise falls back to fp32. Eliminates fp32 weight
269/// storage — freeing ~2.6 GB VRAM for GPU embeddings on yoga 8GB.
270#[cfg(feature = "cuda")]
271pub fn gemm_backward_a_fp16_dispatch(
272    grad_output: &GpuBuffer<f32>,
273    w_fp16: Option<&GpuBuffer<u16>>,
274    w_fp32: &GpuBuffer<f32>,
275    grad_a: &mut GpuBuffer<f32>,
276    m: u32,
277    k: u32,
278    n: u32,
279    stream: &CudaStream,
280    ctx: &trueno_gpu::driver::CudaContext,
281) -> Result<()> {
282    if let Some(w16) = w_fp16 {
283        let elems = (m * n) as usize;
284        let mut grad_f16 = GpuBuffer::<u16>::new(ctx, elems)
285            .map_err(|e| CudaTensorError::AllocationFailed(format!("grad f16 cast: {e:?}")))?;
286        crate::autograd::cuda_forward::cast_f32_to_f16_gpu(
287            grad_output,
288            &mut grad_f16,
289            m * n,
290            stream,
291        )?;
292        crate::autograd::cuda_forward::gemm_f16_to_f32_backward_a(
293            &grad_f16, w16, grad_a, m, k, n, stream,
294        )
295    } else {
296        gemm_backward_a(grad_output, w_fp32, grad_a, m, k, n, stream)
297    }
298}