Skip to main content

tract_linalg/frame/
unicast.rs

1use std::fmt::Debug;
2use std::marker::PhantomData;
3
4use tract_data::TractResult;
5use tract_data::internal::TensorView;
6
7use crate::frame::element_wise_helper::TempBuffer;
8use crate::{BinFn, LADatum};
9
10// A unicast binary 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 a unicast routine: the kernel, its registry descriptor and its accuracy tests, from one
14/// statement. `op` is what the kernel computes, which is what the tests compare against, and `isa`
15/// says which machines may run it, and therefore which may test it.
16macro_rules! routine_unicast_rust {
17    (arm; $($rest:tt)*) => { routine_unicast_rust!(@ arm, target_arch = "arm"; $($rest)*); };
18    (aarch64; $($rest:tt)*) => {
19        routine_unicast_rust!(@ aarch64, target_arch = "aarch64"; $($rest)*);
20    };
21    (x86_64; $($rest:tt)*) => {
22        routine_unicast_rust!(@ x86_64, target_arch = "x86_64"; $($rest)*);
23    };
24    (riscv64; $($rest:tt)*) => {
25        routine_unicast_rust!(@ riscv64, target_arch = "riscv64"; $($rest)*);
26    };
27    (wasm32; $($rest:tt)*) => {
28        routine_unicast_rust!(@ wasm32,
29            all(target_arch = "wasm32", target_feature = "simd128"); $($rest)*);
30    };
31    (generic; $($rest:tt)*) => { routine_unicast_rust!(@ generic, all(); $($rest)*); };
32
33    (@ $arch:ident, $built:meta; $ti:ident, $ker:ident, $nr:expr, $alignment_items:expr,
34     $run:item, op($op:ident) $(, isa($($isa:ident),+))?) => {
35        unicast_kernel!(@ $built; $ti, $ker, $nr, $alignment_items, $run);
36        paste! {
37            submit_routine!($arch; [<Bin $ti:upper>], BinUnicast($op), $ker $(, isa($($isa),+))?);
38            #[cfg(test)]
39            mod [<test_ $ker:snake>] {
40                use super::*;
41                unicast_frame_tests!(
42                    cfg!($built)
43                        && $crate::isa::IsaReq::ANY
44                            $(.needing(&[$($crate::isa::Isa::$isa),+]))?
45                            .satisfied_by($crate::isa::native()),
46                    $ti,
47                    $ker,
48                    bin_reference!($op)
49                );
50            }
51        }
52    };
53}
54
55macro_rules! unicast_kernel {
56    (arm; $($rest:tt)*) => { unicast_kernel!(@ target_arch = "arm"; $($rest)*); };
57    (aarch64; $($rest:tt)*) => { unicast_kernel!(@ target_arch = "aarch64"; $($rest)*); };
58    (x86_64; $($rest:tt)*) => { unicast_kernel!(@ target_arch = "x86_64"; $($rest)*); };
59    (riscv64; $($rest:tt)*) => { unicast_kernel!(@ target_arch = "riscv64"; $($rest)*); };
60    (wasm32; $($rest:tt)*) => { unicast_kernel!(@ all(target_arch = "wasm32", target_feature = "simd128"); $($rest)*); };
61
62    (@ $built:meta; $ti:ident, $func:ident, $nr:expr, $alignment_items:expr, $run:item) => {
63        #[cfg($built)]
64        unicast_kernel!($ti, $func, $nr, $alignment_items, $run);
65        #[cfg(not($built))]
66        unicast_kernel!($ti, $func, $nr, $alignment_items,
67            fn run(_a: &mut [$ti], _b: &[$ti]) {
68                panic!(concat!(stringify!($func), ": kernel not built for this target"))
69            }
70        );
71    };
72
73    ($ti: ident, $func: ident, $nr: expr, $alignment_items: expr, $run: item) => {
74        paste! {
75            #[derive(Copy, Clone, Debug)]
76            #[allow(non_camel_case_types)]
77            pub struct $func;
78
79            impl crate::frame::unicast::UnicastKer<$ti> for $func {
80                #[inline(always)]
81                fn name() -> &'static str {
82                    stringify!($func)
83                }
84                #[inline(always)]
85                fn nr() -> usize {
86                    $nr
87                }
88                #[inline(always)]
89                fn alignment_items() -> usize {
90                    $alignment_items
91                }
92                $run
93            }
94        }
95    };
96}
97
98pub trait Unicast<T>: Send + Sync + Debug + dyn_clone::DynClone
99where
100    T: Copy + Debug + PartialEq + Send + Sync,
101{
102    fn name(&self) -> &'static str;
103    fn run(&self, a: &mut [T], b: &[T]) -> TractResult<()>;
104}
105
106dyn_clone::clone_trait_object!(<T> Unicast<T> where T: Copy);
107
108#[derive(Debug, Clone, new)]
109pub struct UnicastImpl<K, T>
110where
111    T: LADatum,
112    K: UnicastKer<T> + Clone,
113{
114    phantom: PhantomData<(K, T)>,
115}
116
117impl<K, T> UnicastImpl<K, T>
118where
119    T: LADatum,
120    K: UnicastKer<T> + Clone,
121{
122}
123impl<K, T> Unicast<T> for UnicastImpl<K, T>
124where
125    T: LADatum,
126    K: UnicastKer<T> + Clone,
127{
128    fn name(&self) -> &'static str {
129        K::name()
130    }
131    fn run(&self, a: &mut [T], b: &[T]) -> TractResult<()> {
132        unicast_with_alignment(a, b, |a, b| K::run(a, b), K::nr(), K::alignment_bytes())
133    }
134}
135
136pub trait UnicastKer<T>: Send + Sync + Debug + dyn_clone::DynClone + Clone + 'static
137where
138    T: LADatum,
139{
140    fn name() -> &'static str;
141    fn alignment_bytes() -> usize {
142        Self::alignment_items() * T::datum_type().size_of()
143    }
144    fn alignment_items() -> usize;
145    fn nr() -> usize;
146    fn run(a: &mut [T], b: &[T]);
147    fn bin() -> Box<BinFn> {
148        Box::new(|a: &mut TensorView, b: &TensorView| {
149            let a_slice = a.as_slice_mut()?;
150            let b_slice = b.as_slice()?;
151            UnicastImpl::<Self, T>::new().run(a_slice, b_slice)
152        })
153    }
154}
155
156std::thread_local! {
157    static TMP: std::cell::RefCell<(TempBuffer, TempBuffer)> = std::cell::RefCell::new((TempBuffer::default(), TempBuffer::default()));
158}
159
160pub(crate) fn unicast_with_alignment<T>(
161    a: &mut [T],
162    b: &[T],
163    f: impl Fn(&mut [T], &[T]),
164    nr: usize,
165    alignment_bytes: usize,
166) -> TractResult<()>
167where
168    T: LADatum,
169{
170    if a.is_empty() {
171        return Ok(());
172    }
173    unsafe {
174        TMP.with(|buffers| {
175            let mut buffers = buffers.borrow_mut();
176            buffers.0.ensure(nr * T::datum_type().size_of(), alignment_bytes);
177            buffers.1.ensure(nr * T::datum_type().size_of(), alignment_bytes);
178            let tmp_a = std::slice::from_raw_parts_mut(buffers.0.buffer as *mut T, nr);
179            let tmp_b = std::slice::from_raw_parts_mut(buffers.1.buffer as *mut T, nr);
180            let mut compute_via_temp_buffer = |a: &mut [T], b: &[T]| {
181                tmp_a[..a.len()].copy_from_slice(a);
182                tmp_b[..b.len()].copy_from_slice(b);
183                f(tmp_a, tmp_b);
184                a.copy_from_slice(&tmp_a[..a.len()])
185            };
186
187            let mut num_element_processed = 0;
188            let a_prefix_len = a.as_ptr().align_offset(alignment_bytes).min(a.len());
189            let b_prefix_len = b.as_ptr().align_offset(alignment_bytes).min(b.len());
190            assert!(
191                a_prefix_len == b_prefix_len,
192                "Both inputs should be of the same alignement, got {a_prefix_len:?}, {b_prefix_len:?}"
193            );
194            let mut applied_prefix_len = 0;
195            if a_prefix_len > 0 {
196                // Incomplete tile needs to be created to process unaligned data.
197                let sub_a = &mut a[..a_prefix_len];
198                let sub_b = &b[..a_prefix_len];
199                compute_via_temp_buffer(sub_a, sub_b);
200                num_element_processed += a_prefix_len;
201                applied_prefix_len = a_prefix_len;
202            }
203
204            let num_complete_tiles = (a.len() - applied_prefix_len) / nr;
205            if num_complete_tiles > 0 {
206                // Process all tiles that are complete.
207                let sub_a = &mut a[applied_prefix_len..][..(num_complete_tiles * nr)];
208                let sub_b = &b[applied_prefix_len..][..(num_complete_tiles * nr)];
209                f(sub_a, sub_b);
210                num_element_processed += num_complete_tiles * nr;
211            }
212
213            if num_element_processed < a.len() {
214                // Incomplete tile needs to be created to process remaining elements.
215                compute_via_temp_buffer(
216                    &mut a[num_element_processed..],
217                    &b[num_element_processed..],
218                );
219            }
220        })
221    }
222    Ok(())
223}
224
225#[cfg(test)]
226#[macro_use]
227pub mod test {
228    use super::*;
229    use crate::LADatum;
230    use proptest::test_runner::{TestCaseError, TestCaseResult};
231    use tract_data::internal::*;
232    use tract_num_traits::{AsPrimitive, Float};
233
234    pub fn test_unicast<K: UnicastKer<T>, T: LADatum>(
235        a: &mut [T],
236        b: &[T],
237        reference: impl Fn(T, T) -> T,
238    ) -> TestCaseResult {
239        crate::setup_test_logger();
240        let op = UnicastImpl::<K, T>::new();
241        let expected = a.iter().zip(b.iter()).map(|(a, b)| (reference)(*a, *b)).collect::<Vec<_>>();
242        op.run(a, b).unwrap();
243        tensor1(a)
244            .close_enough(&tensor1(&expected), true)
245            .map_err(|e| TestCaseError::fail(e.root_cause().to_string()))?;
246        Ok(())
247    }
248
249    pub fn test_unicast_t<K: UnicastKer<T>, T: LADatum + Float>(
250        a: &[f32],
251        b: &[f32],
252        func: impl Fn(T, T) -> T,
253    ) -> TestCaseResult
254    where
255        f32: AsPrimitive<T>,
256    {
257        crate::setup_test_logger();
258        let vec_a: Vec<T> = a.iter().copied().map(|x| x.as_()).collect();
259        // We allocate a tensor to ensure allocation is done with alignement
260        let mut a = unsafe { Tensor::from_slice_align(vec_a.as_slice(), vector_size()).unwrap() };
261        let vec_b: Vec<T> = b.iter().copied().map(|x| x.as_()).collect();
262        // We allocate a tensor to ensure allocation is done with alignement
263        let b = unsafe { Tensor::from_slice_align(vec_b.as_slice(), vector_size()).unwrap() };
264        crate::frame::unicast::test::test_unicast::<K, _>(
265            a.try_as_plain_mut().unwrap().as_slice_mut::<T>().unwrap(),
266            b.try_as_plain().unwrap().as_slice::<T>().unwrap(),
267            func,
268        )
269    }
270
271    #[macro_export]
272    macro_rules! unicast_frame_tests {
273        ($cond:expr, $t: ty, $ker:ty, $func:expr) => {
274            pastey::paste! {
275                proptest::proptest! {
276                    #[test]
277                    fn [<prop_ $ker:snake>](
278                        (a, b) in proptest::strategy::Strategy::prop_flat_map(
279                            0..100_usize,
280                            |len| (vec![-25f32..25.0; len], vec![-25f32..25.0; len])
281                        )
282                    ) {
283                        if $cond {
284                            $crate::frame::unicast::test::test_unicast_t::<$ker, $t>(&*a, &*b, $func).unwrap()
285                        }
286                    }
287                }
288
289                #[test]
290                fn [<empty_ $ker:snake>]() {
291                    if $cond {
292                        $crate::frame::unicast::test::test_unicast_t::<$ker, $t>(&[], &[], $func).unwrap()
293                    }
294                }
295            }
296        };
297    }
298}