Skip to main content

tract_linalg/frame/
element_wise.rs

1use std::fmt::Debug;
2use std::marker::PhantomData;
3
4use tract_data::TractResult;
5
6use crate::LADatum;
7
8use super::element_wise_helper::map_slice_with_alignment;
9
10// An element-wise kernel from a `run` body. A leading arch ident is for bodies that are
11// inline arch asm or intrinsics, which will not even compile elsewhere: those builds get a
12// signature-matched panic stub instead, so the kernel struct exists everywhere.
13/// Declare an element-wise routine whose body is written here: the kernel, its registry descriptor
14/// and its accuracy tests, from one statement. `func` says which cell of the registry it fills and
15/// which tests it answers to; `param` marks a kernel taking a scalar of its own type; `isa` says
16/// which machines may run it, and therefore which may test it; `boost` is for a kernel that must
17/// never be chosen.
18macro_rules! routine_ew_rust {
19    (arm; $($rest:tt)*) => { routine_ew_rust!(@ arm, target_arch = "arm"; $($rest)*); };
20    (aarch64; $($rest:tt)*) => { routine_ew_rust!(@ aarch64, target_arch = "aarch64"; $($rest)*); };
21    (x86_64; $($rest:tt)*) => { routine_ew_rust!(@ x86_64, target_arch = "x86_64"; $($rest)*); };
22    (riscv64; $($rest:tt)*) => {
23        routine_ew_rust!(@ riscv64, target_arch = "riscv64"; $($rest)*);
24    };
25    (wasm32; $($rest:tt)*) => {
26        routine_ew_rust!(@ wasm32, all(target_arch = "wasm32", target_feature = "simd128");
27            $($rest)*);
28    };
29    (generic; $($rest:tt)*) => { routine_ew_rust!(@ generic, all(); $($rest)*); };
30
31    // A scalar-parameter kernel takes its own datum type as the parameter, and answers in the
32    // parameter shape; a plain one takes nothing.
33    (@ $arch:ident, $built:meta; $ti:ident, $ker:ident, $nr:expr, $alignment_items:expr,
34     $run:item, func($f:ident), param $(, isa($($isa:ident),+))? $(, boost($boost:expr))?) => {
35        paste! {
36            routine_ew_rust!(@@ $arch, $built; $ti, $ker, $nr, $alignment_items, $ti, $run, $f,
37                [<$ti:upper Param>] $(, isa($($isa),+))? $(, boost($boost))?);
38        }
39    };
40    (@ $arch:ident, $built:meta; $ti:ident, $ker:ident, $nr:expr, $alignment_items:expr,
41     $run:item, func($f:ident) $(, isa($($isa:ident),+))? $(, boost($boost:expr))?) => {
42        paste! {
43            routine_ew_rust!(@@ $arch, $built; $ti, $ker, $nr, $alignment_items, (), $run, $f,
44                [<$ti:upper>] $(, isa($($isa),+))? $(, boost($boost))?);
45        }
46    };
47
48    (@@ $arch:ident, $built:meta; $ti:ident, $ker:ident, $nr:expr, $alignment_items:expr,
49     $params:ty, $run:item, $f:ident, $factory:ident
50     $(, isa($($isa:ident),+))? $(, boost($boost:expr))?) => {
51        ew_kernel!(@ $built; $ti, $ker, $nr, $alignment_items, $params, $run);
52        paste! {
53            submit_routine!($arch; $factory, $f, $ker $(, isa($($isa),+))? $(, boost($boost))?);
54            #[cfg(test)]
55            mod [<test_ $ker:snake>] {
56                use super::*;
57                crate::[<$f:snake _frame_tests>]!(
58                    cfg!($built)
59                        && $crate::isa::IsaReq::ANY
60                            $(.needing(&[$($crate::isa::Isa::$isa),+]))?
61                            .satisfied_by($crate::isa::native()),
62                    $ti,
63                    $ker
64                );
65            }
66        }
67    };
68}
69
70macro_rules! ew_kernel {
71    (arm; $($rest:tt)*) => { ew_kernel!(@ target_arch = "arm"; $($rest)*); };
72    (aarch64; $($rest:tt)*) => { ew_kernel!(@ target_arch = "aarch64"; $($rest)*); };
73    (x86_64; $($rest:tt)*) => { ew_kernel!(@ target_arch = "x86_64"; $($rest)*); };
74    (riscv64; $($rest:tt)*) => { ew_kernel!(@ target_arch = "riscv64"; $($rest)*); };
75    (wasm32; $($rest:tt)*) => { ew_kernel!(@ all(target_arch = "wasm32", target_feature = "simd128"); $($rest)*); };
76
77    (@ $built:meta; $ti:ident, $func:ident, $nr:expr, $alignment_items:expr, $params:ty, $run:item) => {
78        #[cfg($built)]
79        ew_kernel!($ti, $func, $nr, $alignment_items, $params, $run);
80        #[cfg(not($built))]
81        ew_kernel!($ti, $func, $nr, $alignment_items, $params,
82            fn run(_vec: &mut [$ti], _params: $params) {
83                panic!(concat!(stringify!($func), ": kernel not built for this target"))
84            }
85        );
86    };
87
88    ($ti: ident, $func: ident, $nr: expr, $alignment_items: expr, $params: ty, $run: item) => {
89        paste! {
90            #[derive(Copy, Clone, Debug)]
91            #[allow(non_camel_case_types)]
92            pub struct $func;
93
94            impl crate::frame::element_wise::ElementWiseKer<$ti, $params> for $func {
95                #[inline(always)]
96                fn name() -> &'static str {
97                    stringify!($func)
98                }
99                #[inline(always)]
100                fn nr() -> usize {
101                    $nr
102                }
103                #[inline(always)]
104                fn alignment_items() -> usize {
105                    $alignment_items
106                }
107                $run
108            }
109        }
110    };
111}
112
113/// Define an f16 element-wise kernel for cores without native f16 arithmetic by
114/// round-tripping through an existing f32 kernel: convert each `CHUNK`-sized f16
115/// slice into an aligned f32 scratch, run the f32 kernel in place, convert back.
116///
117/// Callers supply the `unsafe` f16<->f32 conversion fns (their target-feature
118/// gating, if any, lives on those fns — this macro is architecture-agnostic), the
119/// f32 kernel to reuse, the f32-scratch `CHUNK`, and the scratch alignment (must
120/// satisfy the f32 kernel's input-alignment contract, since `run` is called
121/// directly, bypassing `map_slice_with_alignment`). The remaining arguments match
122/// `ew_kernel!`.
123///
124/// `CHUNK` must be a multiple of `nr`: the f32 kernel steps `nr` lanes with no
125/// tail, and each chunk length passed to it is a multiple of `nr` only because
126/// both `CHUNK` and every buffer length are.
127///
128/// The param arm converts the f16-side param into the f32 kernel's param via
129/// `$pname => $pconv` (e.g. `f16, alpha => alpha.to_f32()`), computed once per call.
130/// Declare an element-wise routine whose body round-trips through an f32 kernel: the kernel, its
131/// registry descriptor and its accuracy tests, from one statement. The arguments after the
132/// alignment are the round-trip's -- scratch length, scratch alignment, the two conversions and
133/// the f32 kernel to reuse -- and `param` names the f16 parameter and how it converts.
134macro_rules! routine_ew_via_f32 {
135    (aarch64; $($rest:tt)*) => {
136        routine_ew_via_f32!(@ aarch64, target_arch = "aarch64"; $($rest)*);
137    };
138    (x86_64; $($rest:tt)*) => {
139        routine_ew_via_f32!(@ x86_64, target_arch = "x86_64"; $($rest)*);
140    };
141    (arm; $($rest:tt)*) => { routine_ew_via_f32!(@ arm, target_arch = "arm"; $($rest)*); };
142    (wasm32; $($rest:tt)*) => {
143        routine_ew_via_f32!(@ wasm32, all(target_arch = "wasm32", target_feature = "simd128");
144            $($rest)*);
145    };
146
147    (@ $arch:ident, $built:meta; $ker:ident, $nr:expr, $alignment_items:expr, $chunk:expr,
148     $scratch_align:literal, $cvt_in:path, $cvt_out:path, $f32_kernel:ty, func($f:ident),
149     param($pname:ident => $pconv:expr) $(, isa($($isa:ident),+))? $(, boost($boost:expr))?) => {
150        ew_kernel_via_f32!($ker, $nr, $alignment_items, $chunk, $scratch_align,
151            $cvt_in, $cvt_out, $f32_kernel, f16, $pname => $pconv);
152        routine_ew_via_f32!(@@ $arch, $built; $ker, $f, F16Param
153            $(, isa($($isa),+))? $(, boost($boost))?);
154    };
155
156    (@ $arch:ident, $built:meta; $ker:ident, $nr:expr, $alignment_items:expr, $chunk:expr,
157     $scratch_align:literal, $cvt_in:path, $cvt_out:path, $f32_kernel:ty, func($f:ident)
158     $(, isa($($isa:ident),+))? $(, boost($boost:expr))?) => {
159        ew_kernel_via_f32!($ker, $nr, $alignment_items, $chunk, $scratch_align,
160            $cvt_in, $cvt_out, $f32_kernel);
161        routine_ew_via_f32!(@@ $arch, $built; $ker, $f, F16
162            $(, isa($($isa),+))? $(, boost($boost))?);
163    };
164
165    (@@ $arch:ident, $built:meta; $ker:ident, $f:ident, $factory:ident
166     $(, isa($($isa:ident),+))? $(, boost($boost:expr))?) => {
167        submit_routine!($arch; $factory, $f, $ker
168            $(, isa($($isa),+))? $(, boost($boost))?, round_trip(true));
169        paste! {
170            #[cfg(test)]
171            mod [<test_ $ker:snake>] {
172                use super::*;
173                crate::[<$f:snake _frame_tests>]!(
174                    cfg!($built)
175                        && $crate::isa::IsaReq::ANY
176                            $(.needing(&[$($crate::isa::Isa::$isa),+]))?
177                            .satisfied_by($crate::isa::native()),
178                    f16,
179                    $ker
180                );
181            }
182        }
183    };
184}
185
186macro_rules! ew_kernel_via_f32 {
187    ($func:ident, $nr:expr, $alignment_items:expr, $chunk:expr, $scratch_align:literal,
188     $cvt_in:path, $cvt_out:path, $f32_kernel:ty) => {
189        ew_kernel_via_f32!(@build $func, $nr, $alignment_items, $chunk, $scratch_align,
190            $cvt_in, $cvt_out, $f32_kernel, (), _params, ());
191    };
192    ($func:ident, $nr:expr, $alignment_items:expr, $chunk:expr, $scratch_align:literal,
193     $cvt_in:path, $cvt_out:path, $f32_kernel:ty, $params:ty, $pname:ident => $pconv:expr) => {
194        ew_kernel_via_f32!(@build $func, $nr, $alignment_items, $chunk, $scratch_align,
195            $cvt_in, $cvt_out, $f32_kernel, $params, $pname, $pconv);
196    };
197    (@build $func:ident, $nr:expr, $alignment_items:expr, $chunk:expr, $scratch_align:literal,
198     $cvt_in:path, $cvt_out:path, $f32_kernel:ty, $params:ty, $pname:ident, $pconv:expr) => {
199        ew_kernel!(
200            f16, $func, $nr, $alignment_items, $params,
201            #[inline(never)]
202            fn run(buf: &mut [f16], $pname: $params) {
203                const _: () = assert!(
204                    $chunk % $nr == 0,
205                    "CHUNK must be a multiple of nr; the f32 kernel steps nr lanes with no tail"
206                );
207                #[repr(C, align($scratch_align))]
208                struct AlignedScratch([f32; $chunk]);
209                debug_assert!(buf.len() % Self::nr() == 0);
210                debug_assert!(buf.as_ptr() as usize % Self::alignment_bytes() == 0);
211                if buf.is_empty() {
212                    return;
213                }
214                let f32_params = $pconv;
215                let mut scratch = std::mem::MaybeUninit::<AlignedScratch>::uninit();
216                // SAFETY: f32 has no invalid bit patterns, and every `s[..n]` element is
217                // written by `$cvt_in` before the f32 kernel or `$cvt_out` reads it, so the
218                // scratch never needs zero-initialising.
219                let s = unsafe { &mut (*scratch.as_mut_ptr()).0 };
220                let mut i = 0;
221                while i < buf.len() {
222                    let n = ($chunk).min(buf.len() - i);
223                    unsafe { $cvt_in(&buf[i..i + n], &mut s[..n]) };
224                    <$f32_kernel>::run(&mut s[..n], f32_params);
225                    unsafe { $cvt_out(&s[..n], &mut buf[i..i + n]) };
226                    i += n;
227                }
228            }
229        );
230    };
231}
232
233// An element-wise kernel whose body is an asm extern. A leading arch ident emits that extern
234// only in builds carrying the arch's instructions, replaced elsewhere by a bail stub of the
235// same signature, so the module links everywhere.
236
237/// Declare an element-wise routine: the kernel, the registry descriptor, and the accuracy tests,
238/// from one statement. The leading architecture ident is the one every kernel-declaration macro
239/// takes, omitted for generic Rust; `isa` names what the architecture must offer beyond
240/// itself.
241///
242/// The instruction set is declared once and answers both questions it used to be asked twice:
243/// which machines may select the kernel, and which machines may test it. A kernel whose tests
244/// are skipped everywhere it could run has nothing left to say, so the two must not be able to
245/// disagree.
246macro_rules! routine_ew_extern {
247    (arm; $($rest:tt)*) => { routine_ew_extern!(@ arm, target_arch = "arm"; $($rest)*); };
248    (aarch64; $($rest:tt)*) => { routine_ew_extern!(@ aarch64, target_arch = "aarch64"; $($rest)*); };
249    (x86_64; $($rest:tt)*) => { routine_ew_extern!(@ x86_64, target_arch = "x86_64"; $($rest)*); };
250    (riscv64; $($rest:tt)*) => { routine_ew_extern!(@ riscv64, target_arch = "riscv64"; $($rest)*); };
251    (wasm32; $($rest:tt)*) => {
252        routine_ew_extern!(@ wasm32,
253            all(target_arch = "wasm32", target_feature = "simd128"); $($rest)*);
254    };
255
256    (@ $arch:ident, $built:meta; $func:ident, $ti:ident, $ker:ident,
257     $nr:expr, $alignment_items:expr $(, isa($($isa:ident),+))?) => {
258        ew_kernel_extern!($arch; $ti, $ker, $nr, $alignment_items);
259        paste! {
260            submit_routine!($arch; [<$ti:upper>], $func, $ker $(, isa($($isa),+))?);
261        }
262        #[cfg(test)]
263        paste! {
264            mod [<test_ $ker:snake>] {
265                use super::*;
266                [<$func:snake _frame_tests>]!(
267                    cfg!($built)
268                        && $crate::isa::IsaReq::ANY
269                            $(.needing(&[$($crate::isa::Isa::$isa),+]))?
270                            .satisfied_by($crate::isa::native()),
271                    $ti,
272                    $ker
273                );
274            }
275        }
276    };
277}
278
279macro_rules! ew_kernel_extern {
280    (arm; $($rest:tt)*) => { ew_kernel_extern!(@ target_arch = "arm"; $($rest)*); };
281    (aarch64; $($rest:tt)*) => { ew_kernel_extern!(@ target_arch = "aarch64"; $($rest)*); };
282    (x86_64; $($rest:tt)*) => { ew_kernel_extern!(@ target_arch = "x86_64"; $($rest)*); };
283    (riscv64; $($rest:tt)*) => { ew_kernel_extern!(@ target_arch = "riscv64"; $($rest)*); };
284    (wasm32; $($rest:tt)*) => { ew_kernel_extern!(@ all(target_arch = "wasm32", target_feature = "simd128"); $($rest)*); };
285
286    (@ $built:meta; $ti:ident, $func:ident, $nr:expr, $alignment_items:expr) => {
287        paste! {
288            mod [<sys_ $func>] {
289                #[allow(unused_imports)]
290                use tract_data::prelude::f16;
291
292                #[cfg($built)]
293                extern_kernel!(fn $func(ptr: *mut $ti, count: usize) -> ());
294
295                #[cfg(not($built))]
296                #[allow(dead_code)]
297                pub unsafe fn $func(_ptr: *mut $ti, _count: usize) {
298                    panic!(concat!(stringify!($func), ": activation kernel not built for this target"))
299                }
300            }
301            ew_kernel!($ti, $func, $nr, $alignment_items, (),
302                #[inline(never)]
303                fn run(buf: &mut [$ti], _params: ()) {
304                    unsafe { [<sys_ $func>]::$func(buf.as_mut_ptr(), buf.len()) }
305                }
306            );
307        }
308    };
309
310}
311
312pub trait ElementWise<T, Params = ()>: Send + Sync + Debug + dyn_clone::DynClone
313where
314    Params: Copy + Send + Sync + Debug + 'static + Default,
315    T: Copy + Debug + PartialEq + Send + Sync,
316{
317    fn name(&self) -> &'static str;
318    fn run(&self, vec: &mut [T]) -> TractResult<()> {
319        self.run_with_params(vec, Params::default())
320    }
321    fn run_with_params(&self, vec: &mut [T], params: Params) -> TractResult<()>;
322}
323
324dyn_clone::clone_trait_object!(<T, Params> ElementWise<T, Params> where T: Copy, Params: Copy);
325
326#[derive(Debug, Clone, new)]
327pub struct ElementWiseImpl<K, T, Params = ()>
328where
329    T: LADatum,
330    Params: Copy + Send + Sync + Debug + 'static + Default,
331    K: ElementWiseKer<T, Params> + Clone,
332{
333    phantom: PhantomData<(K, T, Params)>,
334}
335
336impl<K, T, Params> ElementWise<T, Params> for ElementWiseImpl<K, T, Params>
337where
338    T: LADatum,
339    Params: Copy + Send + Sync + Debug + 'static + Default,
340    K: ElementWiseKer<T, Params> + Clone,
341{
342    fn name(&self) -> &'static str {
343        K::name()
344    }
345    fn run_with_params(&self, vec: &mut [T], params: Params) -> TractResult<()> {
346        map_slice_with_alignment(vec, |data| K::run(data, params), K::nr(), K::alignment_bytes())
347    }
348}
349
350pub trait ElementWiseKer<T, Params = ()>:
351    Send + Sync + Debug + dyn_clone::DynClone + Clone + 'static
352where
353    Params: Copy + Send + Sync + Debug + 'static + Default,
354    T: LADatum,
355{
356    fn name() -> &'static str;
357    fn alignment_bytes() -> usize {
358        Self::alignment_items() * T::datum_type().size_of()
359    }
360    fn alignment_items() -> usize;
361    fn nr() -> usize;
362    fn run(vec: &mut [T], params: Params);
363    fn ew() -> Box<dyn ElementWise<T, Params>> {
364        Box::new(ElementWiseImpl::<Self, T, Params>::new())
365    }
366}
367
368#[cfg(test)]
369pub mod test {
370    use crate::{LADatum, frame::element_wise::*};
371    use num_traits::AsPrimitive;
372    use proptest::test_runner::{TestCaseError, TestCaseResult};
373    use tract_data::internal::*;
374
375    /// Every finite `f16`, or a 1/4096 grid of `[-30, 30]` for wider types.
376    ///
377    /// The grid samples where the `f16` set is enumerated, but it reaches past every input
378    /// clamp the f32 kernels apply, so no input outside its bounds takes a path it has not
379    /// already exercised.
380    ///
381    /// The step has to stay fine because these invariants break on value-specific
382    /// rounding, not over a contiguous region: the Tanh kernels leave `[-1, 1]` only
383    /// inside a band about 0.3 wide, and a 1/256 grid steps clean over
384    /// `arm64simd_tanh_f32_4n`'s share of it.
385    fn invariant_sweep<T: LADatum>() -> Vec<T>
386    where
387        f32: AsPrimitive<T>,
388    {
389        if T::datum_type() == f16::datum_type() {
390            let all: Vec<f16> =
391                (0..=u16::MAX).map(f16::from_bits).filter(|x| x.is_finite()).collect();
392            let all = tensor1(&all).cast_to::<T>().unwrap().into_owned();
393            return all.try_as_plain().unwrap().as_slice::<T>().unwrap().to_vec();
394        }
395        (-30 * 4096..=30 * 4096).map(|i| (i as f32 / 4096.).as_()).collect()
396    }
397
398    /// Assert `invariant` holds of every `(input, output)` pair a kernel produces over
399    /// [`invariant_sweep`], reporting `expected` on the first pair that breaks it.
400    ///
401    /// The accuracy tests cannot stand in for this on the saturating tails: there the true
402    /// value is smaller than the rounding error of the kernels' own arithmetic, so an
403    /// output that violates the range or the sign still compares close to the reference.
404    pub fn test_element_wise_invariant<K: ElementWiseKer<T>, T: LADatum>(
405        expected: &str,
406        invariant: impl Fn(T, T) -> bool,
407    ) -> TestCaseResult
408    where
409        f32: AsPrimitive<T>,
410    {
411        crate::setup_test_logger();
412        let values = invariant_sweep::<T>();
413        let mut found = values.clone();
414        K::ew().run(&mut found).unwrap();
415        for (x, y) in values.iter().zip(found.iter()) {
416            proptest::prop_assert!(
417                invariant(*x, *y),
418                "{}({x:?}) returned {y:?}, expected {expected}",
419                K::name()
420            );
421        }
422        Ok(())
423    }
424
425    pub fn test_element_wise<K: ElementWiseKer<T, ()>, T: LADatum, F: Fn(T) -> T>(
426        values: &[T],
427        reference: F,
428    ) -> TestCaseResult {
429        test_element_wise_params::<K, T, F, ()>(values, reference, ())
430    }
431
432    pub fn test_element_wise_params<
433        K: ElementWiseKer<T, Params>,
434        T: LADatum,
435        F: Fn(T) -> T,
436        Params,
437    >(
438        values: &[T],
439        reference: F,
440        params: Params,
441    ) -> TestCaseResult
442    where
443        Params: Copy + Send + Sync + Debug + 'static + Default,
444    {
445        crate::setup_test_logger();
446        let op = ElementWiseImpl::<K, T, Params>::new();
447        let mut values = values.to_vec();
448        while values.len() < K::nr() {
449            values.push(T::zero());
450        }
451        let expected = values.iter().copied().map(reference).collect::<Vec<_>>();
452        let mut found = values;
453        op.run_with_params(&mut found, params).unwrap();
454        tensor1(&found)
455            .close_enough(&tensor1(&expected), true)
456            .map_err(|e| TestCaseError::fail(e.root_cause().to_string()))?;
457        Ok(())
458    }
459}