entrenar/autograd/cuda_forward/cache.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 std::collections::HashMap;
8#[cfg(feature = "cuda")]
9use std::sync::{Mutex, OnceLock};
10
11#[cfg(feature = "cuda")]
12use trueno_gpu::driver::{CublasHandle, CudaContext, CudaModule, CudaStream};
13#[cfg(feature = "cuda")]
14use trueno_gpu::kernels::{
15 Batched4DGemmKernel, BatchedRopeNeoxBackwardKernel, BatchedSoftmaxKernel,
16 BatchedToInterleavedKernel, BatchedTransposeKernel, BatchedVectorizedRmsNormKernel,
17 ElementwiseMulKernel, FusedSwigluKernel, GemmKernel, InterleavedToBatchedKernel, Kernel,
18 Nf4GemmKernel, Nf4GemmTransposeKernel, ResidualAddKernel, ScaleKernel, SiluKernel,
19};
20
21use crate::autograd::cuda_tensor::{CudaTensorError, Result};
22
23/// Cached compiled CUDA modules for forward kernels
24#[cfg(feature = "cuda")]
25pub(super) static FORWARD_KERNEL_CACHE: OnceLock<Mutex<ForwardKernelCache>> = OnceLock::new();
26
27/// Cache for compiled forward kernel modules
28///
29/// Stores the device's SM target (e.g. "sm_89") detected at init time.
30/// All PTX must be emitted for this target before compilation.
31///
32/// # Contract: F-PTX-001 (Target Parity)
33///
34/// PTX `.target` directive MUST match the device compute capability.
35/// The cache validates this at compile time and rejects mismatched PTX.
36#[cfg(feature = "cuda")]
37pub(super) struct ForwardKernelCache {
38 ctx: std::sync::Arc<CudaContext>,
39 modules: HashMap<String, CudaModule>,
40 /// Device SM target string (e.g. "sm_89" for RTX 4090)
41 sm_target: String,
42 /// cuBLAS handle (ALB-075): forward=tensor cores, backward=SIMD (ALB-076/trueno#170)
43 cublas: Option<CublasHandle>,
44}
45
46#[cfg(feature = "cuda")]
47impl ForwardKernelCache {
48 pub(super) fn new(ctx: std::sync::Arc<CudaContext>) -> Self {
49 // Detect device compute capability at construction time.
50 // Falls back to sm_70 if detection fails (should never happen
51 // since we already have a valid CudaContext).
52 let sm_target = ctx.sm_target().unwrap_or_else(|_| "sm_70".to_string());
53
54 // entrenar#318: Forward uses TF32 tensor cores (~41x faster than SIMD on sm_89).
55 // ALB-076: TF32 is safe for forward (NoTrans/NoTrans). Backward uses SIMD handle.
56 let cublas = match CublasHandle::new_with_tensor_cores(&ctx) {
57 Ok(handle) => {
58 eprintln!("[CUDA] cuBLAS initialized — forward TF32 tensor cores (41x vs SIMD)");
59 Some(handle)
60 }
61 Err(e) => {
62 eprintln!("[CUDA] cuBLAS not available ({e:?}), using PTX GEMMs");
63 None
64 }
65 };
66
67 eprintln!("[CUDA] Kernel cache initialized for target: {sm_target}");
68 Self { ctx, modules: HashMap::new(), sm_target, cublas }
69 }
70
71 /// Get a reference to the cuBLAS handle, if available.
72 pub(super) fn cublas(&self) -> Option<&CublasHandle> {
73 self.cublas.as_ref()
74 }
75
76 /// Bind cuBLAS to a stream for the current training step.
77 pub(super) fn set_cublas_stream(&self, stream: &CudaStream) -> Result<()> {
78 if let Some(ref handle) = self.cublas {
79 handle.set_stream(stream).map_err(|e| {
80 CudaTensorError::KernelError(format!("cuBLAS set_stream failed: {e:?}"))
81 })?;
82 }
83 Ok(())
84 }
85
86 /// Get the device SM target for PTX emission.
87 ///
88 /// Consumers MUST use this to emit PTX via `kernel.emit_ptx_for_target(cache.sm_target())`.
89 pub(super) fn sm_target(&self) -> &str {
90 &self.sm_target
91 }
92
93 /// Look up a previously compiled module by key (KAIZEN-058).
94 ///
95 /// Returns `Some` if the module is already cached (post-pre-warm: always).
96 /// Callers should use this before generating PTX to avoid unnecessary
97 /// multi-KB String allocations (~1000 per training step).
98 pub(super) fn get_cached(&mut self, name: &str) -> Option<&mut CudaModule> {
99 self.modules.get_mut(name)
100 }
101
102 /// Compile PTX and cache the resulting module.
103 ///
104 /// # Contract: F-PTX-001 (Target Parity)
105 ///
106 /// Validates that the PTX `.target` directive matches the device's compute
107 /// capability. Rejects PTX compiled for the wrong architecture.
108 pub(super) fn get_or_compile(&mut self, name: &str, ptx: &str) -> Result<&mut CudaModule> {
109 use std::collections::hash_map::Entry;
110
111 // F-PTX-001: Validate PTX target matches device
112 if let Some(target_line) = ptx.lines().find(|l| l.starts_with(".target ")) {
113 let ptx_target = target_line.trim().trim_start_matches(".target ");
114 if ptx_target != self.sm_target {
115 return Err(CudaTensorError::KernelError(format!(
116 "F-PTX-001 violated: PTX target '{ptx_target}' != device target '{}'. \
117 Use kernel.emit_ptx_for_target(\"{}\") instead of emit_ptx().",
118 self.sm_target, self.sm_target
119 )));
120 }
121 }
122
123 match self.modules.entry(name.to_string()) {
124 Entry::Occupied(e) => Ok(e.into_mut()),
125 Entry::Vacant(e) => {
126 // PMAT-698i: diagnostic logging. Surfaces every forward-cache
127 // JIT event with its kernel name so missing pre-warm entries
128 // are identifiable in O(1) instead of O(N) iterations.
129 eprintln!("[FWD-CACHE] Compiling '{name}' (ptx_len={})", ptx.len());
130 // trueno#200: Use from_ptx_direct on Blackwell
131 let (major, _) = self.ctx.compute_capability().map_err(|e| {
132 CudaTensorError::KernelError(format!("compute_capability: {e:?}"))
133 })?;
134 let module = if major >= 12 {
135 CudaModule::from_ptx_direct(&self.ctx, ptx)
136 } else {
137 CudaModule::from_ptx(&self.ctx, ptx)
138 }
139 .map_err(|err| {
140 CudaTensorError::KernelError(format!("Failed to compile {name}: {err:?}"))
141 })?;
142 eprintln!("[FWD-CACHE] OK '{name}'");
143 Ok(e.insert(module))
144 }
145 }
146 }
147
148 /// Pre-warm all kernels needed for transformer forward pass.
149 ///
150 /// # Contract: C-PREWARM-001 (JIT Before Payload)
151 ///
152 /// - **Precondition**: Kernel cache initialized, GPU VRAM mostly free (no blocks uploaded yet)
153 /// - **Postcondition**: All forward-pass PTX modules JIT-compiled and cached
154 /// - **Invariant**: Subsequent `get_or_compile()` calls for these keys hit cache (zero JIT)
155 ///
156 /// CUDA's `cuModuleLoadDataEx` JIT compiler needs device memory for compilation.
157 /// If called after uploading 36 transformer blocks (~22 GB), the near-OOM state causes
158 /// `CUDA_ERROR_ILLEGAL_ADDRESS` during JIT (trueno#107). Pre-warming compiles all PTX
159 /// while VRAM is free, avoiding this failure mode entirely.
160 pub(super) fn pre_warm_for_model(
161 &mut self,
162 hidden_size: usize,
163 intermediate_size: usize,
164 num_heads: usize,
165 num_kv_heads: usize,
166 head_dim: usize,
167 max_seq_len: usize,
168 ) -> Result<()> {
169 let s = max_seq_len as u32;
170 let h = hidden_size as u32;
171 let q_dim = (num_heads * head_dim) as u32; // Q/O projection dim (may differ from h)
172 let kv_h = (num_kv_heads * head_dim) as u32;
173 let i = intermediate_size as u32;
174 let nh = num_heads as u32;
175 let _nkv = num_kv_heads as u32;
176 let hd = head_dim as u32;
177 let sh = s * h; // seq_len * hidden_size
178 let si = s * i; // seq_len * intermediate_size
179
180 let mut count = 0u32;
181 let target = self.sm_target.clone();
182
183 // Helper: generate PTX and compile.
184 //
185 // PMAT-698j: previously hardcoded "silu_forward" as the cache key,
186 // which meant every warm!() call collided on the same HashMap entry.
187 // Only the FIRST kernel compiled actually got stored; all subsequent
188 // warm!() invocations short-circuited because "silu_forward" was
189 // already occupied. At runtime every other kernel (rmsnorm, rope,
190 // softmax, swiglu, residual, etc.) cache-missed under its real key
191 // and JIT-compiled mid-training — on Blackwell sm_121 that
192 // corrupted the CUDA stream and surfaced as the cascading "Block 0
193 // upload failed" / "forward_backward_with_grad returned None"
194 // errors hunted across PMAT-698e..i.
195 //
196 // Discovered by PMAT-698i diagnostic logging: [FWD-CACHE] showed
197 // every "pre-warmed" kernel actually JIT'd at first use because
198 // the cache only contained one entry. One-character fix.
199 macro_rules! warm {
200 ($key:expr, $kernel:expr) => {{
201 let key = $key;
202 let ptx = $kernel.emit_ptx_for_target(&target);
203 self.get_or_compile(&key, &ptx)?;
204 count += 1;
205 }};
206 }
207
208 // 1. RMSNorm (batched: single launch for all rows via grid.y)
209 // ALB-076: Use BatchedVectorizedRmsNormKernel instead of per-row RmsNormKernel
210 //
211 // PMAT-698k: the runtime key format includes the eps as bit-pattern
212 // suffix (normalization.rs:139:
213 // let key = format!("batched_rmsnorm_fwd_{hidden_size}_eps{eps_bits:08x}"))
214 // Pre-warm key used to omit the eps suffix → cache miss at runtime →
215 // JIT mid-forward → Blackwell sm_121 stream poisoning.
216 //
217 // PMAT-698n: PMAT-698k pre-warmed at eps=1e-5 (0x3727c5ac) but the
218 // dominant model (Qwen2 / Qwen2.5) uses rms_norm_eps=1e-6
219 // (0x358637bd). Live diagnostic confirmed the runtime key on the
220 // Phase 3 dispatch was `batched_rmsnorm_fwd_896_eps358637bd`. Switch
221 // the pre-warm default to 1e-6 (Qwen2 standard) AND additionally
222 // pre-warm 1e-5 (Llama/Mistral standard) for cross-family coverage.
223 // The cost of pre-warming both is ~30 KB of cache headroom.
224 let qwen2_eps_bits = 1.0e-6_f32.to_bits(); // 0x358637bd
225 let llama_eps_bits = 1.0e-5_f32.to_bits(); // 0x3727c5ac
226 warm!(
227 format!("batched_rmsnorm_fwd_{h}_eps{qwen2_eps_bits:08x}"),
228 BatchedVectorizedRmsNormKernel::new(h, 1)
229 );
230 if qwen2_eps_bits != llama_eps_bits {
231 warm!(
232 format!("batched_rmsnorm_fwd_{h}_eps{llama_eps_bits:08x}"),
233 BatchedVectorizedRmsNormKernel::new(h, 1)
234 );
235 }
236
237 // 1b. Fused residual add + RMSNorm (post-attention norm in the NF4
238 // QLoRA block). FALSIFY-CUDA-FUSED-RMSNORM-DEADLOCK-001 fix #4: this
239 // kernel previously had NO pre-warm entry and JIT-compiled
240 // mid-training (Blackwell stream-poisoning class, PMAT-698). Warm at
241 // both Qwen2 (1e-6) and Llama (1e-5) eps like batched_rmsnorm_fwd.
242 {
243 use trueno_gpu::kernels::BatchedFusedResidualRmsNormKernel;
244 for eps in [1.0e-6_f32, 1.0e-5_f32] {
245 let eps_bits = eps.to_bits();
246 warm!(
247 format!("batched_fused_residual_rmsnorm_{h}_eps{eps_bits:08x}"),
248 BatchedFusedResidualRmsNormKernel::new(h, 1).with_epsilon(eps)
249 );
250 }
251 }
252
253 // PMAT-700 (SPEC-BLACKWELL-FIX-001 Fix #2): when cuBLAS is available
254 // and the runtime takes its fast path for the standard 2D GEMMs
255 // (Q/K/V/O/gate/up/down projections — see ALB-075 dispatch in
256 // gemm.rs:47-49 and cuda_block.rs:2895), pre-warming the PTX
257 // equivalents is wasted VRAM. On sm_121 (Blackwell GB10) the
258 // resulting JIT-cache footprint pushes block upload over the budget
259 // and CUDA_ERROR_OUT_OF_MEMORY fires at "Block 0 upload". Skipping
260 // these four pre-warms when cuBLAS is bound saves ~5-7 PTX modules
261 // per cache (more on multi-block-size models) and unblocks gx10
262 // dispatch without any runtime path change.
263 //
264 // Falsifier: F-BLACKWELL-CUBLAS-PREWARM-001 — assert the cache
265 // module count after pre_warm_for_model decreases when cuBLAS is
266 // present, and that runtime forward still produces identical
267 // results on a known input (cuBLAS path was already taken).
268 let has_cublas = self.cublas.is_some();
269 if !has_cublas {
270 // 2. GEMM: Q/O projections (S, H, H)
271 warm!(format!("gemm_forward_{s}_{h}_{h}"), GemmKernel::naive(s, h, h));
272
273 // 3. GEMM: K/V projections (S, H, kv_hidden)
274 if kv_h != h {
275 warm!(format!("gemm_forward_{s}_{h}_{kv_h}"), GemmKernel::naive(s, kv_h, h));
276 }
277
278 // 4. GEMM: gate/up projections (S, H, I)
279 warm!(format!("gemm_forward_{s}_{h}_{i}"), GemmKernel::naive(s, i, h));
280
281 // 5. GEMM: down projection (S, I, H)
282 warm!(format!("gemm_forward_{s}_{i}_{h}"), GemmKernel::naive(s, h, i));
283 } else {
284 eprintln!("[CUDA] Skipping PTX pre-warm for 4 GEMM kernels (cuBLAS active — PMAT-700)");
285 }
286
287 // PMAT-698k + PMAT-698p: pre-warm batched_rope_fwd at BOTH seq_len=1
288 // (Phase 3 single-token smoke) AND APR_DISTILL_SMOKE_SEQ_LEN
289 // (default 256 — Phase 4 real-corpus seq). Runtime keys
290 // (normalization.rs:339):
291 // batched_rope_fwd_{num_heads}_{head_dim}_{seq_len}_th{theta_bits:08x}
292 // Stage C/D dispatch on gx10 confirmed runtime emits 2 [FWD-CACHE]
293 // Compiling events post-pre-warm for rope_fwd at seq=256 — avoidable
294 // JIT-cache pressure that PMAT-700-B closed for GEMMs.
295 use trueno_gpu::kernels::BatchedRopeNeoxKernel;
296 let qwen_theta = 1_000_000.0_f32;
297 let qwen_theta_bits = qwen_theta.to_bits();
298 let phase4_rope_seq: u32 = std::env::var("APR_DISTILL_SMOKE_SEQ_LEN")
299 .ok()
300 .and_then(|v| v.parse().ok())
301 .unwrap_or(256);
302 let nkv = _nkv;
303 for rope_seq in [1_u32, phase4_rope_seq] {
304 warm!(
305 format!("batched_rope_neox_fwd_{nh}_{hd}_{rope_seq}_th{qwen_theta_bits:08x}"),
306 BatchedRopeNeoxKernel::new(nh, hd, rope_seq, qwen_theta)
307 );
308 if nkv != nh {
309 warm!(
310 format!("batched_rope_neox_fwd_{nkv}_{hd}_{rope_seq}_th{qwen_theta_bits:08x}"),
311 BatchedRopeNeoxKernel::new(nkv, hd, rope_seq, qwen_theta)
312 );
313 }
314 }
315
316 // 6. Fused SwiGLU
317 warm!("fused_swiglu_forward".to_string(), FusedSwigluKernel::new(si));
318
319 // 7. Residual add (seq * hidden)
320 warm!("residual_add_forward".to_string(), ResidualAddKernel::new(sh));
321
322 // 8. Interleaved-to-batched (dimension-independent: one module handles all dims)
323 warm!("interleaved_to_batched".to_string(), InterleavedToBatchedKernel::new(s, nh, hd));
324
325 // 9. Batched transpose (dimension-independent: one module handles all dims)
326 warm!("batched_transpose".to_string(), BatchedTransposeKernel::new(nh, s, hd));
327
328 // 10. Batched 4D GEMM: Q@K^T (1, NH, S, S, HD)
329 warm!(
330 format!("batched_4d_gemm_1_{nh}_{s}_{s}_{hd}"),
331 Batched4DGemmKernel::new(1, nh, s, s, hd)
332 );
333
334 // 11. Scale: attention scores (NH * S * S)
335 let score_n = nh * s * s;
336 warm!("scale_forward".to_string(), ScaleKernel::new(score_n));
337
338 // 12. Batched softmax (dimension-independent: one module handles all dims)
339 let softmax_rows = nh * s;
340 warm!("batched_softmax_forward".to_string(), BatchedSoftmaxKernel::new(softmax_rows, s));
341
342 // 13. Batched 4D GEMM: attn@V (1, NH, S, HD, S)
343 warm!(
344 format!("batched_4d_gemm_1_{nh}_{s}_{hd}_{s}"),
345 Batched4DGemmKernel::new(1, nh, s, hd, s)
346 );
347
348 // 13b. Batched 4D GEMM: attention backward grad_V^T (1, NH, HD, S, S)
349 warm!(
350 format!("batched_4d_gemm_1_{nh}_{hd}_{s}_{s}"),
351 Batched4DGemmKernel::new(1, nh, hd, s, s)
352 );
353
354 // 14. Batched-to-interleaved (dimension-independent: one module handles all dims)
355 warm!("batched_to_interleaved".to_string(), BatchedToInterleavedKernel::new(s, nh, hd));
356
357 // 15. Element-wise multiply (used in FFN backward for SwiGLU gate * up)
358 warm!("elementwise_mul_forward".to_string(), ElementwiseMulKernel::new(si));
359
360 // 16. SiLU forward activation (standalone, used in LoRA FFN path)
361 warm!("silu_forward".to_string(), SiluKernel::new(si));
362
363 // 17-20. NF4 quantized GEMM variants (trueno#108: QLoRA support)
364 // Same 4 GEMM shapes but with Nf4GemmKernel instead of GemmKernel.
365 // Only compiled if K is divisible by 64 (NF4 block size).
366 if h.is_multiple_of(64) {
367 // NF4 cache keys exclude M (seq_len) — PTX is shape-independent
368 // (m/n/k are runtime params). Including M causes cache misses when
369 // actual seq_len != max_seq_len, triggering on-demand JIT that fails
370 // after GPU memory is loaded (trueno#184).
371 //
372 // Attention projections use q_dim (= num_heads * head_dim) which may
373 // differ from hidden_size (e.g. Qwen3-4B: h=2560, q_dim=4096).
374 // Q proj: input[S,h] @ W_q[h, q_dim] — key {h}_{q_dim}
375 warm!(format!("nf4_gemm_forward_{h}_{q_dim}"), Nf4GemmKernel::new(s, q_dim, h));
376 // O proj: input[S,q_dim] @ W_o[q_dim, h] — key {q_dim}_{h}
377 if q_dim != h {
378 warm!(format!("nf4_gemm_forward_{q_dim}_{h}"), Nf4GemmKernel::new(s, h, q_dim));
379 }
380 if kv_h != h && kv_h != q_dim && kv_h.is_multiple_of(64) {
381 warm!(format!("nf4_gemm_forward_{h}_{kv_h}"), Nf4GemmKernel::new(s, kv_h, h));
382 }
383 if i.is_multiple_of(64) {
384 warm!(format!("nf4_gemm_forward_{h}_{i}"), Nf4GemmKernel::new(s, i, h));
385 warm!(format!("nf4_gemm_forward_{i}_{h}"), Nf4GemmKernel::new(s, h, i));
386 }
387 }
388
389 // PMAT-475: Fused NF4 Gate+Up GEMM for FFN (shared input load).
390 if h.is_multiple_of(64) && i.is_multiple_of(64) {
391 use trueno_gpu::kernels::FusedNf4GateUpGemmKernel;
392 warm!(format!("fused_nf4_gate_up_{h}_{i}"), FusedNf4GateUpGemmKernel::new(s, i, h));
393 }
394 // PMAT-478: Fused K+V GEMM for GQA attention (reuses Gate+Up kernel).
395 if h.is_multiple_of(64) && kv_h.is_multiple_of(64) && kv_h != i {
396 use trueno_gpu::kernels::FusedNf4GateUpGemmKernel;
397 warm!(
398 format!("fused_nf4_gate_up_{h}_{kv_h}"),
399 FusedNf4GateUpGemmKernel::new(s, kv_h, h)
400 );
401 }
402
403 // 19-22. NF4 transposed GEMM for QLoRA backward (ENT-153).
404 // C[M×K] = A[M×N] @ B[K×N]^T — gradient propagation through frozen NF4 layers.
405 if h.is_multiple_of(64) {
406 // Q proj backward: grad[S,q_dim] @ W_q[h, q_dim]^T → [S,h]
407 warm!(
408 format!("nf4_gemm_transpose_{q_dim}_{h}"),
409 Nf4GemmTransposeKernel::new(s, q_dim, h)
410 );
411 // O proj backward: grad[S,h] @ W_o[q_dim, h]^T → [S,q_dim]
412 if q_dim != h {
413 warm!(
414 format!("nf4_gemm_transpose_{h}_{q_dim}"),
415 Nf4GemmTransposeKernel::new(s, h, q_dim)
416 );
417 }
418 if kv_h != h && kv_h != q_dim && kv_h.is_multiple_of(64) {
419 // K/V proj backward: grad[S,kv_h] @ W_k[h, kv_h]^T → [S,h]
420 warm!(
421 format!("nf4_gemm_transpose_{kv_h}_{h}"),
422 Nf4GemmTransposeKernel::new(s, kv_h, h)
423 );
424 }
425 if i.is_multiple_of(64) {
426 // Gate/Up backward: grad[S,I] @ W_gate[h,I]^T → [S,h]
427 warm!(format!("nf4_gemm_transpose_{i}_{h}"), Nf4GemmTransposeKernel::new(s, i, h));
428 // Down backward: grad[S,h] @ W_down[I,h]^T → [S,I]
429 warm!(format!("nf4_gemm_transpose_{h}_{i}"), Nf4GemmTransposeKernel::new(s, h, i));
430 }
431 }
432
433 eprintln!("[CUDA] Pre-warmed {count} forward kernels (JIT compiled before block upload)");
434 Ok(())
435 }
436
437 /// Pre-warm LoRA backward GEMM kernels for QLoRA training (ENT-153).
438 ///
439 /// The LoRA backward uses regular fp32 GEMMs for:
440 /// - Forward LoRA: x @ A → [S, R], inter @ B → [S, proj_dim]
441 /// - Backward A: x^T @ grad_inter → grad_A [H, R]
442 /// - Backward B: inter^T @ grad_proj → grad_B [R, proj_dim]
443 /// - Backward input: grad_proj @ B^T → [S, R], then [S, R] @ A^T → [S, H]
444 ///
445 /// These shapes are small (rank << hidden_size) but must still be JIT-compiled.
446 pub(super) fn pre_warm_lora_backward(
447 &mut self,
448 hidden_size: usize,
449 q_dim: usize,
450 kv_hidden_size: usize,
451 max_seq_len: usize,
452 lora_rank: usize,
453 ) -> Result<()> {
454 if lora_rank == 0 {
455 return Ok(());
456 }
457
458 let s = max_seq_len as u32;
459 let h = hidden_size as u32;
460 let r = lora_rank as u32;
461 let qd = q_dim as u32;
462 let kv = kv_hidden_size as u32;
463
464 let mut count = 0u32;
465 let target = self.sm_target.clone();
466
467 macro_rules! warm {
468 ($key:expr, $kernel:expr) => {{
469 let ptx = $kernel.emit_ptx_for_target(&target);
470 self.get_or_compile(&$key, &ptx)?;
471 count += 1;
472 }};
473 }
474
475 // LoRA forward GEMMs (also needed in backward for activation checkpointing)
476 // x[S,H] @ A[H,R] → [S,R]
477 warm!(format!("gemm_forward_{s}_{h}_{r}"), GemmKernel::naive(s, r, h));
478 // inter[S,R] @ B[R,qd] → [S,qd]
479 warm!(format!("gemm_forward_{s}_{r}_{qd}"), GemmKernel::naive(s, qd, r));
480 // inter[S,R] @ B[R,kv] → [S,kv]
481 if kv != qd {
482 warm!(format!("gemm_forward_{s}_{r}_{kv}"), GemmKernel::naive(s, kv, r));
483 }
484
485 // LoRA backward GEMMs (gemm_backward_a and gemm_backward_b use regular GEMM shapes)
486 // grad_B = inter^T[R,S] @ grad_proj[S,qd] → [R,qd]
487 // This is a GEMM with M=R, N=qd, K=S
488 warm!(format!("gemm_forward_{r}_{s}_{qd}"), GemmKernel::naive(r, qd, s));
489 if kv != qd {
490 warm!(format!("gemm_forward_{r}_{s}_{kv}"), GemmKernel::naive(r, kv, s));
491 }
492
493 // grad_li = grad_proj[S,qd] @ B^T[qd,R] → [S,R]
494 // This is effectively GEMM with M=S, N=R, K=qd
495 warm!(format!("gemm_forward_{s}_{qd}_{r}"), GemmKernel::naive(s, r, qd));
496 if kv != qd {
497 warm!(format!("gemm_forward_{s}_{kv}_{r}"), GemmKernel::naive(s, r, kv));
498 }
499
500 // grad_A = x^T[H,S] @ grad_li[S,R] → [H,R]
501 warm!(format!("gemm_forward_{h}_{s}_{r}"), GemmKernel::naive(h, r, s));
502
503 // grad_input += grad_li[S,R] @ A^T[R,H] → [S,H]
504 warm!(format!("gemm_forward_{s}_{r}_{h}"), GemmKernel::naive(s, h, r));
505
506 eprintln!("[CUDA] Pre-warmed {count} LoRA backward kernels");
507 Ok(())
508 }
509}
510
511/// Initialize forward kernel cache with CUDA context
512#[cfg(feature = "cuda")]
513pub fn init_forward_kernel_cache(ctx: std::sync::Arc<CudaContext>) -> Result<()> {
514 FORWARD_KERNEL_CACHE.get_or_init(|| Mutex::new(ForwardKernelCache::new(ctx)));
515 Ok(())
516}
517/// Pre-allocate cuBLAS workspace for CUDA graph capture (PMAT-063).
518#[cfg(feature = "cuda")]
519pub fn set_cublas_workspace(ptr: u64, size: usize) -> Result<()> {
520 let c = FORWARD_KERNEL_CACHE.get().ok_or(CudaTensorError::DeviceNotInitialized)?;
521 let c = c.lock().map_err(|_| CudaTensorError::KernelError("lock".into()))?;
522 if let Some(h) = c.cublas() {
523 h.set_workspace(ptr, size).map_err(|e| CudaTensorError::KernelError(format!("{e}")))?;
524 }
525 Ok(())
526}
527/// Bind cuBLAS handle to a stream (ALB-075).
528#[cfg(feature = "cuda")]
529pub fn set_forward_cublas_stream(stream: &CudaStream) -> Result<()> {
530 let cache = FORWARD_KERNEL_CACHE.get().ok_or(CudaTensorError::DeviceNotInitialized)?;
531 let cache = cache.lock().map_err(|_err| {
532 CudaTensorError::KernelError("Failed to acquire kernel cache lock".to_string())
533 })?;
534 cache.set_cublas_stream(stream)
535}
536
537/// Pre-warm forward kernels (C-PREWARM-001: JIT before block upload).
538#[cfg(feature = "cuda")]
539pub fn pre_warm_forward_kernels(
540 hidden_size: usize,
541 intermediate_size: usize,
542 num_heads: usize,
543 num_kv_heads: usize,
544 head_dim: usize,
545 max_seq_len: usize,
546) -> Result<()> {
547 // trueno#200: Pre-warm backward kernels too (Blackwell JIT crash workaround)
548 pre_warm_backward_kernels_in_forward_cache(num_heads, num_kv_heads, head_dim, max_seq_len)?;
549 let cache = FORWARD_KERNEL_CACHE.get().ok_or(CudaTensorError::DeviceNotInitialized)?;
550 let mut cache = cache.lock().map_err(|_err| {
551 CudaTensorError::KernelError("Failed to acquire kernel cache lock".to_string())
552 })?;
553 cache.pre_warm_for_model(
554 hidden_size,
555 intermediate_size,
556 num_heads,
557 num_kv_heads,
558 head_dim,
559 max_seq_len,
560 )
561}
562
563/// Pre-warm backward kernels in forward cache (trueno#200 Blackwell).
564///
565/// CONTRACT: All backward kernels must be compiled before GPU work starts.
566/// On Blackwell (sm_121), cuModuleLoadData fails during active GPU computation.
567#[cfg(feature = "cuda")]
568fn pre_warm_backward_kernels_in_forward_cache(
569 num_heads: usize,
570 _num_kv_heads: usize,
571 head_dim: usize,
572 max_seq_len: usize,
573) -> Result<()> {
574 let cache = FORWARD_KERNEL_CACHE.get().ok_or(CudaTensorError::DeviceNotInitialized)?;
575 let mut cache = cache.lock().map_err(|_err| {
576 CudaTensorError::KernelError("Failed to acquire kernel cache lock".to_string())
577 })?;
578
579 let target = cache.sm_target.clone();
580 let _nh = num_heads as u32;
581 let _hd = head_dim as u32;
582 let _s = max_seq_len as u32;
583
584 macro_rules! warm {
585 ($key:expr, $kernel:expr) => {{
586 let ptx = $kernel.emit_ptx_for_target(&target);
587 cache.get_or_compile(&$key, &ptx)?;
588 }};
589 }
590
591 // Batched RoPE backward — missing from pre_warm_for_model, causes
592 // CUDA context poisoning on Blackwell when compiled during backward pass.
593 // Need BOTH num_heads AND num_kv_heads variants (GQA uses different head count for K/V).
594 //
595 // FALSIFY-CUDA-ROPE-THETA-CACHE-KEY-001: cache key now includes theta_bits
596 // (matching runtime in `batched_rope_neox_backward`). The hardcoded
597 // 1_000_000.0 here matches Qwen2 / Qwen2.5 default; for Llama
598 // pretrain (theta=10000) the runtime call will compile its own
599 // module on first use, no longer silently shadowing the Qwen warm.
600 let nh = num_heads as u32;
601 let nkv = _num_kv_heads as u32;
602 let hd = head_dim as u32;
603 let s = max_seq_len as u32;
604 let qwen_theta_bits = 1_000_000.0_f32.to_bits();
605 warm!(
606 format!("batched_rope_neox_bwd_{nh}_{hd}_{s}_th{qwen_theta_bits:08x}"),
607 BatchedRopeNeoxBackwardKernel::new(nh, hd, s, 1_000_000.0)
608 );
609 if nkv != nh {
610 warm!(
611 format!("batched_rope_neox_bwd_{nkv}_{hd}_{s}_th{qwen_theta_bits:08x}"),
612 BatchedRopeNeoxBackwardKernel::new(nkv, hd, s, 1_000_000.0)
613 );
614 }
615
616 eprintln!(" ✓ Backward rope kernel pre-warmed in forward cache");
617 Ok(())
618}
619
620/// Pre-warm LoRA backward GEMM kernels for QLoRA training (ENT-153).
621///
622/// Must be called BEFORE uploading transformer blocks. Compiles the
623/// small-matrix GEMMs needed for LoRA gradient computation.
624#[cfg(feature = "cuda")]
625pub fn pre_warm_lora_backward_kernels(
626 hidden_size: usize,
627 q_dim: usize,
628 kv_hidden_size: usize,
629 max_seq_len: usize,
630 lora_rank: usize,
631) -> Result<()> {
632 let cache = FORWARD_KERNEL_CACHE.get().ok_or(CudaTensorError::DeviceNotInitialized)?;
633 let mut cache = cache.lock().map_err(|_err| {
634 CudaTensorError::KernelError("Failed to acquire kernel cache lock".to_string())
635 })?;
636 cache.pre_warm_lora_backward(hidden_size, q_dim, kv_hidden_size, max_seq_len, lora_rank)
637}