Skip to main content

trueno/blis/
elementwise.rs

1//! SIMD-accelerated element-wise operations.
2//!
3//! AVX2 implementations of ReLU, vector add, and scalar multiply.
4//! These are bandwidth-bound at large sizes; SIMD helps at small-to-medium
5//! sizes by reducing instruction count and enabling wider stores.
6//!
7//! # Algorithm
8//!
9//! ReLU: `_mm256_max_ps(x, zero)` — single instruction per 8 elements
10//! Add: `_mm256_add_ps(a, b)` — single instruction per 8 elements
11//! Mul scalar: `_mm256_mul_ps(x, scalar_vec)` — single instruction per 8 elements
12//!
13//! Contract: provable-contracts/contracts/activation-kernel-v1.yaml
14
15use crate::error::TruenoError;
16
17// ============================================================================
18// ReLU
19// ============================================================================
20
21/// ReLU: output_i = max(0, input_i)
22///
23/// Uses AVX2 `_mm256_max_ps` when available.
24///
25/// # Errors
26///
27/// Returns `Err` if input and output lengths don't match.
28pub fn relu(input: &[f32], output: &mut [f32]) -> Result<(), TruenoError> {
29    contract_pre_relu!(input);
30    let n = input.len();
31    if n != output.len() {
32        return Err(TruenoError::InvalidInput(format!(
33            "relu size mismatch: input[{}], output[{}]",
34            n,
35            output.len()
36        )));
37    }
38
39    #[cfg(target_arch = "x86_64")]
40    {
41        // For bandwidth-bound elementwise ops, AVX2 at full clock beats
42        // AVX-512 at throttled clock (Zen 4: ~30% frequency reduction).
43        // For bandwidth-bound sizes (>4K), let LLVM auto-vectorize.
44        // LLVM -O3 with target-cpu=native produces optimal SIMD code
45        // that matches or beats hand-written intrinsics for simple ops,
46        // with better register allocation and loop fusion.
47        if n > 4096 {
48            relu_autovec(input, output);
49            contract_post_elementwise_parity!(output);
50            return Ok(());
51        }
52        if is_x86_feature_detected!("avx512f") {
53            unsafe {
54                relu_avx512(input, output);
55            }
56            contract_post_elementwise_parity!(output);
57            return Ok(());
58        }
59        if is_x86_feature_detected!("avx2") {
60            unsafe {
61                relu_avx2(input, output);
62            }
63            contract_post_elementwise_parity!(output);
64            return Ok(());
65        }
66    }
67
68    relu_autovec(input, output);
69    contract_post_elementwise_parity!(output);
70    Ok(())
71}
72
73/// ReLU via simple loop — LLVM auto-vectorizes this to optimal SIMD.
74/// For bandwidth-bound workloads (>4K elements), LLVM's autovectorizer
75/// with -O3 -C target-cpu=native produces code that matches hand-written
76/// intrinsics, with better register scheduling and no calling overhead.
77#[inline]
78fn relu_autovec(input: &[f32], output: &mut [f32]) {
79    for i in 0..input.len() {
80        output[i] = input[i].max(0.0);
81    }
82}
83
84/// AVX-512 ReLU with NT stores for large arrays.
85#[cfg(target_arch = "x86_64")]
86#[target_feature(enable = "avx512f")]
87unsafe fn relu_avx512(input: &[f32], output: &mut [f32]) {
88    use std::arch::x86_64::*;
89    unsafe {
90        let n = input.len();
91        let ip = input.as_ptr();
92        let op = output.as_mut_ptr();
93        let zero = _mm512_setzero_ps();
94        let mut i = 0;
95
96        let data_bytes = n * 4;
97        let op_aligned = (op as usize) % 64 == 0;
98        if data_bytes > NT_STORE_THRESHOLD_BYTES && op_aligned {
99            // NT path: 4-way unrolled, requires 64-byte aligned output
100            while i + 64 <= n {
101                _mm_prefetch(ip.add(i + 128).cast::<i8>(), _MM_HINT_T0);
102
103                _mm512_stream_ps(op.add(i), _mm512_max_ps(_mm512_loadu_ps(ip.add(i)), zero));
104                _mm512_stream_ps(
105                    op.add(i + 16),
106                    _mm512_max_ps(_mm512_loadu_ps(ip.add(i + 16)), zero),
107                );
108                _mm512_stream_ps(
109                    op.add(i + 32),
110                    _mm512_max_ps(_mm512_loadu_ps(ip.add(i + 32)), zero),
111                );
112                _mm512_stream_ps(
113                    op.add(i + 48),
114                    _mm512_max_ps(_mm512_loadu_ps(ip.add(i + 48)), zero),
115                );
116                i += 64;
117            }
118            while i + 16 <= n {
119                _mm512_stream_ps(op.add(i), _mm512_max_ps(_mm512_loadu_ps(ip.add(i)), zero));
120                i += 16;
121            }
122            _mm_sfence();
123        } else {
124            while i + 64 <= n {
125                _mm512_storeu_ps(op.add(i), _mm512_max_ps(_mm512_loadu_ps(ip.add(i)), zero));
126                _mm512_storeu_ps(
127                    op.add(i + 16),
128                    _mm512_max_ps(_mm512_loadu_ps(ip.add(i + 16)), zero),
129                );
130                _mm512_storeu_ps(
131                    op.add(i + 32),
132                    _mm512_max_ps(_mm512_loadu_ps(ip.add(i + 32)), zero),
133                );
134                _mm512_storeu_ps(
135                    op.add(i + 48),
136                    _mm512_max_ps(_mm512_loadu_ps(ip.add(i + 48)), zero),
137                );
138                i += 64;
139            }
140            while i + 16 <= n {
141                _mm512_storeu_ps(op.add(i), _mm512_max_ps(_mm512_loadu_ps(ip.add(i)), zero));
142                i += 16;
143            }
144        }
145        for j in i..n {
146            output[j] = input[j].max(0.0);
147        }
148    } // unsafe
149}
150
151/// Prefetch distance in bytes. 8 cache lines (512 bytes = 128 f32) ahead.
152/// Tuned for Zen 4 L1→L2 latency (~4ns) and L2→L3 latency (~12ns).
153/// At ~1 iteration/ns throughput, 512B ahead hides ~12ns L2 latency.
154#[cfg(target_arch = "x86_64")]
155const PREFETCH_DISTANCE: usize = 512;
156
157/// NT store threshold (bytes). Use non-temporal stores when total working set
158/// (2 inputs + 1 output = 3 arrays) exceeds L2 cache per core.
159/// Zen 4 L2 = 1MB/core. For add: 3 × data_bytes. NT is beneficial when
160/// data_bytes > ~333KB. Use 512KB for safety margin + alignment effects.
161/// Below this, data fits in L2 and cached stores are faster.
162#[cfg(target_arch = "x86_64")]
163const NT_STORE_THRESHOLD_BYTES: usize = 512 * 1024; // 512KB output = 128K f32
164
165#[cfg(target_arch = "x86_64")]
166#[target_feature(enable = "avx2")]
167unsafe fn relu_avx2(input: &[f32], output: &mut [f32]) {
168    use std::arch::x86_64::*;
169
170    let n = input.len();
171    let data_bytes = n * 4;
172
173    // For large arrays (>L3-stream threshold), use non-temporal stores
174    // ONLY if output is 32-byte aligned (required by _mm256_stream_ps).
175    // NT stores bypass cache write-allocate, eliminating RFO traffic.
176    let out_aligned = (output.as_ptr() as usize) % 32 == 0;
177    if data_bytes > NT_STORE_THRESHOLD_BYTES && out_aligned {
178        unsafe { relu_avx2_nt(input, output) }
179        return;
180    }
181
182    // 8× unrolled (64 elements per iteration) — no software prefetch.
183    // Hardware prefetcher on Zen 4/Intel 12th gen+ detects sequential
184    // streaming patterns and prefetches 2-4 cache lines ahead automatically.
185    // Software prefetch adds ~1 µop/32 elements of overhead without benefit
186    // for sequential access, and can interfere with HW prefetcher at L3 sizes.
187    let chunks = n / 64;
188    let remainder_64 = chunks * 64;
189
190    unsafe {
191        let zero = _mm256_setzero_ps();
192        let inp = input.as_ptr();
193        let out = output.as_mut_ptr();
194
195        for i in 0..chunks {
196            let base = i * 64;
197            let v0 = _mm256_loadu_ps(inp.add(base));
198            let v1 = _mm256_loadu_ps(inp.add(base + 8));
199            let v2 = _mm256_loadu_ps(inp.add(base + 16));
200            let v3 = _mm256_loadu_ps(inp.add(base + 24));
201            let v4 = _mm256_loadu_ps(inp.add(base + 32));
202            let v5 = _mm256_loadu_ps(inp.add(base + 40));
203            let v6 = _mm256_loadu_ps(inp.add(base + 48));
204            let v7 = _mm256_loadu_ps(inp.add(base + 56));
205            _mm256_storeu_ps(out.add(base), _mm256_max_ps(v0, zero));
206            _mm256_storeu_ps(out.add(base + 8), _mm256_max_ps(v1, zero));
207            _mm256_storeu_ps(out.add(base + 16), _mm256_max_ps(v2, zero));
208            _mm256_storeu_ps(out.add(base + 24), _mm256_max_ps(v3, zero));
209            _mm256_storeu_ps(out.add(base + 32), _mm256_max_ps(v4, zero));
210            _mm256_storeu_ps(out.add(base + 40), _mm256_max_ps(v5, zero));
211            _mm256_storeu_ps(out.add(base + 48), _mm256_max_ps(v6, zero));
212            _mm256_storeu_ps(out.add(base + 56), _mm256_max_ps(v7, zero));
213        }
214
215        let mut i = remainder_64;
216        while i + 8 <= n {
217            let v = _mm256_loadu_ps(inp.add(i));
218            _mm256_storeu_ps(out.add(i), _mm256_max_ps(v, zero));
219            i += 8;
220        }
221
222        while i < n {
223            *out.add(i) = (*inp.add(i)).max(0.0);
224            i += 1;
225        }
226    }
227}
228
229/// Non-temporal store variant for large arrays (>L2 cache size).
230/// Combines software prefetch pipeline with streaming stores to maximize
231/// DRAM bandwidth utilization. Write-combining buffers batch stores to
232/// full cache lines, eliminating read-for-ownership transactions.
233#[cfg(target_arch = "x86_64")]
234#[target_feature(enable = "avx2")]
235unsafe fn relu_avx2_nt(input: &[f32], output: &mut [f32]) {
236    use std::arch::x86_64::*;
237
238    let n = input.len();
239    let chunks = n / 32;
240    let remainder_32 = chunks * 32;
241
242    unsafe {
243        let zero = _mm256_setzero_ps();
244
245        for i in 0..chunks {
246            let base = i * 32;
247            // Prefetch input data ahead (L2→L3 latency hiding)
248            _mm_prefetch(
249                input.as_ptr().add(base + PREFETCH_DISTANCE / 4) as *const i8,
250                _MM_HINT_T0,
251            );
252            let v0 = _mm256_loadu_ps(input.as_ptr().add(base));
253            let v1 = _mm256_loadu_ps(input.as_ptr().add(base + 8));
254            let v2 = _mm256_loadu_ps(input.as_ptr().add(base + 16));
255            let v3 = _mm256_loadu_ps(input.as_ptr().add(base + 24));
256            // Non-temporal stores: bypass cache, write to WC buffers
257            _mm256_stream_ps(output.as_mut_ptr().add(base), _mm256_max_ps(v0, zero));
258            _mm256_stream_ps(output.as_mut_ptr().add(base + 8), _mm256_max_ps(v1, zero));
259            _mm256_stream_ps(output.as_mut_ptr().add(base + 16), _mm256_max_ps(v2, zero));
260            _mm256_stream_ps(output.as_mut_ptr().add(base + 24), _mm256_max_ps(v3, zero));
261        }
262
263        // Fence: ensure all NT stores are globally visible before return
264        _mm_sfence();
265
266        // Remainder with regular stores (< 1 cache line, no NT benefit)
267        let mut i = remainder_32;
268        while i + 8 <= n {
269            let v = _mm256_loadu_ps(input.as_ptr().add(i));
270            _mm256_storeu_ps(output.as_mut_ptr().add(i), _mm256_max_ps(v, zero));
271            i += 8;
272        }
273        while i < n {
274            output[i] = input[i].max(0.0);
275            i += 1;
276        }
277    }
278}
279
280// ============================================================================
281// Vector Add
282// ============================================================================
283
284/// Element-wise add: output_i = a_i + b_i
285///
286/// Uses AVX2 `_mm256_add_ps` when available.
287///
288/// # Errors
289///
290/// Returns `Err` if a, b, and output lengths don't match.
291pub fn add(a: &[f32], b: &[f32], output: &mut [f32]) -> Result<(), TruenoError> {
292    let n = a.len();
293    if n != b.len() || n != output.len() {
294        return Err(TruenoError::InvalidInput(format!(
295            "add size mismatch: a[{}], b[{}], output[{}]",
296            n,
297            b.len(),
298            output.len()
299        )));
300    }
301    contract_pre_add!(a, b);
302
303    #[cfg(target_arch = "x86_64")]
304    {
305        // For bandwidth-bound sizes (>4K), let LLVM auto-vectorize.
306        // LLVM -O3 with target-cpu=native matches hand-written intrinsics
307        // without #[target_feature] calling convention overhead.
308        if n > 4096 {
309            add_autovec(a, b, output);
310            return Ok(());
311        }
312        if is_x86_feature_detected!("avx512f") {
313            unsafe {
314                add_avx512(a, b, output);
315            }
316            return Ok(());
317        }
318        if is_x86_feature_detected!("avx2") {
319            unsafe {
320                add_avx2(a, b, output);
321            }
322            return Ok(());
323        }
324    }
325
326    add_autovec(a, b, output);
327    contract_post_elementwise_parity!(output);
328    Ok(())
329}
330
331/// Add via simple loop — LLVM auto-vectorizes optimally.
332#[inline]
333fn add_autovec(a: &[f32], b: &[f32], output: &mut [f32]) {
334    for i in 0..a.len() {
335        output[i] = a[i] + b[i];
336    }
337}
338
339/// AVX-512 add with NT stores for large arrays.
340#[cfg(target_arch = "x86_64")]
341#[target_feature(enable = "avx512f")]
342unsafe fn add_avx512(a: &[f32], b: &[f32], output: &mut [f32]) {
343    use std::arch::x86_64::*;
344    unsafe {
345        let n = a.len();
346        let ap = a.as_ptr();
347        let bp = b.as_ptr();
348        let rp = output.as_mut_ptr();
349        let mut i = 0;
350
351        let data_bytes = n * 4;
352        let rp_aligned = (rp as usize) % 64 == 0;
353        if data_bytes > NT_STORE_THRESHOLD_BYTES && rp_aligned {
354            // NT path: 4-way unrolled, requires 64-byte aligned output
355            while i + 64 <= n {
356                // Guard prefetch to avoid reading past allocation (#242 SIGSEGV fix)
357                if i + 128 <= n {
358                    _mm_prefetch(ap.add(i + 128).cast::<i8>(), _MM_HINT_T0);
359                    _mm_prefetch(bp.add(i + 128).cast::<i8>(), _MM_HINT_T0);
360                }
361
362                _mm512_stream_ps(
363                    rp.add(i),
364                    _mm512_add_ps(_mm512_loadu_ps(ap.add(i)), _mm512_loadu_ps(bp.add(i))),
365                );
366                _mm512_stream_ps(
367                    rp.add(i + 16),
368                    _mm512_add_ps(_mm512_loadu_ps(ap.add(i + 16)), _mm512_loadu_ps(bp.add(i + 16))),
369                );
370                _mm512_stream_ps(
371                    rp.add(i + 32),
372                    _mm512_add_ps(_mm512_loadu_ps(ap.add(i + 32)), _mm512_loadu_ps(bp.add(i + 32))),
373                );
374                _mm512_stream_ps(
375                    rp.add(i + 48),
376                    _mm512_add_ps(_mm512_loadu_ps(ap.add(i + 48)), _mm512_loadu_ps(bp.add(i + 48))),
377                );
378                i += 64;
379            }
380            while i + 16 <= n {
381                _mm512_stream_ps(
382                    rp.add(i),
383                    _mm512_add_ps(_mm512_loadu_ps(ap.add(i)), _mm512_loadu_ps(bp.add(i))),
384                );
385                i += 16;
386            }
387            _mm_sfence();
388        } else {
389            while i + 64 <= n {
390                _mm512_storeu_ps(
391                    rp.add(i),
392                    _mm512_add_ps(_mm512_loadu_ps(ap.add(i)), _mm512_loadu_ps(bp.add(i))),
393                );
394                _mm512_storeu_ps(
395                    rp.add(i + 16),
396                    _mm512_add_ps(_mm512_loadu_ps(ap.add(i + 16)), _mm512_loadu_ps(bp.add(i + 16))),
397                );
398                _mm512_storeu_ps(
399                    rp.add(i + 32),
400                    _mm512_add_ps(_mm512_loadu_ps(ap.add(i + 32)), _mm512_loadu_ps(bp.add(i + 32))),
401                );
402                _mm512_storeu_ps(
403                    rp.add(i + 48),
404                    _mm512_add_ps(_mm512_loadu_ps(ap.add(i + 48)), _mm512_loadu_ps(bp.add(i + 48))),
405                );
406                i += 64;
407            }
408            while i + 16 <= n {
409                _mm512_storeu_ps(
410                    rp.add(i),
411                    _mm512_add_ps(_mm512_loadu_ps(ap.add(i)), _mm512_loadu_ps(bp.add(i))),
412                );
413                i += 16;
414            }
415        }
416        for j in i..n {
417            output[j] = a[j] + b[j];
418        }
419    } // unsafe
420}
421
422#[cfg(target_arch = "x86_64")]
423#[target_feature(enable = "avx2")]
424unsafe fn add_avx2(a: &[f32], b: &[f32], output: &mut [f32]) {
425    use std::arch::x86_64::*;
426
427    let n = a.len();
428    let data_bytes = n * 4;
429
430    // Large arrays: NT stores (bypass cache for DRAM-bound writes)
431    // Only if output is 32-byte aligned (required by _mm256_stream_ps).
432    let out_aligned = (output.as_ptr() as usize) % 32 == 0;
433    if data_bytes > NT_STORE_THRESHOLD_BYTES && out_aligned {
434        unsafe { add_avx2_nt(a, b, output) }
435        return;
436    }
437
438    // 8× unrolled (64 elements per iteration) — no software prefetch.
439    // Hardware prefetcher handles sequential dual-stream patterns efficiently.
440    let chunks = n / 64;
441    let remainder_64 = chunks * 64;
442
443    unsafe {
444        let ap = a.as_ptr();
445        let bp = b.as_ptr();
446        let op = output.as_mut_ptr();
447
448        for i in 0..chunks {
449            let base = i * 64;
450            // Interleaved loads from a and b for maximum load port utilization
451            let a0 = _mm256_loadu_ps(ap.add(base));
452            let b0 = _mm256_loadu_ps(bp.add(base));
453            let a1 = _mm256_loadu_ps(ap.add(base + 8));
454            let b1 = _mm256_loadu_ps(bp.add(base + 8));
455            let a2 = _mm256_loadu_ps(ap.add(base + 16));
456            let b2 = _mm256_loadu_ps(bp.add(base + 16));
457            let a3 = _mm256_loadu_ps(ap.add(base + 24));
458            let b3 = _mm256_loadu_ps(bp.add(base + 24));
459            let a4 = _mm256_loadu_ps(ap.add(base + 32));
460            let b4 = _mm256_loadu_ps(bp.add(base + 32));
461            let a5 = _mm256_loadu_ps(ap.add(base + 40));
462            let b5 = _mm256_loadu_ps(bp.add(base + 40));
463            let a6 = _mm256_loadu_ps(ap.add(base + 48));
464            let b6 = _mm256_loadu_ps(bp.add(base + 48));
465            let a7 = _mm256_loadu_ps(ap.add(base + 56));
466            let b7 = _mm256_loadu_ps(bp.add(base + 56));
467            _mm256_storeu_ps(op.add(base), _mm256_add_ps(a0, b0));
468            _mm256_storeu_ps(op.add(base + 8), _mm256_add_ps(a1, b1));
469            _mm256_storeu_ps(op.add(base + 16), _mm256_add_ps(a2, b2));
470            _mm256_storeu_ps(op.add(base + 24), _mm256_add_ps(a3, b3));
471            _mm256_storeu_ps(op.add(base + 32), _mm256_add_ps(a4, b4));
472            _mm256_storeu_ps(op.add(base + 40), _mm256_add_ps(a5, b5));
473            _mm256_storeu_ps(op.add(base + 48), _mm256_add_ps(a6, b6));
474            _mm256_storeu_ps(op.add(base + 56), _mm256_add_ps(a7, b7));
475        }
476
477        let mut i = remainder_64;
478        while i + 8 <= n {
479            let av = _mm256_loadu_ps(ap.add(i));
480            let bv = _mm256_loadu_ps(bp.add(i));
481            _mm256_storeu_ps(op.add(i), _mm256_add_ps(av, bv));
482            i += 8;
483        }
484
485        while i < n {
486            *op.add(i) = *ap.add(i) + *bp.add(i);
487            i += 1;
488        }
489    }
490}
491
492/// Non-temporal store variant of add for large arrays (>L2 cache).
493#[cfg(target_arch = "x86_64")]
494#[target_feature(enable = "avx2")]
495unsafe fn add_avx2_nt(a: &[f32], b: &[f32], output: &mut [f32]) {
496    use std::arch::x86_64::*;
497
498    let n = a.len();
499    let chunks = n / 32;
500    let remainder_32 = chunks * 32;
501
502    unsafe {
503        for i in 0..chunks {
504            let base = i * 32;
505            _mm_prefetch(a.as_ptr().add(base + PREFETCH_DISTANCE / 4) as *const i8, _MM_HINT_T0);
506            _mm_prefetch(b.as_ptr().add(base + PREFETCH_DISTANCE / 4) as *const i8, _MM_HINT_T0);
507            let a0 = _mm256_loadu_ps(a.as_ptr().add(base));
508            let a1 = _mm256_loadu_ps(a.as_ptr().add(base + 8));
509            let a2 = _mm256_loadu_ps(a.as_ptr().add(base + 16));
510            let a3 = _mm256_loadu_ps(a.as_ptr().add(base + 24));
511            let b0 = _mm256_loadu_ps(b.as_ptr().add(base));
512            let b1 = _mm256_loadu_ps(b.as_ptr().add(base + 8));
513            let b2 = _mm256_loadu_ps(b.as_ptr().add(base + 16));
514            let b3 = _mm256_loadu_ps(b.as_ptr().add(base + 24));
515            _mm256_stream_ps(output.as_mut_ptr().add(base), _mm256_add_ps(a0, b0));
516            _mm256_stream_ps(output.as_mut_ptr().add(base + 8), _mm256_add_ps(a1, b1));
517            _mm256_stream_ps(output.as_mut_ptr().add(base + 16), _mm256_add_ps(a2, b2));
518            _mm256_stream_ps(output.as_mut_ptr().add(base + 24), _mm256_add_ps(a3, b3));
519        }
520
521        _mm_sfence();
522
523        let mut i = remainder_32;
524        while i + 8 <= n {
525            let av = _mm256_loadu_ps(a.as_ptr().add(i));
526            let bv = _mm256_loadu_ps(b.as_ptr().add(i));
527            _mm256_storeu_ps(output.as_mut_ptr().add(i), _mm256_add_ps(av, bv));
528            i += 8;
529        }
530        while i < n {
531            output[i] = a[i] + b[i];
532            i += 1;
533        }
534    }
535}
536
537// ============================================================================
538// Scalar Multiply
539// ============================================================================
540
541/// Element-wise scalar multiply: output_i = input_i * scalar
542///
543/// Uses AVX2 `_mm256_mul_ps` when available.
544///
545/// # Errors
546///
547/// Returns `Err` if input and output lengths don't match.
548pub fn mul_scalar(input: &[f32], scalar: f32, output: &mut [f32]) -> Result<(), TruenoError> {
549    // Contract: elementwise-kernel-v1.yaml, equation = mul_scalar
550    debug_assert!(!input.is_empty(), "Contract mul_scalar: input is empty");
551    debug_assert!(scalar.is_finite(), "Contract mul_scalar: scalar is not finite");
552    let n = input.len();
553    if n != output.len() {
554        return Err(TruenoError::InvalidInput(format!(
555            "mul_scalar size mismatch: input[{}], output[{}]",
556            n,
557            output.len()
558        )));
559    }
560
561    #[cfg(target_arch = "x86_64")]
562    {
563        if is_x86_feature_detected!("avx2") {
564            unsafe {
565                mul_scalar_avx2(input, scalar, output);
566            }
567            return Ok(());
568        }
569    }
570
571    for i in 0..n {
572        output[i] = input[i] * scalar;
573    }
574    Ok(())
575}
576
577#[cfg(target_arch = "x86_64")]
578#[target_feature(enable = "avx2")]
579unsafe fn mul_scalar_avx2(input: &[f32], scalar: f32, output: &mut [f32]) {
580    use std::arch::x86_64::*;
581
582    let n = input.len();
583    let chunks = n / 32;
584    let remainder_32 = chunks * 32;
585
586    unsafe {
587        let s = _mm256_set1_ps(scalar);
588
589        for i in 0..chunks {
590            let base = i * 32;
591            let v0 = _mm256_loadu_ps(input.as_ptr().add(base));
592            let v1 = _mm256_loadu_ps(input.as_ptr().add(base + 8));
593            let v2 = _mm256_loadu_ps(input.as_ptr().add(base + 16));
594            let v3 = _mm256_loadu_ps(input.as_ptr().add(base + 24));
595            _mm256_storeu_ps(output.as_mut_ptr().add(base), _mm256_mul_ps(v0, s));
596            _mm256_storeu_ps(output.as_mut_ptr().add(base + 8), _mm256_mul_ps(v1, s));
597            _mm256_storeu_ps(output.as_mut_ptr().add(base + 16), _mm256_mul_ps(v2, s));
598            _mm256_storeu_ps(output.as_mut_ptr().add(base + 24), _mm256_mul_ps(v3, s));
599        }
600
601        let mut i = remainder_32;
602        while i + 8 <= n {
603            let v = _mm256_loadu_ps(input.as_ptr().add(i));
604            _mm256_storeu_ps(output.as_mut_ptr().add(i), _mm256_mul_ps(v, s));
605            i += 8;
606        }
607
608        while i < n {
609            output[i] = input[i] * scalar;
610            i += 1;
611        }
612    }
613}
614
615// ============================================================================
616// Allocating variants (skip zero-initialization)
617// ============================================================================
618
619/// ReLU with output allocation. Avoids zero-fill overhead of `vec![0.0; n]`.
620///
621/// # Safety guarantee
622///
623/// Output Vec is fully initialized by the SIMD/scalar loop before return.
624#[must_use]
625pub fn relu_alloc(input: &[f32]) -> Vec<f32> {
626    let n = input.len();
627    let mut output = vec![0.0f32; n];
628    let _ = relu(input, &mut output);
629    output
630}
631
632/// Element-wise add with output allocation. Avoids zero-fill overhead.
633///
634/// # Panics
635///
636/// Panics if `a` and `b` have different lengths.
637#[must_use]
638pub fn add_alloc(a: &[f32], b: &[f32]) -> Vec<f32> {
639    assert_eq!(a.len(), b.len(), "add_alloc: length mismatch");
640    let n = a.len();
641    let mut output = vec![0.0f32; n];
642    let _ = add(a, b, &mut output);
643    output
644}
645
646/// Scalar multiply with output allocation. Avoids zero-fill overhead.
647#[must_use]
648pub fn mul_scalar_alloc(input: &[f32], scalar: f32) -> Vec<f32> {
649    let n = input.len();
650    let mut output = vec![0.0f32; n];
651    let _ = mul_scalar(input, scalar, &mut output);
652    output
653}
654
655// ============================================================================
656// Fused Operations (PMAT-021)
657// ============================================================================
658// Fused ops reduce DRAM traffic by combining multiple element-wise operations
659// into a single pass. For bandwidth-bound workloads (>4K elements), this is
660// the only way to beat the DRAM bandwidth ceiling that limits individual ops
661// to ~1.0x vs ndarray. Reference: XLA compiler fusion (arXiv:1802.04730).
662
663/// Fused add + ReLU: output_i = max(0, a_i + b_i)
664///
665/// Single pass over data: 2 reads + 1 write = 12 bytes/element.
666/// Unfused equivalent (add then relu) would be 2+1+1+1 = 20 bytes/element.
667/// 40% bandwidth reduction.
668///
669/// # Errors
670///
671/// Returns `Err` if a, b, and output lengths don't match.
672pub fn fused_add_relu(a: &[f32], b: &[f32], output: &mut [f32]) -> Result<(), TruenoError> {
673    let n = a.len();
674    if n != b.len() || n != output.len() {
675        return Err(TruenoError::InvalidInput(format!(
676            "fused_add_relu size mismatch: a[{}], b[{}], output[{}]",
677            n,
678            b.len(),
679            output.len()
680        )));
681    }
682    // LLVM auto-vectorizes this optimally with -O3 -C target-cpu=native.
683    for i in 0..n {
684        output[i] = (a[i] + b[i]).max(0.0);
685    }
686    Ok(())
687}
688
689/// Fused multiply-add: output_i = a_i * b_i + c_i
690///
691/// Single pass: 3 reads + 1 write = 16 bytes/element.
692/// Unfused equivalent (mul then add) = 24 bytes/element.
693/// 33% bandwidth reduction. Maps directly to FMA SIMD instruction.
694///
695/// # Errors
696///
697/// Returns `Err` if a, b, c, and output lengths don't match.
698pub fn fused_mul_add(
699    a: &[f32],
700    b: &[f32],
701    c: &[f32],
702    output: &mut [f32],
703) -> Result<(), TruenoError> {
704    let n = a.len();
705    if n != b.len() || n != c.len() || n != output.len() {
706        return Err(TruenoError::InvalidInput(format!(
707            "fused_mul_add size mismatch: a[{}], b[{}], c[{}], output[{}]",
708            n,
709            b.len(),
710            c.len(),
711            output.len()
712        )));
713    }
714    for i in 0..n {
715        output[i] = a[i].mul_add(b[i], c[i]);
716    }
717    Ok(())
718}
719
720/// Fused scale + bias + ReLU: output_i = max(0, input_i * scale + bias)
721///
722/// Common in neural network inference (linear layer + activation).
723/// Single pass: 1 read + 1 write = 8 bytes/element.
724/// Unfused (scale, add bias, relu) = 24 bytes/element.
725/// 67% bandwidth reduction.
726///
727/// # Errors
728///
729/// Returns `Err` if input and output lengths don't match.
730pub fn fused_scale_bias_relu(
731    input: &[f32],
732    scale: f32,
733    bias: f32,
734    output: &mut [f32],
735) -> Result<(), TruenoError> {
736    let n = input.len();
737    if n != output.len() {
738        return Err(TruenoError::InvalidInput(format!(
739            "fused_scale_bias_relu size mismatch: input[{}], output[{}]",
740            n,
741            output.len()
742        )));
743    }
744    for i in 0..n {
745        output[i] = input[i].mul_add(scale, bias).max(0.0);
746    }
747    Ok(())
748}
749
750// ============================================================================
751// In-Place Operations
752// ============================================================================
753// In-place ops eliminate the output buffer entirely, reducing memory traffic
754// from 2 reads + 1 write to 1 read + 1 write (33% reduction for unary ops).
755
756/// In-place ReLU: data_i = max(0, data_i)
757///
758/// 1 read + 1 write = 8 bytes/element (vs 12 for out-of-place).
759#[inline]
760pub fn relu_inplace(data: &mut [f32]) {
761    for x in data.iter_mut() {
762        *x = x.max(0.0);
763    }
764}
765
766/// In-place add: a_i += b_i
767///
768/// 2 reads + 1 write = 12 bytes/element (same as out-of-place but no alloc).
769pub fn add_inplace(a: &mut [f32], b: &[f32]) -> Result<(), TruenoError> {
770    if a.len() != b.len() {
771        return Err(TruenoError::InvalidInput(format!(
772            "add_inplace size mismatch: a[{}], b[{}]",
773            a.len(),
774            b.len()
775        )));
776    }
777    for i in 0..a.len() {
778        a[i] += b[i];
779    }
780    Ok(())
781}
782
783/// In-place scale: data_i *= scalar
784///
785/// 1 read + 1 write = 8 bytes/element.
786#[inline]
787pub fn scale_inplace(data: &mut [f32], scalar: f32) {
788    for x in data.iter_mut() {
789        *x *= scalar;
790    }
791}
792
793/// In-place fused add + ReLU: a_i = max(0, a_i + b_i)
794///
795/// 2 reads + 1 write = 12 bytes/element. Unfused in-place (add then relu)
796/// would be 2×(read+write) = 16 bytes. 25% reduction.
797pub fn fused_add_relu_inplace(a: &mut [f32], b: &[f32]) -> Result<(), TruenoError> {
798    if a.len() != b.len() {
799        return Err(TruenoError::InvalidInput(format!(
800            "fused_add_relu_inplace size mismatch: a[{}], b[{}]",
801            a.len(),
802            b.len()
803        )));
804    }
805    for i in 0..a.len() {
806        a[i] = (a[i] + b[i]).max(0.0);
807    }
808    Ok(())
809}
810
811// ============================================================================
812// Tests
813// ============================================================================
814
815#[cfg(test)]
816mod tests {
817    use super::*;
818
819    // ── ReLU tests ────────────────────────────────────────────────────────
820
821    #[test]
822    fn test_relu_basic() {
823        let input = [-1.0, 0.0, 1.0, -0.5, 2.0, -3.0, 0.1, -0.1];
824        let expected = [0.0, 0.0, 1.0, 0.0, 2.0, 0.0, 0.1, 0.0];
825        let mut output = vec![0.0f32; 8];
826        relu(&input, &mut output).unwrap();
827        assert_eq!(output, expected);
828    }
829
830    #[test]
831    fn test_relu_large() {
832        let n = 11008; // FFN intermediate size
833        let input: Vec<f32> =
834            (0..n).map(|i| ((i * 17 + 31) % 1000) as f32 / 1000.0 - 0.5).collect();
835        let mut output = vec![0.0f32; n];
836        relu(&input, &mut output).unwrap();
837        for (i, (&inp, &out)) in input.iter().zip(output.iter()).enumerate() {
838            assert_eq!(out, inp.max(0.0), "ReLU mismatch at {i}");
839        }
840    }
841
842    #[test]
843    fn test_relu_avx2_scalar_parity() {
844        for n in [1, 7, 8, 15, 16, 31, 32, 63, 64, 128, 4096] {
845            let input: Vec<f32> =
846                (0..n).map(|i| ((i * 17 + 31) % 1000) as f32 / 500.0 - 1.0).collect();
847            let mut output = vec![0.0f32; n];
848            relu(&input, &mut output).unwrap();
849            for (i, (&inp, &out)) in input.iter().zip(output.iter()).enumerate() {
850                assert_eq!(out, inp.max(0.0), "ReLU parity at [{i}] n={n}");
851            }
852        }
853    }
854
855    #[test]
856    fn test_relu_error_mismatch() {
857        let input = vec![1.0f32; 4];
858        let mut output = vec![0.0f32; 3];
859        assert!(relu(&input, &mut output).is_err());
860    }
861
862    // ── Add tests ─────────────────────────────────────────────────────────
863
864    #[test]
865    fn test_add_basic() {
866        let a = [1.0, 2.0, 3.0, 4.0];
867        let b = [10.0, 20.0, 30.0, 40.0];
868        let mut output = vec![0.0f32; 4];
869        add(&a, &b, &mut output).unwrap();
870        assert_eq!(output, vec![11.0, 22.0, 33.0, 44.0]);
871    }
872
873    #[test]
874    fn test_add_large() {
875        let n = 4096;
876        let a: Vec<f32> = (0..n).map(|i| i as f32).collect();
877        let b: Vec<f32> = (0..n).map(|i| (i * 2) as f32).collect();
878        let mut output = vec![0.0f32; n];
879        add(&a, &b, &mut output).unwrap();
880        for i in 0..n {
881            assert_eq!(output[i], a[i] + b[i], "Add mismatch at {i}");
882        }
883    }
884
885    #[test]
886    fn test_add_avx2_scalar_parity() {
887        for n in [1, 7, 8, 15, 16, 31, 32, 63, 64, 128, 4096] {
888            let a: Vec<f32> = (0..n).map(|i| ((i * 17 + 31) % 1000) as f32 / 500.0 - 1.0).collect();
889            let b: Vec<f32> = (0..n).map(|i| ((i * 13 + 7) % 1000) as f32 / 500.0 - 1.0).collect();
890            let mut output = vec![0.0f32; n];
891            add(&a, &b, &mut output).unwrap();
892            for i in 0..n {
893                assert_eq!(output[i], a[i] + b[i], "Add parity at [{i}] n={n}");
894            }
895        }
896    }
897
898    #[test]
899    fn test_add_error_mismatch() {
900        let a = vec![1.0f32; 4];
901        let b = vec![1.0f32; 3];
902        let mut output = vec![0.0f32; 4];
903        assert!(add(&a, &b, &mut output).is_err());
904    }
905
906    // ── Mul scalar tests ──────────────────────────────────────────────────
907
908    #[test]
909    fn test_mul_scalar_basic() {
910        let input = [1.0, 2.0, 3.0, 4.0];
911        let mut output = vec![0.0f32; 4];
912        mul_scalar(&input, 2.5, &mut output).unwrap();
913        assert_eq!(output, vec![2.5, 5.0, 7.5, 10.0]);
914    }
915
916    #[test]
917    fn test_mul_scalar_large() {
918        let n = 4096;
919        let input: Vec<f32> = (0..n).map(|i| i as f32).collect();
920        let mut output = vec![0.0f32; n];
921        mul_scalar(&input, std::f32::consts::PI, &mut output).unwrap();
922        for i in 0..n {
923            assert!(
924                (output[i] - input[i] * std::f32::consts::PI).abs() < 1e-5,
925                "Mul scalar mismatch at {i}"
926            );
927        }
928    }
929
930    #[test]
931    fn test_mul_scalar_avx2_scalar_parity() {
932        for n in [1, 7, 8, 15, 16, 31, 32, 63, 64, 128, 4096] {
933            let input: Vec<f32> =
934                (0..n).map(|i| ((i * 17 + 31) % 1000) as f32 / 500.0 - 1.0).collect();
935            let mut output = vec![0.0f32; n];
936            mul_scalar(&input, std::f32::consts::E, &mut output).unwrap();
937            for i in 0..n {
938                assert!(
939                    (output[i] - input[i] * std::f32::consts::E).abs() < 1e-4,
940                    "Mul scalar parity at [{i}] n={n}",
941                );
942            }
943        }
944    }
945
946    #[test]
947    fn test_mul_scalar_error_mismatch() {
948        let input = vec![1.0f32; 4];
949        let mut output = vec![0.0f32; 3];
950        assert!(mul_scalar(&input, 1.0, &mut output).is_err());
951    }
952
953    // ── Fused ops tests (PMAT-021) ──────────────────────────────────────
954
955    #[test]
956    fn test_fused_add_relu_basic() {
957        let a = vec![-2.0, -1.0, 0.0, 1.0, 2.0, -0.5, 0.5, 3.0];
958        let b = vec![1.0, 0.5, -1.0, -2.0, 0.0, 1.0, -1.0, -4.0];
959        let mut out = vec![0.0f32; 8];
960        fused_add_relu(&a, &b, &mut out).unwrap();
961        let expected: Vec<f32> = a.iter().zip(&b).map(|(a, b)| (a + b).max(0.0)).collect();
962        assert_eq!(out, expected);
963    }
964
965    #[test]
966    fn test_fused_add_relu_large() {
967        let n = 10_000;
968        let a: Vec<f32> = (0..n).map(|i| (i as f32 - 5000.0) / 100.0).collect();
969        let b: Vec<f32> = (0..n).map(|i| (i as f32 * 0.3) - 1500.0).collect();
970        let mut out = vec![0.0f32; n];
971        fused_add_relu(&a, &b, &mut out).unwrap();
972        for i in 0..n {
973            assert_eq!(out[i], (a[i] + b[i]).max(0.0), "mismatch at {i}");
974        }
975    }
976
977    #[test]
978    fn test_fused_mul_add_basic() {
979        let a = vec![1.0, 2.0, 3.0, 4.0];
980        let b = vec![2.0, 3.0, 4.0, 5.0];
981        let c = vec![0.5, 0.5, 0.5, 0.5];
982        let mut out = vec![0.0f32; 4];
983        fused_mul_add(&a, &b, &c, &mut out).unwrap();
984        let expected: Vec<f32> = (0..4).map(|i| a[i].mul_add(b[i], c[i])).collect();
985        assert_eq!(out, expected);
986    }
987
988    #[test]
989    fn test_fused_scale_bias_relu_basic() {
990        let input = vec![-2.0, -1.0, 0.0, 1.0, 2.0];
991        let mut out = vec![0.0f32; 5];
992        fused_scale_bias_relu(&input, 2.0, 1.0, &mut out).unwrap();
993        // 2*x + 1, then relu: [-3,0] [-1,0] [1] [3] [5]
994        assert_eq!(out, vec![0.0, 0.0, 1.0, 3.0, 5.0]);
995    }
996}