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::{
10 BatchedFusedResidualRmsNormKernel, BatchedRopeNeoxBackwardKernel, BatchedRopeNeoxKernel,
11 BatchedVectorizedRmsNormKernel, Kernel, LayerNormKernel, PerHeadRmsNormKernel, RopeNeoxKernel,
12};
13
14use crate::autograd::cuda_tensor::{CudaTensorError, Result};
15
16#[cfg(feature = "cuda")]
17use super::cache::FORWARD_KERNEL_CACHE;
18
19#[cfg(feature = "cuda")]
23pub fn layer_norm_forward(
24 input: &GpuBuffer<f32>,
25 gamma: &GpuBuffer<f32>,
26 beta: &GpuBuffer<f32>,
27 output: &mut GpuBuffer<f32>,
28 batch_size: u32,
29 hidden_size: u32,
30 stream: &CudaStream,
31) -> Result<()> {
32 let cache = FORWARD_KERNEL_CACHE.get().ok_or(CudaTensorError::DeviceNotInitialized)?;
33 let mut cache = cache.lock().map_err(|_err| {
34 CudaTensorError::KernelError("Failed to acquire kernel cache lock".to_string())
35 })?;
36
37 let kernel = LayerNormKernel::new(hidden_size);
38 let kernel_name = kernel.name();
39
40 let key = format!("layer_norm_forward_{hidden_size}");
41 let module = match cache.get_cached(&key) {
42 Some(m) => m,
43 None => {
44 let ptx = kernel.emit_ptx_for_target(cache.sm_target());
45 cache.get_or_compile(&key, &ptx)?
46 }
47 };
48
49 let config = LaunchConfig {
50 grid: (batch_size, 1, 1),
51 block: (256.min(hidden_size), 1, 1),
52 shared_mem: 0,
53 };
54
55 let input_ptr = input.as_ptr();
56 let gamma_ptr = gamma.as_ptr();
57 let beta_ptr = beta.as_ptr();
58 let output_ptr = output.as_ptr();
59
60 let mut args: [*mut std::ffi::c_void; 6] = [
61 &input_ptr as *const _ as *mut _,
62 &gamma_ptr as *const _ as *mut _,
63 &beta_ptr as *const _ as *mut _,
64 &output_ptr as *const _ as *mut _,
65 &batch_size as *const _ as *mut _,
66 &hidden_size as *const _ as *mut _,
67 ];
68
69 unsafe {
72 stream.launch_kernel(module, kernel_name, &config, &mut args).map_err(|e| {
73 CudaTensorError::KernelError(format!("LayerNorm forward launch failed: {e:?}"))
74 })?;
75 }
76
77 Ok(())
78}
79
80#[cfg(feature = "cuda")]
91pub fn rms_norm_forward(
92 input: &GpuBuffer<f32>,
93 gamma: &GpuBuffer<f32>,
94 output: &mut GpuBuffer<f32>,
95 batch_size: u32,
96 hidden_size: u32,
97 stream: &CudaStream,
98) -> Result<()> {
99 rms_norm_forward_with_eps(input, gamma, output, batch_size, hidden_size, 1e-5, stream)
104}
105
106#[cfg(feature = "cuda")]
120pub fn rms_norm_forward_with_eps(
121 input: &GpuBuffer<f32>,
122 gamma: &GpuBuffer<f32>,
123 output: &mut GpuBuffer<f32>,
124 batch_size: u32,
125 hidden_size: u32,
126 eps: f32,
127 stream: &CudaStream,
128) -> Result<()> {
129 let cache = FORWARD_KERNEL_CACHE.get().ok_or(CudaTensorError::DeviceNotInitialized)?;
130 let mut cache = cache.lock().map_err(|_err| {
131 CudaTensorError::KernelError("Failed to acquire kernel cache lock".to_string())
132 })?;
133
134 let kernel = BatchedVectorizedRmsNormKernel::new(hidden_size, batch_size).with_epsilon(eps);
135
136 let eps_bits = eps.to_bits();
139 let key = format!("batched_rmsnorm_fwd_{hidden_size}_eps{eps_bits:08x}");
140 let module = match cache.get_cached(&key) {
141 Some(m) => m,
142 None => {
143 let ptx = kernel.emit_ptx_for_target(cache.sm_target());
144 cache.get_or_compile(&key, &ptx)?
145 }
146 };
147
148 let config = LaunchConfig {
151 grid: (1, batch_size, 1),
152 block: (256, 1, 1),
153 shared_mem: 8 * 4, };
155
156 let input_ptr = input.as_ptr();
157 let output_ptr = output.as_ptr();
158 let gamma_ptr = gamma.as_ptr();
159
160 let mut args: [*mut std::ffi::c_void; 3] = [
161 &input_ptr as *const _ as *mut _,
162 &output_ptr as *const _ as *mut _,
163 &gamma_ptr as *const _ as *mut _,
164 ];
165
166 unsafe {
170 stream.launch_kernel(module, "batched_rmsnorm_vectorized", &config, &mut args).map_err(
171 |e| CudaTensorError::KernelError(format!("RMSNorm forward launch failed: {e:?}")),
172 )?;
173 }
174
175 Ok(())
176}
177
178#[cfg(feature = "cuda")]
188pub fn per_head_rmsnorm_forward(
189 input: &GpuBuffer<f32>,
190 gamma: &GpuBuffer<f32>,
191 output: &mut GpuBuffer<f32>,
192 num_heads: u32,
193 head_dim: u32,
194 pos_offset: usize,
195 stream: &CudaStream,
196) -> Result<()> {
197 let cache = FORWARD_KERNEL_CACHE.get().ok_or(CudaTensorError::DeviceNotInitialized)?;
198 let mut cache = cache.lock().map_err(|_err| {
199 CudaTensorError::KernelError("Failed to acquire kernel cache lock".to_string())
200 })?;
201
202 let kernel = PerHeadRmsNormKernel::new(head_dim, num_heads);
203
204 let key = format!("per_head_rmsnorm_fwd_{head_dim}_{num_heads}");
205 let module = match cache.get_cached(&key) {
206 Some(m) => m,
207 None => {
208 let ptx = kernel.emit_ptx_for_target(cache.sm_target());
209 cache.get_or_compile(&key, &ptx)?
210 }
211 };
212
213 let config = LaunchConfig { grid: (num_heads, 1, 1), block: (32, 1, 1), shared_mem: 0 };
215
216 let stride = (num_heads * head_dim) as usize;
218 let input_offset = pos_offset * stride;
219 let output_offset = pos_offset * stride;
220
221 let input_ptr = input.as_ptr() + (input_offset * std::mem::size_of::<f32>()) as u64;
223 let output_ptr = output.as_ptr() + (output_offset * std::mem::size_of::<f32>()) as u64;
224 let gamma_ptr = gamma.as_ptr();
225
226 let mut args: [*mut std::ffi::c_void; 3] = [
227 &input_ptr as *const _ as *mut _,
228 &output_ptr as *const _ as *mut _,
229 &gamma_ptr as *const _ as *mut _,
230 ];
231
232 unsafe {
234 stream.launch_kernel(module, "per_head_rmsnorm", &config, &mut args).map_err(|e| {
235 CudaTensorError::KernelError(format!("PerHeadRmsNorm forward failed: {e:?}"))
236 })?;
237 }
238
239 Ok(())
240}
241
242#[cfg(feature = "cuda")]
251pub fn rope_neox_forward(
252 input: &GpuBuffer<f32>,
253 output: &mut GpuBuffer<f32>,
254 num_heads: u32,
255 head_dim: u32,
256 pos: u32,
257 pos_offset: usize,
258 theta: f32,
259 stream: &CudaStream,
260) -> Result<()> {
261 let cache = FORWARD_KERNEL_CACHE.get().ok_or(CudaTensorError::DeviceNotInitialized)?;
262 let mut cache = cache.lock().map_err(|_err| {
263 CudaTensorError::KernelError("Failed to acquire kernel cache lock".to_string())
264 })?;
265
266 let kernel = RopeNeoxKernel::new(num_heads, head_dim, theta);
267
268 let theta_bits = theta.to_bits();
276 let key = format!("rope_neox_fwd_{num_heads}_{head_dim}_th{theta_bits:08x}");
277 let module = match cache.get_cached(&key) {
278 Some(m) => m,
279 None => {
280 let ptx = kernel.emit_ptx_for_target(cache.sm_target());
281 cache.get_or_compile(&key, &ptx)?
282 }
283 };
284
285 let config =
287 LaunchConfig { grid: (num_heads, 1, 1), block: (head_dim / 2, 1, 1), shared_mem: 0 };
288
289 let stride = (num_heads * head_dim) as usize;
291 let byte_offset = pos_offset * stride * std::mem::size_of::<f32>();
292
293 let input_ptr = input.as_ptr() + byte_offset as u64;
295 let output_ptr = output.as_ptr() + byte_offset as u64;
296
297 let mut args: [*mut std::ffi::c_void; 3] = [
298 &input_ptr as *const _ as *mut _,
299 &output_ptr as *const _ as *mut _,
300 &pos as *const _ as *mut _,
301 ];
302
303 unsafe {
305 stream.launch_kernel(module, "rope_neox", &config, &mut args).map_err(|e| {
306 CudaTensorError::KernelError(format!("RoPE NeoX forward failed: {e:?}"))
307 })?;
308 }
309
310 Ok(())
311}
312
313#[cfg(feature = "cuda")]
320pub fn batched_rope_neox_forward(
321 input: &GpuBuffer<f32>,
322 output: &mut GpuBuffer<f32>,
323 positions: &GpuBuffer<u32>,
324 num_heads: u32,
325 head_dim: u32,
326 seq_len: u32,
327 theta: f32,
328 stream: &CudaStream,
329) -> Result<()> {
330 let cache = FORWARD_KERNEL_CACHE.get().ok_or(CudaTensorError::DeviceNotInitialized)?;
331 let mut cache = cache.lock().map_err(|_err| {
332 CudaTensorError::KernelError("Failed to acquire kernel cache lock".to_string())
333 })?;
334
335 let kernel = BatchedRopeNeoxKernel::new(num_heads, head_dim, seq_len, theta);
340
341 let theta_bits = theta.to_bits();
345 let key = format!("batched_rope_neox_fwd_{num_heads}_{head_dim}_{seq_len}_th{theta_bits:08x}");
346 let module = match cache.get_cached(&key) {
347 Some(m) => m,
348 None => {
349 let ptx = kernel.emit_ptx_for_target(cache.sm_target());
350 cache.get_or_compile(&key, &ptx)?
351 }
352 };
353
354 let config =
355 LaunchConfig { grid: (num_heads, seq_len, 1), block: (head_dim / 2, 1, 1), shared_mem: 0 };
356
357 let input_ptr = input.as_ptr();
358 let output_ptr = output.as_ptr();
359 let positions_ptr = positions.as_ptr();
360
361 let mut args: [*mut std::ffi::c_void; 3] = [
362 &input_ptr as *const _ as *mut _,
363 &output_ptr as *const _ as *mut _,
364 &positions_ptr as *const _ as *mut _,
365 ];
366
367 unsafe {
369 stream.launch_kernel(module, "batched_rope_neox", &config, &mut args).map_err(|e| {
370 CudaTensorError::KernelError(format!("Batched RoPE NeoX forward failed: {e:?}"))
371 })?;
372 }
373
374 Ok(())
375}
376
377#[cfg(feature = "cuda")]
383pub fn batched_rope_neox_backward(
384 grad_input: &GpuBuffer<f32>,
385 grad_output: &mut GpuBuffer<f32>,
386 positions: &GpuBuffer<u32>,
387 num_heads: u32,
388 head_dim: u32,
389 seq_len: u32,
390 theta: f32,
391 stream: &CudaStream,
392) -> Result<()> {
393 let cache = FORWARD_KERNEL_CACHE.get().ok_or(CudaTensorError::DeviceNotInitialized)?;
394 let mut cache = cache.lock().map_err(|_err| {
395 CudaTensorError::KernelError("Failed to acquire kernel cache lock".to_string())
396 })?;
397
398 let kernel = BatchedRopeNeoxBackwardKernel::new(num_heads, head_dim, seq_len, theta);
401
402 let theta_bits = theta.to_bits();
405 let key = format!("batched_rope_neox_bwd_{num_heads}_{head_dim}_{seq_len}_th{theta_bits:08x}");
406 let module = match cache.get_cached(&key) {
407 Some(m) => m,
408 None => {
409 let ptx = kernel.emit_ptx_for_target(cache.sm_target());
410 cache.get_or_compile(&key, &ptx)?
411 }
412 };
413
414 let config =
415 LaunchConfig { grid: (num_heads, seq_len, 1), block: (head_dim / 2, 1, 1), shared_mem: 0 };
416
417 let input_ptr = grad_input.as_ptr();
418 let output_ptr = grad_output.as_ptr();
419 let positions_ptr = positions.as_ptr();
420
421 let mut args: [*mut std::ffi::c_void; 3] = [
422 &input_ptr as *const _ as *mut _,
423 &output_ptr as *const _ as *mut _,
424 &positions_ptr as *const _ as *mut _,
425 ];
426
427 unsafe {
429 stream.launch_kernel(module, "batched_rope_neox_backward", &config, &mut args).map_err(
430 |e| CudaTensorError::KernelError(format!("Batched RoPE NeoX backward failed: {e:?}")),
431 )?;
432 }
433
434 Ok(())
435}
436
437#[cfg(feature = "cuda")]
454pub fn fused_residual_rmsnorm_forward(
455 residual: &GpuBuffer<f32>,
456 input: &GpuBuffer<f32>,
457 residual_out: &mut GpuBuffer<f32>,
458 output: &mut GpuBuffer<f32>,
459 gamma: &GpuBuffer<f32>,
460 batch_size: u32,
461 hidden_size: u32,
462 eps: f32,
463 stream: &CudaStream,
464) -> Result<()> {
465 let cache = FORWARD_KERNEL_CACHE.get().ok_or(CudaTensorError::DeviceNotInitialized)?;
488 let mut cache = cache.lock().map_err(|_err| {
489 CudaTensorError::KernelError("Failed to acquire kernel cache lock".to_string())
490 })?;
491
492 let eps_bits = eps.to_bits();
493 let key = format!("batched_fused_residual_rmsnorm_{hidden_size}_eps{eps_bits:08x}");
494 let module = match cache.get_cached(&key) {
495 Some(m) => m,
496 None => {
497 let kernel =
498 BatchedFusedResidualRmsNormKernel::new(hidden_size, batch_size).with_epsilon(eps);
499 let ptx = kernel.emit_ptx_for_target(cache.sm_target());
500 cache.get_or_compile(&key, &ptx)?
501 }
502 };
503
504 let config = LaunchConfig { grid: (1, batch_size, 1), block: (256, 1, 1), shared_mem: 8 * 4 };
507
508 let residual_ptr = residual.as_ptr();
509 let input_ptr = input.as_ptr();
510 let residual_out_ptr = residual_out.as_ptr();
511 let output_ptr = output.as_ptr();
512 let gamma_ptr = gamma.as_ptr();
513
514 let mut args: [*mut std::ffi::c_void; 5] = [
515 &residual_ptr as *const _ as *mut _,
516 &input_ptr as *const _ as *mut _,
517 &residual_out_ptr as *const _ as *mut _,
518 &output_ptr as *const _ as *mut _,
519 &gamma_ptr as *const _ as *mut _,
520 ];
521
522 unsafe {
529 stream
530 .launch_kernel(module, "batched_fused_residual_rmsnorm", &config, &mut args)
531 .map_err(|e| {
532 CudaTensorError::KernelError(format!(
533 "Fused residual+RMSNorm forward failed: {e:?}"
534 ))
535 })?;
536 }
537
538 Ok(())
539}
540
541#[cfg(all(test, feature = "cuda"))]
542mod tests {
543 use super::*;
544 use crate::autograd::cuda_forward::cache::init_forward_kernel_cache;
545 use crate::autograd::cuda_tensor::CudaDevice;
546 use trueno_gpu::driver::GpuBuffer;
547
548 fn cpu_rmsnorm_reference(input: &[f32], gamma: &[f32], eps: f32) -> Vec<f32> {
551 let n = input.len() as f32;
552 let mean_sq: f32 = input.iter().map(|v| v * v).sum::<f32>() / n;
553 let rms = (mean_sq + eps).sqrt();
554 input.iter().zip(gamma.iter()).map(|(&x, &g)| (x / rms) * g).collect()
555 }
556
557 #[test]
569 fn falsify_cuda_rmsnorm_eps_parity_qwen_1e_minus_6() {
570 let device = match CudaDevice::default_device() {
571 Ok(d) => d,
572 Err(e) => {
573 eprintln!("[falsify-cuda-rmsnorm-eps-parity-001] skipping (no CUDA host): {e}");
574 return;
575 }
576 };
577 let ctx = device.context().clone();
578 let stream = device.stream();
579 if let Err(e) = init_forward_kernel_cache(ctx.clone()) {
580 eprintln!("[falsify-cuda-rmsnorm-eps-parity-001] kernel cache init failed: {e}");
581 return;
582 }
583
584 let hidden_size = 896usize;
589 let batch_size = 4u32;
590 let total = batch_size as usize * hidden_size;
591 let input_data: Vec<f32> =
592 (0..total).map(|i| (((i as f32) * 0.013).sin()) * 0.02).collect();
593 let gamma_data: Vec<f32> =
594 (0..hidden_size).map(|i| 1.0 + ((i as f32) * 0.005).cos() * 0.1).collect();
595
596 let mut cpu_out = Vec::with_capacity(total);
598 for b in 0..batch_size as usize {
599 let row = &input_data[b * hidden_size..(b + 1) * hidden_size];
600 cpu_out.extend(cpu_rmsnorm_reference(row, &gamma_data, 1e-6));
601 }
602
603 let input_gpu = GpuBuffer::from_host(&ctx, &input_data).expect("input");
604 let gamma_gpu = GpuBuffer::from_host(&ctx, &gamma_data).expect("gamma");
605 let mut output_gpu = GpuBuffer::<f32>::new(&ctx, total).expect("output alloc");
606
607 rms_norm_forward_with_eps(
608 &input_gpu,
609 &gamma_gpu,
610 &mut output_gpu,
611 batch_size,
612 hidden_size as u32,
613 1e-6,
614 stream,
615 )
616 .expect("kernel launch");
617 stream.synchronize().expect("sync");
618
619 let mut gpu_out = vec![0.0f32; total];
620 output_gpu.copy_to_host(&mut gpu_out).expect("download");
621
622 let max_diff =
623 cpu_out.iter().zip(gpu_out.iter()).map(|(c, g)| (c - g).abs()).fold(0.0f32, f32::max);
624
625 eprintln!("[falsify-cuda-rmsnorm-eps-parity-001] max_diff={max_diff} (Qwen eps=1e-6)");
626 assert!(
627 max_diff < 1e-4,
628 "FALSIFY-CUDA-RMSNORM-EPS-PARITY-001: max_diff={max_diff} >= 1e-4. \
629 CUDA RMSNorm kernel disagrees with CPU reference at Qwen eps=1e-6. \
630 Pre-fix root cause: BatchedVectorizedRmsNormKernel::new hardcodes \
631 epsilon=1e-5 (Llama default) so calling `rms_norm_forward` for \
632 Qwen2 silently uses the wrong eps. Fix: \
633 `rms_norm_forward_with_eps(.., eps, ..)` threads `config.rms_norm_eps` \
634 into the kernel and the cache key includes eps bits to avoid stale \
635 PTX shadowing. See contract apr-pretrain-cuda-rmsnorm-eps-parity-v1.yaml."
636 );
637 }
638
639 #[test]
662 fn falsify_cuda_fused_rmsnorm_distinct_residual_out_no_deadlock() {
663 use std::sync::mpsc;
664 use std::time::Duration;
665
666 let hidden_size = 1536usize; let batch_size = 4u32;
668 let total = batch_size as usize * hidden_size;
669
670 let residual_data: Vec<f32> =
671 (0..total).map(|i| (((i as f32) * 0.017).sin()) * 0.02).collect();
672 let input_data: Vec<f32> =
673 (0..total).map(|i| (((i as f32) * 0.011).cos()) * 0.02).collect();
674 let gamma_data: Vec<f32> =
675 (0..hidden_size).map(|i| 1.0 + ((i as f32) * 0.007).cos() * 0.1).collect();
676
677 enum Outcome {
678 NoCuda(String),
679 Done { residual_out: Vec<f32>, output: Vec<f32> },
680 }
681
682 let (tx, rx) = mpsc::channel();
683 let res_clone = residual_data.clone();
684 let inp_clone = input_data.clone();
685 let gam_clone = gamma_data.clone();
686 std::thread::spawn(move || {
689 let device = match CudaDevice::default_device() {
690 Ok(d) => d,
691 Err(e) => {
692 let _ = tx.send(Outcome::NoCuda(format!("{e}")));
693 return;
694 }
695 };
696 let ctx = device.context().clone();
697 let stream = device.stream();
698 if let Err(e) = init_forward_kernel_cache(ctx.clone()) {
699 let _ = tx.send(Outcome::NoCuda(format!("cache init: {e}")));
700 return;
701 }
702
703 let residual_gpu = GpuBuffer::from_host(&ctx, &res_clone).expect("residual");
704 let input_gpu = GpuBuffer::from_host(&ctx, &inp_clone).expect("input");
705 let gamma_gpu = GpuBuffer::from_host(&ctx, &gam_clone).expect("gamma");
706 let mut residual_out_gpu =
708 GpuBuffer::<f32>::new(&ctx, res_clone.len()).expect("residual_out alloc");
709 let mut output_gpu =
710 GpuBuffer::<f32>::new(&ctx, res_clone.len()).expect("output alloc");
711
712 fused_residual_rmsnorm_forward(
713 &residual_gpu,
714 &input_gpu,
715 &mut residual_out_gpu,
716 &mut output_gpu,
717 &gamma_gpu,
718 batch_size,
719 hidden_size as u32,
720 1e-6, stream,
722 )
723 .expect("fused_residual_rmsnorm_forward");
724 stream.synchronize().expect("sync");
725
726 let mut residual_out = vec![0.0f32; res_clone.len()];
727 let mut output = vec![0.0f32; res_clone.len()];
728 residual_out_gpu.copy_to_host(&mut residual_out).expect("download residual_out");
729 output_gpu.copy_to_host(&mut output).expect("download output");
730 let _ = tx.send(Outcome::Done { residual_out, output });
731 });
732
733 let outcome = rx.recv_timeout(Duration::from_secs(120)).unwrap_or_else(|_| {
734 panic!(
735 "FALSIFY-CUDA-FUSED-RMSNORM-DEADLOCK-001: \
736 fused_residual_rmsnorm_forward did not complete within 120s with a \
737 distinct residual_out buffer. Pre-fix root cause: the function held \
738 the FORWARD_KERNEL_CACHE mutex guard while calling the public \
739 residual_add_forward, which re-locks the same non-reentrant mutex \
740 on the same thread (self-deadlock). Fix: enqueue the residual add \
741 BEFORE acquiring the cache lock."
742 )
743 });
744
745 let (residual_out, output) = match outcome {
746 Outcome::NoCuda(reason) => {
747 eprintln!(
748 "[falsify-cuda-fused-rmsnorm-deadlock-001] skipping (no CUDA host): {reason}"
749 );
750 return;
751 }
752 Outcome::Done { residual_out, output } => (residual_out, output),
753 };
754
755 let max_add_diff = residual_data
757 .iter()
758 .zip(input_data.iter())
759 .zip(residual_out.iter())
760 .map(|((r, i), out)| (r + i - out).abs())
761 .fold(0.0f32, f32::max);
762 assert!(
763 max_add_diff == 0.0,
764 "FALSIFY-CUDA-FUSED-RMSNORM-DEADLOCK-001: residual_out != residual + input \
765 (max_diff={max_add_diff})"
766 );
767
768 let summed: Vec<f32> =
774 residual_data.iter().zip(input_data.iter()).map(|(r, i)| r + i).collect();
775 let mut cpu_out = Vec::with_capacity(total);
776 for b in 0..batch_size as usize {
777 let row = &summed[b * hidden_size..(b + 1) * hidden_size];
778 cpu_out.extend(cpu_rmsnorm_reference(row, &gamma_data, 1e-6));
779 }
780 let max_norm_diff =
781 cpu_out.iter().zip(output.iter()).map(|(c, g)| (c - g).abs()).fold(0.0f32, f32::max);
782 assert!(
783 max_norm_diff < 1e-4,
784 "FALSIFY-CUDA-FUSED-RMSNORM-DEADLOCK-001: output disagrees with CPU \
785 RMSNorm(residual+input) reference (max_diff={max_norm_diff})"
786 );
787 }
788}