1use super::{random_vec, OpUnderTest, Output};
4
5pub struct RmsNormOp {
13 pub tokens: usize,
14 pub dim: usize,
15 pub eps: f32,
16}
17
18impl RmsNormOp {
19 pub fn validate(&self) -> Result<(), String> {
21 self.expected_output_len().map(|_| ())
22 }
23
24 pub fn expected_output_len(&self) -> Result<usize, String> {
28 if self.tokens == 0 || self.dim == 0 {
29 return Err("RMSNorm tokens and dim must be nonzero".into());
30 }
31 if !self.eps.is_finite() || self.eps <= 0.0 {
32 return Err("RMSNorm epsilon must be finite and positive".into());
33 }
34 let elements = self
35 .tokens
36 .checked_mul(self.dim)
37 .ok_or("RMSNorm tokens * dim overflows usize")?;
38 let bytes = elements
39 .checked_mul(std::mem::size_of::<f32>())
40 .ok_or("RMSNorm f32 buffer byte size overflows usize")?;
41 if bytes > isize::MAX as usize {
42 return Err("RMSNorm f32 buffer exceeds the host allocation address range".into());
43 }
44 let cuda_step = self.dim.min(1024).div_ceil(32) * 32;
49 let max_loop_step = cuda_step.max(32);
50 let final_increment = (self.dim - 1).checked_add(max_loop_step);
51 if elements > i32::MAX as usize
52 || final_increment.is_none_or(|index| index > i32::MAX as usize)
53 {
54 return Err("RMSNorm shape exceeds the kernels' signed 32-bit indexing range".into());
55 }
56 Ok(elements)
57 }
58
59 fn output_len(&self) -> usize {
60 self.expected_output_len().expect("invalid RMSNorm fixture")
61 }
62
63 fn build_input(&self, seed: u64) -> (Vec<f32>, Vec<f32>) {
65 let x = random_vec(self.output_len(), -2.0, 2.0, seed);
66 let w = random_vec(self.dim, 0.5, 1.5, seed.wrapping_add(1));
67 (x, w)
68 }
69}
70
71impl OpUnderTest for RmsNormOp {
72 fn name(&self) -> &str {
73 "rms_norm"
74 }
75
76 fn run_cpu(&self, seed: u64) -> Output {
77 use ferrum_kernels::backend::cpu::CpuBackend;
78 use ferrum_kernels::backend::Backend;
79
80 let (x, w) = self.build_input(seed);
81 let mut ctx = CpuBackend::new_context();
82 let x_buf = CpuBackend::from_slice(&x);
83 let w_buf = CpuBackend::from_slice(&w);
84 let mut out = CpuBackend::alloc(self.output_len());
85 CpuBackend::rms_norm(
86 &mut ctx,
87 &x_buf,
88 &w_buf,
89 self.eps,
90 &mut out,
91 self.tokens,
92 self.dim,
93 );
94 CpuBackend::sync(&mut ctx);
95 CpuBackend::to_vec(&out, self.output_len())
96 }
97
98 #[cfg(all(target_os = "macos", feature = "metal"))]
99 fn run_metal(&self, seed: u64) -> Output {
100 use ferrum_kernels::backend::metal::MetalBackend;
101 use ferrum_kernels::backend::Backend;
102
103 let (x, w) = self.build_input(seed);
104 let mut ctx = MetalBackend::new_context();
105 let x_buf = MetalBackend::from_slice(&x);
106 let w_buf = MetalBackend::from_slice(&w);
107 let mut out = MetalBackend::alloc(self.output_len());
108 MetalBackend::rms_norm(
109 &mut ctx,
110 &x_buf,
111 &w_buf,
112 self.eps,
113 &mut out,
114 self.tokens,
115 self.dim,
116 );
117 MetalBackend::sync_checked(&mut ctx)
118 .unwrap_or_else(|error| panic!("RMSNorm Metal completion failed: {error}"));
119 MetalBackend::to_vec(&out, self.output_len())
120 }
121
122 #[cfg(feature = "cuda")]
123 fn run_cuda(&self, seed: u64) -> Output {
124 use ferrum_kernels::backend::cuda::CudaBackend;
125 use ferrum_kernels::backend::Backend;
126
127 let (x, w) = self.build_input(seed);
128 let mut ctx = CudaBackend::new_context();
129 let x_buf = CudaBackend::from_slice(&x);
130 let w_buf = CudaBackend::from_slice(&w);
131 let mut out = CudaBackend::alloc(self.output_len());
132 CudaBackend::rms_norm(
133 &mut ctx,
134 &x_buf,
135 &w_buf,
136 self.eps,
137 &mut out,
138 self.tokens,
139 self.dim,
140 );
141 CudaBackend::sync(&mut ctx);
142 CudaBackend::to_vec(&out, self.output_len())
143 }
144}
145
146#[cfg(test)]
147mod tests {
148 use super::*;
149
150 fn operation(tokens: usize, dim: usize, eps: f32) -> RmsNormOp {
151 RmsNormOp { tokens, dim, eps }
152 }
153
154 #[test]
155 fn validates_result_shape_before_allocating_inputs() {
156 let op = operation(3, 33, 1e-6);
157 op.validate().unwrap();
158 assert_eq!(op.expected_output_len().unwrap(), 99);
159 let output = op.run_cpu(7);
160 assert_eq!(output.len(), op.expected_output_len().unwrap());
161 assert!(output.iter().all(|value| value.is_finite()));
162 }
163
164 #[test]
165 fn rejects_empty_shapes_and_nonpositive_or_nonfinite_epsilon() {
166 for (tokens, dim) in [(0, 32), (1, 0), (0, 0)] {
167 assert!(operation(tokens, dim, 1e-6).validate().is_err());
168 }
169 for eps in [0.0, -0.0, -1e-6, f32::NAN, f32::INFINITY, f32::NEG_INFINITY] {
170 assert!(operation(1, 32, eps).validate().is_err());
171 }
172 }
173
174 #[test]
175 fn rejects_shape_byte_and_kernel_index_overflow_without_allocating() {
176 for (tokens, dim) in [
177 (usize::MAX, 2),
178 (1, usize::MAX / std::mem::size_of::<f32>() + 1),
179 (1, i32::MAX as usize),
180 (2, i32::MAX as usize / 2 + 1),
181 ] {
182 assert!(operation(tokens, dim, 1e-6).expected_output_len().is_err());
183 }
184 }
185
186 #[test]
187 #[cfg(target_pointer_width = "64")]
188 fn validates_final_cuda_loop_increment_without_allocating() {
189 let last_safe_dim = i32::MAX as usize - (1024 - 1);
190 assert_eq!(
191 operation(1, last_safe_dim, 1e-6).expected_output_len(),
192 Ok(last_safe_dim)
193 );
194 for dim in [last_safe_dim + 1, i32::MAX as usize - 31] {
197 assert!(operation(1, dim, 1e-6).expected_output_len().is_err());
198 }
199 }
200
201 #[test]
202 fn direct_executor_use_rejects_invalid_fixture_before_allocation() {
203 let op = operation(usize::MAX, 2, 1e-6);
204 assert!(std::panic::catch_unwind(|| op.run_cpu(7)).is_err());
205 }
206}