/home/noah/src/trueno/src/backends/scalar.rs
Line | Count | Source |
1 | | //! Scalar (non-SIMD) backend implementation |
2 | | //! |
3 | | //! This is the portable baseline implementation that works on all platforms. |
4 | | //! It uses simple loops without any SIMD instructions. |
5 | | //! |
6 | | //! # Performance |
7 | | //! |
8 | | //! This backend provides correctness reference but no SIMD acceleration. |
9 | | //! Expected to be 8-32x slower than SIMD backends on operations with 1K+ elements. |
10 | | |
11 | | use super::VectorBackend; |
12 | | |
13 | | /// Scalar backend (portable, no SIMD) |
14 | | pub struct ScalarBackend; |
15 | | |
16 | | impl VectorBackend for ScalarBackend { |
17 | | // SAFETY: This function is safe because: |
18 | | // 1. All slice accesses are bounds-checked by Rust iterator/indexing |
19 | | // 2. No raw pointer arithmetic is performed |
20 | | // 3. Marked unsafe only to match VectorBackend trait interface |
21 | 0 | unsafe fn add(a: &[f32], b: &[f32], result: &mut [f32]) { |
22 | 0 | for i in 0..a.len() { |
23 | 0 | result[i] = a[i] + b[i]; |
24 | 0 | } |
25 | 0 | } |
26 | | |
27 | | // SAFETY: This function is safe because: |
28 | | // 1. All slice accesses are bounds-checked by Rust iterator/indexing |
29 | | // 2. No raw pointer arithmetic is performed |
30 | | // 3. Marked unsafe only to match VectorBackend trait interface |
31 | 0 | unsafe fn sub(a: &[f32], b: &[f32], result: &mut [f32]) { |
32 | 0 | for i in 0..a.len() { |
33 | 0 | result[i] = a[i] - b[i]; |
34 | 0 | } |
35 | 0 | } |
36 | | |
37 | | // SAFETY: This function is safe because: |
38 | | // 1. All slice accesses are bounds-checked by Rust iterator/indexing |
39 | | // 2. No raw pointer arithmetic is performed |
40 | | // 3. Marked unsafe only to match VectorBackend trait interface |
41 | 0 | unsafe fn mul(a: &[f32], b: &[f32], result: &mut [f32]) { |
42 | 0 | for i in 0..a.len() { |
43 | 0 | result[i] = a[i] * b[i]; |
44 | 0 | } |
45 | 0 | } |
46 | | |
47 | | // SAFETY: This function is safe because: |
48 | | // 1. All slice accesses are bounds-checked by Rust iterator/indexing |
49 | | // 2. No raw pointer arithmetic is performed |
50 | | // 3. Marked unsafe only to match VectorBackend trait interface |
51 | 0 | unsafe fn div(a: &[f32], b: &[f32], result: &mut [f32]) { |
52 | 0 | for i in 0..a.len() { |
53 | 0 | result[i] = a[i] / b[i]; |
54 | 0 | } |
55 | 0 | } |
56 | | |
57 | | // SAFETY: This function is safe because: |
58 | | // 1. All slice accesses are bounds-checked by Rust iterator/indexing |
59 | | // 2. No raw pointer arithmetic is performed |
60 | | // 3. Marked unsafe only to match VectorBackend trait interface |
61 | | // |
62 | | // OPTIMIZATION: 4× unrolling with mul_add for better ILP and auto-vectorization. |
63 | | // This follows the cuda-tile pattern for improved throughput (spec: cuda-tile-behavior.md). |
64 | | // Using f32::mul_add provides FMA semantics where available, improving accuracy. |
65 | | #[inline(always)] |
66 | 0 | unsafe fn dot(a: &[f32], b: &[f32]) -> f32 { |
67 | 0 | let len = a.len(); |
68 | 0 | let chunks = len / 4; |
69 | | |
70 | | // 4 independent accumulators for better ILP (cuda-tile inspired optimization) |
71 | 0 | let mut acc0 = 0.0f32; |
72 | 0 | let mut acc1 = 0.0f32; |
73 | 0 | let mut acc2 = 0.0f32; |
74 | 0 | let mut acc3 = 0.0f32; |
75 | | |
76 | | // Process 4 elements at a time with independent accumulation chains |
77 | 0 | for i in 0..chunks { |
78 | 0 | let base = i * 4; |
79 | 0 | acc0 = a[base].mul_add(b[base], acc0); |
80 | 0 | acc1 = a[base + 1].mul_add(b[base + 1], acc1); |
81 | 0 | acc2 = a[base + 2].mul_add(b[base + 2], acc2); |
82 | 0 | acc3 = a[base + 3].mul_add(b[base + 3], acc3); |
83 | 0 | } |
84 | | |
85 | | // Combine all 4 accumulators |
86 | 0 | let mut sum = (acc0 + acc1) + (acc2 + acc3); |
87 | | |
88 | | // Handle remainder |
89 | 0 | for i in (chunks * 4)..len { |
90 | 0 | sum = a[i].mul_add(b[i], sum); |
91 | 0 | } |
92 | | |
93 | 0 | sum |
94 | 0 | } |
95 | | |
96 | | // SAFETY: This function is safe because: |
97 | | // 1. All slice accesses are bounds-checked by Rust iterator |
98 | | // 2. No raw pointer arithmetic is performed |
99 | | // 3. Marked unsafe only to match VectorBackend trait interface |
100 | 0 | unsafe fn sum(a: &[f32]) -> f32 { |
101 | 0 | let mut total = 0.0; |
102 | 0 | for &val in a { |
103 | 0 | total += val; |
104 | 0 | } |
105 | 0 | total |
106 | 0 | } |
107 | | |
108 | | // SAFETY: This function is safe because: |
109 | | // 1. All slice accesses are bounds-checked by Rust slicing/iteration |
110 | | // 2. Caller must ensure slice is non-empty (a[0] access) |
111 | | // 3. Marked unsafe only to match VectorBackend trait interface |
112 | 0 | unsafe fn max(a: &[f32]) -> f32 { |
113 | 0 | let mut maximum = a[0]; |
114 | 0 | for &val in &a[1..] { |
115 | 0 | if val > maximum { |
116 | 0 | maximum = val; |
117 | 0 | } |
118 | | } |
119 | 0 | maximum |
120 | 0 | } |
121 | | |
122 | | // SAFETY: This function is safe because: |
123 | | // 1. All slice accesses are bounds-checked by Rust slicing/iteration |
124 | | // 2. Caller must ensure slice is non-empty (a[0] access) |
125 | | // 3. Marked unsafe only to match VectorBackend trait interface |
126 | 0 | unsafe fn min(a: &[f32]) -> f32 { |
127 | 0 | let mut minimum = a[0]; |
128 | 0 | for &val in &a[1..] { |
129 | 0 | if val < minimum { |
130 | 0 | minimum = val; |
131 | 0 | } |
132 | | } |
133 | 0 | minimum |
134 | 0 | } |
135 | | |
136 | | // SAFETY: This function is safe because: |
137 | | // 1. All slice accesses are bounds-checked by Rust iterator |
138 | | // 2. Caller must ensure slice is non-empty (a[0] access) |
139 | | // 3. Marked unsafe only to match VectorBackend trait interface |
140 | 0 | unsafe fn argmax(a: &[f32]) -> usize { |
141 | 0 | let mut max_value = a[0]; |
142 | 0 | let mut max_index = 0; |
143 | 0 | for (i, &val) in a.iter().enumerate() { |
144 | 0 | if val > max_value { |
145 | 0 | max_value = val; |
146 | 0 | max_index = i; |
147 | 0 | } |
148 | | } |
149 | 0 | max_index |
150 | 0 | } |
151 | | |
152 | | // SAFETY: This function is safe because: |
153 | | // 1. All slice accesses are bounds-checked by Rust iterator |
154 | | // 2. Caller must ensure slice is non-empty (a[0] access) |
155 | | // 3. Marked unsafe only to match VectorBackend trait interface |
156 | 0 | unsafe fn argmin(a: &[f32]) -> usize { |
157 | 0 | let mut min_value = a[0]; |
158 | 0 | let mut min_index = 0; |
159 | 0 | for (i, &val) in a.iter().enumerate() { |
160 | 0 | if val < min_value { |
161 | 0 | min_value = val; |
162 | 0 | min_index = i; |
163 | 0 | } |
164 | | } |
165 | 0 | min_index |
166 | 0 | } |
167 | | |
168 | | // SAFETY: This function is safe because: |
169 | | // 1. All slice accesses are bounds-checked by Rust iterator |
170 | | // 2. Kahan summation uses only safe floating-point arithmetic |
171 | | // 3. Marked unsafe only to match VectorBackend trait interface |
172 | 0 | unsafe fn sum_kahan(a: &[f32]) -> f32 { |
173 | 0 | let mut sum = 0.0; |
174 | 0 | let mut c = 0.0; // Compensation for lost low-order bits |
175 | | |
176 | 0 | for &value in a { |
177 | 0 | let y = value - c; // Subtract the compensation |
178 | 0 | let t = sum + y; // Add to sum |
179 | 0 | c = (t - sum) - y; // Update compensation |
180 | 0 | sum = t; // Update sum |
181 | 0 | } |
182 | | |
183 | 0 | sum |
184 | 0 | } |
185 | | |
186 | | // SAFETY: This function is safe because: |
187 | | // 1. All slice accesses are bounds-checked by Rust iterator |
188 | | // 2. Empty check prevents undefined behavior |
189 | | // 3. Marked unsafe only to match VectorBackend trait interface |
190 | 0 | unsafe fn norm_l2(a: &[f32]) -> f32 { |
191 | 0 | if a.is_empty() { |
192 | 0 | return 0.0; |
193 | 0 | } |
194 | | |
195 | 0 | let mut sum_of_squares = 0.0; |
196 | 0 | for &val in a { |
197 | 0 | sum_of_squares += val * val; |
198 | 0 | } |
199 | 0 | sum_of_squares.sqrt() |
200 | 0 | } |
201 | | |
202 | | // SAFETY: This function is safe because: |
203 | | // 1. All slice accesses are bounds-checked by Rust iterator |
204 | | // 2. Empty check prevents undefined behavior |
205 | | // 3. Marked unsafe only to match VectorBackend trait interface |
206 | 0 | unsafe fn norm_l1(a: &[f32]) -> f32 { |
207 | 0 | if a.is_empty() { |
208 | 0 | return 0.0; |
209 | 0 | } |
210 | | |
211 | 0 | let mut sum = 0.0; |
212 | 0 | for &val in a { |
213 | 0 | sum += val.abs(); |
214 | 0 | } |
215 | 0 | sum |
216 | 0 | } |
217 | | |
218 | | // SAFETY: This function is safe because: |
219 | | // 1. All slice accesses are bounds-checked by Rust iterator |
220 | | // 2. Empty check prevents undefined behavior |
221 | | // 3. Marked unsafe only to match VectorBackend trait interface |
222 | 0 | unsafe fn norm_linf(a: &[f32]) -> f32 { |
223 | 0 | if a.is_empty() { |
224 | 0 | return 0.0; |
225 | 0 | } |
226 | | |
227 | 0 | let mut max_val = 0.0_f32; |
228 | 0 | for &val in a { |
229 | 0 | let abs_val = val.abs(); |
230 | 0 | if abs_val > max_val { |
231 | 0 | max_val = abs_val; |
232 | 0 | } |
233 | | } |
234 | 0 | max_val |
235 | 0 | } |
236 | | |
237 | | // SAFETY: This function is safe because: |
238 | | // 1. All slice accesses are bounds-checked by Rust iterator/enumerate |
239 | | // 2. No raw pointer arithmetic is performed |
240 | | // 3. Marked unsafe only to match VectorBackend trait interface |
241 | 0 | unsafe fn scale(a: &[f32], scalar: f32, result: &mut [f32]) { |
242 | 0 | for (i, &val) in a.iter().enumerate() { |
243 | 0 | result[i] = val * scalar; |
244 | 0 | } |
245 | 0 | } |
246 | | |
247 | | // SAFETY: This function is safe because: |
248 | | // 1. All slice accesses are bounds-checked by Rust iterator/enumerate |
249 | | // 2. No raw pointer arithmetic is performed |
250 | | // 3. Marked unsafe only to match VectorBackend trait interface |
251 | 0 | unsafe fn abs(a: &[f32], result: &mut [f32]) { |
252 | 0 | for (i, &val) in a.iter().enumerate() { |
253 | 0 | result[i] = val.abs(); |
254 | 0 | } |
255 | 0 | } |
256 | | |
257 | | // SAFETY: This function is safe because: |
258 | | // 1. All slice accesses are bounds-checked by Rust iterator/enumerate |
259 | | // 2. No raw pointer arithmetic is performed |
260 | | // 3. Marked unsafe only to match VectorBackend trait interface |
261 | 0 | unsafe fn clamp(a: &[f32], min_val: f32, max_val: f32, result: &mut [f32]) { |
262 | 0 | for (i, &val) in a.iter().enumerate() { |
263 | 0 | result[i] = val.max(min_val).min(max_val); |
264 | 0 | } |
265 | 0 | } |
266 | | |
267 | | // SAFETY: This function is safe because: |
268 | | // 1. All slice accesses are bounds-checked by Rust iterator/enumerate/zip |
269 | | // 2. No raw pointer arithmetic is performed |
270 | | // 3. Marked unsafe only to match VectorBackend trait interface |
271 | 0 | unsafe fn lerp(a: &[f32], b: &[f32], t: f32, result: &mut [f32]) { |
272 | 0 | for (i, (&a_val, &b_val)) in a.iter().zip(b.iter()).enumerate() { |
273 | 0 | // result = a + t * (b - a) |
274 | 0 | result[i] = a_val + t * (b_val - a_val); |
275 | 0 | } |
276 | 0 | } |
277 | | |
278 | | // SAFETY: This function is safe because: |
279 | | // 1. All slice accesses are bounds-checked by Rust iterator/enumerate/zip |
280 | | // 2. No raw pointer arithmetic is performed |
281 | | // 3. Marked unsafe only to match VectorBackend trait interface |
282 | 0 | unsafe fn fma(a: &[f32], b: &[f32], c: &[f32], result: &mut [f32]) { |
283 | 0 | for (i, ((&a_val, &b_val), &c_val)) in a.iter().zip(b.iter()).zip(c.iter()).enumerate() { |
284 | 0 | // result = a * b + c |
285 | 0 | result[i] = a_val * b_val + c_val; |
286 | 0 | } |
287 | 0 | } |
288 | | |
289 | | // SAFETY: This function is safe because: |
290 | | // 1. All slice accesses are bounds-checked by Rust iterator/enumerate |
291 | | // 2. No raw pointer arithmetic is performed |
292 | | // 3. Marked unsafe only to match VectorBackend trait interface |
293 | 0 | unsafe fn relu(a: &[f32], result: &mut [f32]) { |
294 | 0 | for (i, &val) in a.iter().enumerate() { |
295 | 0 | result[i] = if val > 0.0 { val } else { 0.0 }; |
296 | | } |
297 | 0 | } |
298 | | |
299 | | // SAFETY: This function is safe because: |
300 | | // 1. All slice accesses are bounds-checked by Rust iterator/enumerate |
301 | | // 2. No raw pointer arithmetic is performed |
302 | | // 3. Marked unsafe only to match VectorBackend trait interface |
303 | 0 | unsafe fn exp(a: &[f32], result: &mut [f32]) { |
304 | 0 | for (i, &val) in a.iter().enumerate() { |
305 | 0 | result[i] = val.exp(); |
306 | 0 | } |
307 | 0 | } |
308 | | |
309 | | // SAFETY: This function is safe because: |
310 | | // 1. All slice accesses are bounds-checked by Rust iterator/enumerate |
311 | | // 2. Clamping prevents exp() overflow |
312 | | // 3. Marked unsafe only to match VectorBackend trait interface |
313 | 0 | unsafe fn sigmoid(a: &[f32], result: &mut [f32]) { |
314 | 0 | for (i, &val) in a.iter().enumerate() { |
315 | | // Handle extreme values for numerical stability |
316 | 0 | result[i] = if val < -50.0 { |
317 | 0 | 0.0 // exp(-x) would overflow, but sigmoid approaches 0 |
318 | 0 | } else if val > 50.0 { |
319 | 0 | 1.0 // exp(-x) underflows to 0, sigmoid approaches 1 |
320 | | } else { |
321 | 0 | 1.0 / (1.0 + (-val).exp()) |
322 | | }; |
323 | | } |
324 | 0 | } |
325 | | |
326 | | // SAFETY: This function is safe because: |
327 | | // 1. All slice accesses are bounds-checked by Rust iterator/enumerate |
328 | | // 2. No raw pointer arithmetic is performed |
329 | | // 3. Marked unsafe only to match VectorBackend trait interface |
330 | 0 | unsafe fn gelu(a: &[f32], result: &mut [f32]) { |
331 | | // GELU approximation: 0.5 * x * (1 + tanh(sqrt(2/π) * (x + 0.044715 * x³))) |
332 | | const SQRT_2_OVER_PI: f32 = 0.797_884_6; |
333 | | const COEFF: f32 = 0.044715; |
334 | | |
335 | 0 | for (i, &x) in a.iter().enumerate() { |
336 | 0 | let x3 = x * x * x; |
337 | 0 | let inner = SQRT_2_OVER_PI * (x + COEFF * x3); |
338 | 0 | result[i] = 0.5 * x * (1.0 + inner.tanh()); |
339 | 0 | } |
340 | 0 | } |
341 | | |
342 | | // SAFETY: This function is safe because: |
343 | | // 1. All slice accesses are bounds-checked by Rust iterator/enumerate |
344 | | // 2. Clamping prevents exp() overflow |
345 | | // 3. Marked unsafe only to match VectorBackend trait interface |
346 | 0 | unsafe fn swish(a: &[f32], result: &mut [f32]) { |
347 | | // Swish: x * sigmoid(x) = x / (1 + exp(-x)) |
348 | 0 | for (i, &x) in a.iter().enumerate() { |
349 | 0 | if x < -50.0 { |
350 | 0 | result[i] = 0.0; // x * 0 = 0 |
351 | 0 | } else if x > 50.0 { |
352 | 0 | result[i] = x; // x * 1 = x |
353 | 0 | } else { |
354 | 0 | let sigmoid = 1.0 / (1.0 + (-x).exp()); |
355 | 0 | result[i] = x * sigmoid; |
356 | 0 | } |
357 | | } |
358 | 0 | } |
359 | | |
360 | | // SAFETY: This function is safe because: |
361 | | // 1. All slice accesses are bounds-checked by Rust iterator/enumerate |
362 | | // 2. No raw pointer arithmetic is performed |
363 | | // 3. Marked unsafe only to match VectorBackend trait interface |
364 | 0 | unsafe fn tanh(a: &[f32], result: &mut [f32]) { |
365 | | // tanh(x) = (exp(2x) - 1) / (exp(2x) + 1) |
366 | 0 | for (i, &x) in a.iter().enumerate() { |
367 | 0 | result[i] = x.tanh(); |
368 | 0 | } |
369 | 0 | } |
370 | | |
371 | | // SAFETY: This function is safe because: |
372 | | // 1. All slice accesses are bounds-checked by Rust iterator/enumerate |
373 | | // 2. No raw pointer arithmetic is performed |
374 | | // 3. Marked unsafe only to match VectorBackend trait interface |
375 | 0 | unsafe fn sqrt(a: &[f32], result: &mut [f32]) { |
376 | 0 | for (i, &val) in a.iter().enumerate() { |
377 | 0 | result[i] = val.sqrt(); |
378 | 0 | } |
379 | 0 | } |
380 | | |
381 | | // SAFETY: This function is safe because: |
382 | | // 1. All slice accesses are bounds-checked by Rust iterator/enumerate |
383 | | // 2. No raw pointer arithmetic is performed |
384 | | // 3. Marked unsafe only to match VectorBackend trait interface |
385 | 0 | unsafe fn recip(a: &[f32], result: &mut [f32]) { |
386 | 0 | for (i, &val) in a.iter().enumerate() { |
387 | 0 | result[i] = val.recip(); |
388 | 0 | } |
389 | 0 | } |
390 | | |
391 | | // SAFETY: This function is safe because: |
392 | | // 1. All slice accesses are bounds-checked by Rust iterator/enumerate |
393 | | // 2. No raw pointer arithmetic is performed |
394 | | // 3. Marked unsafe only to match VectorBackend trait interface |
395 | 0 | unsafe fn ln(a: &[f32], result: &mut [f32]) { |
396 | 0 | for (i, &val) in a.iter().enumerate() { |
397 | 0 | result[i] = val.ln(); |
398 | 0 | } |
399 | 0 | } |
400 | | |
401 | | // SAFETY: This function is safe because: |
402 | | // 1. All slice accesses are bounds-checked by Rust iterator/enumerate |
403 | | // 2. No raw pointer arithmetic is performed |
404 | | // 3. Marked unsafe only to match VectorBackend trait interface |
405 | 0 | unsafe fn log2(a: &[f32], result: &mut [f32]) { |
406 | 0 | for (i, &val) in a.iter().enumerate() { |
407 | 0 | result[i] = val.log2(); |
408 | 0 | } |
409 | 0 | } |
410 | | |
411 | | // SAFETY: This function is safe because: |
412 | | // 1. All slice accesses are bounds-checked by Rust iterator/enumerate |
413 | | // 2. No raw pointer arithmetic is performed |
414 | | // 3. Marked unsafe only to match VectorBackend trait interface |
415 | 0 | unsafe fn log10(a: &[f32], result: &mut [f32]) { |
416 | 0 | for (i, &val) in a.iter().enumerate() { |
417 | 0 | result[i] = val.log10(); |
418 | 0 | } |
419 | 0 | } |
420 | | |
421 | | // SAFETY: This function is safe because: |
422 | | // 1. All slice accesses are bounds-checked by Rust iterator/enumerate |
423 | | // 2. No raw pointer arithmetic is performed |
424 | | // 3. Marked unsafe only to match VectorBackend trait interface |
425 | 0 | unsafe fn sin(a: &[f32], result: &mut [f32]) { |
426 | 0 | for (i, &val) in a.iter().enumerate() { |
427 | 0 | result[i] = val.sin(); |
428 | 0 | } |
429 | 0 | } |
430 | | |
431 | | // SAFETY: This function is safe because: |
432 | | // 1. All slice accesses are bounds-checked by Rust iterator/enumerate |
433 | | // 2. No raw pointer arithmetic is performed |
434 | | // 3. Marked unsafe only to match VectorBackend trait interface |
435 | 0 | unsafe fn cos(a: &[f32], result: &mut [f32]) { |
436 | 0 | for (i, &val) in a.iter().enumerate() { |
437 | 0 | result[i] = val.cos(); |
438 | 0 | } |
439 | 0 | } |
440 | | |
441 | | // SAFETY: This function is safe because: |
442 | | // 1. All slice accesses are bounds-checked by Rust iterator/enumerate |
443 | | // 2. No raw pointer arithmetic is performed |
444 | | // 3. Marked unsafe only to match VectorBackend trait interface |
445 | 0 | unsafe fn tan(a: &[f32], result: &mut [f32]) { |
446 | 0 | for (i, &val) in a.iter().enumerate() { |
447 | 0 | result[i] = val.tan(); |
448 | 0 | } |
449 | 0 | } |
450 | | |
451 | | // SAFETY: This function is safe because: |
452 | | // 1. All slice accesses are bounds-checked by Rust iterator/enumerate |
453 | | // 2. No raw pointer arithmetic is performed |
454 | | // 3. Marked unsafe only to match VectorBackend trait interface |
455 | 0 | unsafe fn floor(a: &[f32], result: &mut [f32]) { |
456 | 0 | for (i, &val) in a.iter().enumerate() { |
457 | 0 | result[i] = val.floor(); |
458 | 0 | } |
459 | 0 | } |
460 | | |
461 | | // SAFETY: This function is safe because: |
462 | | // 1. All slice accesses are bounds-checked by Rust iterator/enumerate |
463 | | // 2. No raw pointer arithmetic is performed |
464 | | // 3. Marked unsafe only to match VectorBackend trait interface |
465 | 0 | unsafe fn ceil(a: &[f32], result: &mut [f32]) { |
466 | 0 | for (i, &val) in a.iter().enumerate() { |
467 | 0 | result[i] = val.ceil(); |
468 | 0 | } |
469 | 0 | } |
470 | | |
471 | | // SAFETY: This function is safe because: |
472 | | // 1. All slice accesses are bounds-checked by Rust iterator/enumerate |
473 | | // 2. No raw pointer arithmetic is performed |
474 | | // 3. Marked unsafe only to match VectorBackend trait interface |
475 | 0 | unsafe fn round(a: &[f32], result: &mut [f32]) { |
476 | 0 | for (i, &val) in a.iter().enumerate() { |
477 | 0 | result[i] = val.round(); |
478 | 0 | } |
479 | 0 | } |
480 | | } |
481 | | |
482 | | #[cfg(test)] |
483 | | mod tests { |
484 | | use super::*; |
485 | | |
486 | | #[test] |
487 | | fn test_scalar_add() { |
488 | | let a = [1.0, 2.0, 3.0, 4.0]; |
489 | | let b = [5.0, 6.0, 7.0, 8.0]; |
490 | | let mut result = [0.0; 4]; |
491 | | // SAFETY: Test code calling backend trait methods marked unsafe |
492 | | unsafe { |
493 | | ScalarBackend::add(&a, &b, &mut result); |
494 | | } |
495 | | assert_eq!(result, [6.0, 8.0, 10.0, 12.0]); |
496 | | } |
497 | | |
498 | | #[test] |
499 | | fn test_scalar_mul() { |
500 | | let a = [1.0, 2.0, 3.0, 4.0]; |
501 | | let b = [2.0, 3.0, 4.0, 5.0]; |
502 | | let mut result = [0.0; 4]; |
503 | | // SAFETY: Test code calling backend trait methods marked unsafe |
504 | | unsafe { |
505 | | ScalarBackend::mul(&a, &b, &mut result); |
506 | | } |
507 | | assert_eq!(result, [2.0, 6.0, 12.0, 20.0]); |
508 | | } |
509 | | |
510 | | #[test] |
511 | | fn test_scalar_dot() { |
512 | | let a = [1.0, 2.0, 3.0]; |
513 | | let b = [4.0, 5.0, 6.0]; |
514 | | // SAFETY: Test code calling backend trait methods marked unsafe |
515 | | let result = unsafe { ScalarBackend::dot(&a, &b) }; |
516 | | assert_eq!(result, 32.0); // 1*4 + 2*5 + 3*6 = 32 |
517 | | } |
518 | | |
519 | | #[test] |
520 | | fn test_scalar_sum() { |
521 | | let a = [1.0, 2.0, 3.0, 4.0]; |
522 | | // SAFETY: Test code calling backend trait methods marked unsafe |
523 | | let result = unsafe { ScalarBackend::sum(&a) }; |
524 | | assert_eq!(result, 10.0); |
525 | | } |
526 | | |
527 | | #[test] |
528 | | fn test_scalar_max() { |
529 | | let a = [1.0, 5.0, 3.0, 2.0]; |
530 | | // SAFETY: Test code calling backend trait methods marked unsafe |
531 | | let result = unsafe { ScalarBackend::max(&a) }; |
532 | | assert_eq!(result, 5.0); |
533 | | } |
534 | | |
535 | | #[test] |
536 | | fn test_scalar_min() { |
537 | | let a = [1.0, 5.0, 3.0, 2.0]; |
538 | | // SAFETY: Test code calling backend trait methods marked unsafe |
539 | | let result = unsafe { ScalarBackend::min(&a) }; |
540 | | assert_eq!(result, 1.0); |
541 | | } |
542 | | |
543 | | #[test] |
544 | | fn test_scalar_sub() { |
545 | | let a = [5.0, 6.0, 7.0, 8.0]; |
546 | | let b = [1.0, 2.0, 3.0, 4.0]; |
547 | | let mut result = [0.0; 4]; |
548 | | // SAFETY: Test code calling backend trait methods marked unsafe |
549 | | unsafe { |
550 | | ScalarBackend::sub(&a, &b, &mut result); |
551 | | } |
552 | | assert_eq!(result, [4.0, 4.0, 4.0, 4.0]); |
553 | | } |
554 | | |
555 | | #[test] |
556 | | fn test_scalar_div() { |
557 | | let a = [10.0, 20.0, 30.0, 40.0]; |
558 | | let b = [2.0, 4.0, 5.0, 8.0]; |
559 | | let mut result = [0.0; 4]; |
560 | | // SAFETY: Test code calling backend trait methods marked unsafe |
561 | | unsafe { |
562 | | ScalarBackend::div(&a, &b, &mut result); |
563 | | } |
564 | | assert_eq!(result, [5.0, 5.0, 6.0, 5.0]); |
565 | | } |
566 | | |
567 | | #[test] |
568 | | fn test_scalar_argmax() { |
569 | | let a = [1.0, 5.0, 3.0, 2.0]; |
570 | | // SAFETY: Test code calling backend trait methods marked unsafe |
571 | | let result = unsafe { ScalarBackend::argmax(&a) }; |
572 | | assert_eq!(result, 1); // Index of 5.0 |
573 | | } |
574 | | |
575 | | #[test] |
576 | | fn test_scalar_argmin() { |
577 | | let a = [5.0, 1.0, 3.0, 2.0]; |
578 | | // SAFETY: Test code calling backend trait methods marked unsafe |
579 | | let result = unsafe { ScalarBackend::argmin(&a) }; |
580 | | assert_eq!(result, 1); // Index of 1.0 |
581 | | } |
582 | | |
583 | | #[test] |
584 | | fn test_scalar_sum_kahan() { |
585 | | let a = [1.0, 2.0, 3.0, 4.0]; |
586 | | // SAFETY: Test code calling backend trait methods marked unsafe |
587 | | let result = unsafe { ScalarBackend::sum_kahan(&a) }; |
588 | | assert_eq!(result, 10.0); |
589 | | } |
590 | | |
591 | | #[test] |
592 | | fn test_scalar_norm_l1() { |
593 | | let a = [1.0, -2.0, 3.0, -4.0]; |
594 | | // SAFETY: Test code calling backend trait methods marked unsafe |
595 | | let result = unsafe { ScalarBackend::norm_l1(&a) }; |
596 | | assert_eq!(result, 10.0); // |1| + |-2| + |3| + |-4| = 10 |
597 | | } |
598 | | |
599 | | #[test] |
600 | | fn test_scalar_norm_l2() { |
601 | | let a = [3.0, 4.0]; |
602 | | // SAFETY: Test code calling backend trait methods marked unsafe |
603 | | let result = unsafe { ScalarBackend::norm_l2(&a) }; |
604 | | assert_eq!(result, 5.0); // sqrt(3² + 4²) = 5 |
605 | | } |
606 | | |
607 | | #[test] |
608 | | fn test_scalar_scale() { |
609 | | let a = [1.0, 2.0, 3.0, 4.0]; |
610 | | let mut result = [0.0; 4]; |
611 | | // SAFETY: Test code calling backend trait methods marked unsafe |
612 | | unsafe { |
613 | | ScalarBackend::scale(&a, 2.0, &mut result); |
614 | | } |
615 | | assert_eq!(result, [2.0, 4.0, 6.0, 8.0]); |
616 | | } |
617 | | |
618 | | #[test] |
619 | | fn test_scalar_clamp() { |
620 | | let a = [1.0, 5.0, 10.0, 15.0]; |
621 | | let mut result = [0.0; 4]; |
622 | | // SAFETY: Test code calling backend trait methods marked unsafe |
623 | | unsafe { |
624 | | ScalarBackend::clamp(&a, 3.0, 12.0, &mut result); |
625 | | } |
626 | | assert_eq!(result, [3.0, 5.0, 10.0, 12.0]); |
627 | | } |
628 | | |
629 | | #[test] |
630 | | fn test_scalar_lerp() { |
631 | | let a = [0.0, 10.0, 20.0]; |
632 | | let b = [100.0, 110.0, 120.0]; |
633 | | let mut result = [0.0; 3]; |
634 | | // SAFETY: Test code calling backend trait methods marked unsafe |
635 | | unsafe { |
636 | | ScalarBackend::lerp(&a, &b, 0.5, &mut result); |
637 | | } |
638 | | assert_eq!(result, [50.0, 60.0, 70.0]); // Midpoint between a and b |
639 | | } |
640 | | |
641 | | #[test] |
642 | | fn test_scalar_fma() { |
643 | | let a = [1.0, 2.0, 3.0]; |
644 | | let b = [2.0, 3.0, 4.0]; |
645 | | let c = [5.0, 6.0, 7.0]; |
646 | | let mut result = [0.0; 3]; |
647 | | // SAFETY: Test code calling backend trait methods marked unsafe |
648 | | unsafe { |
649 | | ScalarBackend::fma(&a, &b, &c, &mut result); |
650 | | } |
651 | | // FMA: a*b + c |
652 | | assert_eq!(result, [7.0, 12.0, 19.0]); // [1*2+5, 2*3+6, 3*4+7] |
653 | | } |
654 | | |
655 | | #[test] |
656 | | fn test_scalar_relu() { |
657 | | let a = [-3.0, -1.0, 0.0, 1.0, 3.0]; |
658 | | let mut result = [0.0; 5]; |
659 | | // SAFETY: Test code calling backend trait methods marked unsafe |
660 | | unsafe { |
661 | | ScalarBackend::relu(&a, &mut result); |
662 | | } |
663 | | assert_eq!(result, [0.0, 0.0, 0.0, 1.0, 3.0]); |
664 | | } |
665 | | |
666 | | #[test] |
667 | | fn test_scalar_sigmoid() { |
668 | | let a = [-51.0, -1.0, 0.0, 1.0, 51.0]; |
669 | | let mut result = [0.0; 5]; |
670 | | // SAFETY: Test code calling backend trait methods marked unsafe |
671 | | unsafe { |
672 | | ScalarBackend::sigmoid(&a, &mut result); |
673 | | } |
674 | | // sigmoid(-51) = 0, sigmoid(0) = 0.5, sigmoid(51) = 1 |
675 | | assert_eq!(result[0], 0.0); // Clamped to 0 for numerical stability |
676 | | assert!((result[1] - 0.2689).abs() < 0.001); // sigmoid(-1) |
677 | | assert_eq!(result[2], 0.5); // sigmoid(0) |
678 | | assert!((result[3] - 0.7311).abs() < 0.001); // sigmoid(1) |
679 | | assert_eq!(result[4], 1.0); // Clamped to 1 for numerical stability |
680 | | } |
681 | | |
682 | | #[test] |
683 | | fn test_scalar_gelu() { |
684 | | let a = [-2.0, -1.0, 0.0, 1.0, 2.0]; |
685 | | let mut result = [0.0; 5]; |
686 | | // SAFETY: Test code calling backend trait methods marked unsafe |
687 | | unsafe { |
688 | | ScalarBackend::gelu(&a, &mut result); |
689 | | } |
690 | | // GELU approximation values |
691 | | assert!((result[0] - (-0.0454)).abs() < 0.01); // gelu(-2) |
692 | | assert!((result[1] - (-0.1588)).abs() < 0.01); // gelu(-1) |
693 | | assert_eq!(result[2], 0.0); // gelu(0) = 0 |
694 | | assert!((result[3] - 0.8413).abs() < 0.01); // gelu(1) |
695 | | assert!((result[4] - 1.9545).abs() < 0.01); // gelu(2) |
696 | | } |
697 | | |
698 | | #[test] |
699 | | fn test_scalar_swish() { |
700 | | let a = [-51.0, -1.0, 0.0, 1.0, 51.0]; |
701 | | let mut result = [0.0; 5]; |
702 | | // SAFETY: Test code calling backend trait methods marked unsafe |
703 | | unsafe { |
704 | | ScalarBackend::swish(&a, &mut result); |
705 | | } |
706 | | // swish(x) = x * sigmoid(x) |
707 | | assert_eq!(result[0], 0.0); // x * 0 = 0 (numerical stability) |
708 | | assert!((result[1] - (-0.2689)).abs() < 0.001); // -1 * sigmoid(-1) |
709 | | assert_eq!(result[2], 0.0); // 0 * sigmoid(0) = 0 |
710 | | assert!((result[3] - 0.7311).abs() < 0.001); // 1 * sigmoid(1) |
711 | | assert_eq!(result[4], 51.0); // x * 1 = x (numerical stability) |
712 | | } |
713 | | |
714 | | // cuda-tile-behavior.md: Falsification test #81 - Backend equivalence |
715 | | #[test] |
716 | | fn test_scalar_dot_unrolled_various_sizes() { |
717 | | // Test various sizes to exercise all code paths in unrolled implementation: |
718 | | // - 0 elements (edge case) |
719 | | // - 1-3 elements (remainder only) |
720 | | // - 4 elements (exactly one unrolled chunk) |
721 | | // - 5-7 elements (one chunk + remainder) |
722 | | // - 8 elements (two chunks) |
723 | | // - 100 elements (realistic workload) |
724 | | // - 1000 elements (larger workload) |
725 | | let sizes = [0, 1, 2, 3, 4, 5, 6, 7, 8, 100, 1000]; |
726 | | |
727 | | for &size in &sizes { |
728 | | if size == 0 { |
729 | | continue; // Empty slice edge case |
730 | | } |
731 | | |
732 | | let a: Vec<f32> = (0..size).map(|i| (i as f32) * 0.1).collect(); |
733 | | let b: Vec<f32> = (0..size).map(|i| ((size - i) as f32) * 0.1).collect(); |
734 | | |
735 | | // Calculate expected result using naive implementation |
736 | | let expected: f32 = a.iter().zip(&b).map(|(x, y)| x * y).sum(); |
737 | | // SAFETY: CPU feature verified at runtime, slices bounds-checked |
738 | | let result = unsafe { ScalarBackend::dot(&a, &b) }; |
739 | | |
740 | | // Tolerance accounts for FP reordering from unrolling (different accumulator summing order) |
741 | | // Use relative tolerance for larger sums where absolute error grows with magnitude |
742 | | let tolerance = (1e-5 * expected.abs()).max(1e-4); |
743 | | assert!( |
744 | | (result - expected).abs() < tolerance, |
745 | | "dot mismatch at size {}: got={}, expected={}, tolerance={}", |
746 | | size, |
747 | | result, |
748 | | expected, |
749 | | tolerance |
750 | | ); |
751 | | } |
752 | | } |
753 | | |
754 | | // cuda-tile-behavior.md: Falsification test #92 - FMA single-rounding accuracy |
755 | | #[test] |
756 | | fn test_scalar_dot_mul_add_accuracy() { |
757 | | // Test that mul_add provides better accuracy than separate mul+add |
758 | | // FMA has single rounding vs two roundings for separate operations |
759 | | let a = vec![1.0000001_f32; 1000]; |
760 | | let b = vec![1.0000001_f32; 1000]; |
761 | | |
762 | | // SAFETY: CPU feature verified at runtime, slices bounds-checked |
763 | | let result = unsafe { ScalarBackend::dot(&a, &b) }; |
764 | | |
765 | | // Expected: 1000 * 1.0000001 * 1.0000001 ≈ 1000.0002 |
766 | | // With FMA, error should be smaller due to single rounding |
767 | | let expected = 1000.0 * 1.0000001_f32 * 1.0000001_f32; |
768 | | assert!( |
769 | | (result - expected).abs() < 1e-3, |
770 | | "FMA accuracy test: got={}, expected={}", |
771 | | result, |
772 | | expected |
773 | | ); |
774 | | } |
775 | | } |