Skip to main content

nuvec_cpu/
lib.rs

1//! Pure-Rust CPU kernels for nuvec.
2//!
3//! Kernels work on typed slices and know nothing about arrays or graphs; `nuvec-core`
4//! dispatches to them. Inputs may have any layout (contiguous, transposed, sliced with
5//! negative steps, broadcast with zero strides); outputs are always row-contiguous.
6//!
7//! **Status:** pre-alpha. Element-wise, reduction and matrix-multiplication kernels.
8
9mod matmul;
10
11pub use matmul::{Gemm, MatView, matmul};
12
13/// The version of this crate.
14pub const VERSION: &str = env!("CARGO_PKG_VERSION");
15
16/// A strided view of elements: element `i` (a multi-index) lives at
17/// `data[offset + Σ i_k · strides_k]`.
18#[derive(Debug, Clone, Copy)]
19pub struct View<'a, T> {
20    /// The underlying storage.
21    pub data: &'a [T],
22    /// Position of the first element, in elements.
23    pub offset: isize,
24    /// Per-dimension steps, in elements. May be zero (broadcast) or negative.
25    pub strides: &'a [isize],
26}
27
28/// Applies `f` element-wise: `out[i] = f(a[i])` in row-major order of `shape`.
29///
30/// # Panics
31///
32/// If `out.len()` is not the element count of `shape`, or a view indexes outside its data.
33/// Both are caller bugs: `nuvec-core` validates shapes and layouts beforehand.
34pub fn map1<A: Copy, U>(shape: &[usize], a: View<'_, A>, out: &mut [U], f: impl Fn(A) -> U) {
35    for_rows(
36        shape,
37        [a.strides],
38        [a.offset],
39        out,
40        |row, [ba], [sa]| match lane(a.data, ba, sa, row.len()) {
41            Lane::Slice(x) => row.iter_mut().zip(x).for_each(|(o, &x)| *o = f(x)),
42            la => row
43                .iter_mut()
44                .enumerate()
45                .for_each(|(j, o)| *o = f(la.get(j))),
46        },
47    );
48}
49
50/// Applies `f` element-wise: `out[i] = f(a[i], b[i])` in row-major order of `shape`.
51///
52/// # Panics
53///
54/// As [`map1`].
55pub fn map2<A: Copy, B: Copy, U>(
56    shape: &[usize],
57    a: View<'_, A>,
58    b: View<'_, B>,
59    out: &mut [U],
60    f: impl Fn(A, B) -> U,
61) {
62    for_rows(
63        shape,
64        [a.strides, b.strides],
65        [a.offset, b.offset],
66        out,
67        |row, [ba, bb], [sa, sb]| {
68            let n = row.len();
69            match (lane(a.data, ba, sa, n), lane(b.data, bb, sb, n)) {
70                (Lane::Slice(x), Lane::Slice(y)) => {
71                    row.iter_mut()
72                        .zip(x)
73                        .zip(y)
74                        .for_each(|((o, &x), &y)| *o = f(x, y));
75                }
76                (Lane::Slice(x), Lane::Repeat(y)) => {
77                    row.iter_mut().zip(x).for_each(|(o, &x)| *o = f(x, y))
78                }
79                (Lane::Repeat(x), Lane::Slice(y)) => {
80                    row.iter_mut().zip(y).for_each(|(o, &y)| *o = f(x, y))
81                }
82                (la, lb) => row
83                    .iter_mut()
84                    .enumerate()
85                    .for_each(|(j, o)| *o = f(la.get(j), lb.get(j))),
86            }
87        },
88    );
89}
90
91/// Applies `f` element-wise: `out[i] = f(a[i], b[i], c[i])` in row-major order of `shape`.
92///
93/// # Panics
94///
95/// As [`map1`].
96pub fn map3<A: Copy, B: Copy, C: Copy, U>(
97    shape: &[usize],
98    a: View<'_, A>,
99    b: View<'_, B>,
100    c: View<'_, C>,
101    out: &mut [U],
102    f: impl Fn(A, B, C) -> U,
103) {
104    let strides = [a.strides, b.strides, c.strides];
105    for_rows(
106        shape,
107        strides,
108        [a.offset, b.offset, c.offset],
109        out,
110        |row, [ba, bb, bc], [sa, sb, sc]| {
111            let n = row.len();
112            match (
113                lane(a.data, ba, sa, n),
114                lane(b.data, bb, sb, n),
115                lane(c.data, bc, sc, n),
116            ) {
117                (Lane::Slice(x), Lane::Slice(y), Lane::Slice(z)) => {
118                    row.iter_mut()
119                        .zip(x)
120                        .zip(y)
121                        .zip(z)
122                        .for_each(|(((o, &x), &y), &z)| *o = f(x, y, z));
123                }
124                (la, lb, lc) => row
125                    .iter_mut()
126                    .enumerate()
127                    .for_each(|(j, o)| *o = f(la.get(j), lb.get(j), lc.get(j))),
128            }
129        },
130    );
131}
132
133/// One operand's elements along an output row.
134enum Lane<'a, T> {
135    /// Contiguous (stride 1).
136    Slice(&'a [T]),
137    /// Broadcast (stride 0).
138    Repeat(T),
139    /// Any other stride.
140    Strided {
141        data: &'a [T],
142        start: isize,
143        step: isize,
144    },
145}
146
147impl<T: Copy> Lane<'_, T> {
148    fn get(&self, j: usize) -> T {
149        match self {
150            Self::Slice(x) => x[j],
151            Self::Repeat(x) => *x,
152            Self::Strided { data, start, step } => data[index(start + j as isize * step)],
153        }
154    }
155}
156
157/// The lane of `n` elements starting at `base` with `step`. Bounds are checked once here
158/// for contiguous lanes.
159fn lane<T: Copy>(data: &[T], base: isize, step: isize, n: usize) -> Lane<'_, T> {
160    match step {
161        1 => Lane::Slice(&data[index(base)..index(base) + n]),
162        0 => Lane::Repeat(data[index(base)]),
163        _ => Lane::Strided {
164            data,
165            start: base,
166            step,
167        },
168    }
169}
170
171/// Calls `row_fn` for each row of the (collapsed) output, in row-major order, with every
172/// operand's offset at the row start and its stride along the row.
173fn for_rows<const N: usize, U>(
174    shape: &[usize],
175    strides: [&[isize]; N],
176    offsets: [isize; N],
177    out: &mut [U],
178    mut row_fn: impl FnMut(&mut [U], [isize; N], [isize; N]),
179) {
180    let size: usize = shape.iter().product();
181    assert_eq!(out.len(), size, "output length does not match shape");
182    if size == 0 {
183        return;
184    }
185    let (shape, strides) = collapse_dims(shape, strides);
186    let ndim = shape.len();
187    if ndim == 0 {
188        row_fn(out, offsets, [0; N]);
189        return;
190    }
191
192    let inner = shape[ndim - 1];
193    let inner_strides: [isize; N] = std::array::from_fn(|k| strides[k][ndim - 1]);
194    let mut position = vec![0usize; ndim - 1];
195    let mut base = offsets;
196    for row in out.chunks_exact_mut(inner) {
197        row_fn(row, base, inner_strides);
198        // Advance the odometer over the outer dimensions.
199        for d in (0..ndim - 1).rev() {
200            position[d] += 1;
201            for k in 0..N {
202                base[k] += strides[k][d];
203            }
204            if position[d] < shape[d] {
205                break;
206            }
207            for k in 0..N {
208                base[k] -= strides[k][d] * shape[d] as isize;
209            }
210            position[d] = 0;
211        }
212    }
213}
214
215/// Folds blocks of elements: `out[i]` is the fold of the `reduced` block at output position
216/// `i`, starting from `init`. `kept` and `reduced` are `(shape, strides)` pairs that split the
217/// input's dimensions; the output is row-major over `kept`.
218///
219/// Contiguous blocks are passed whole to `block`; other blocks are folded element by element
220/// with `f`, which also receives each element's row-major index within its block. Both must
221/// compute the same fold.
222///
223/// # Panics
224///
225/// As [`map1`].
226#[allow(clippy::too_many_arguments)]
227pub fn reduce<T: Copy, A: Clone>(
228    kept: (&[usize], &[isize]),
229    reduced: (&[usize], &[isize]),
230    data: &[T],
231    offset: isize,
232    out: &mut [A],
233    init: A,
234    f: impl Fn(A, T, usize) -> A,
235    block: impl Fn(A, &[T]) -> A,
236) {
237    let (shape, [strides]) = collapse_dims(reduced.0, [reduced.1]);
238    let n: usize = shape.iter().product();
239    let contiguous = shape.len() == 1 && strides[0] == 1;
240    let fold = |base: isize| -> A {
241        let mut acc = init.clone();
242        if n == 0 {
243            return acc;
244        }
245        if contiguous || shape.is_empty() {
246            return block(acc, &data[index(base)..index(base) + n]);
247        }
248        let mut position = vec![0usize; shape.len()];
249        let mut cursor = base;
250        for i in 0..n {
251            acc = f(acc, data[index(cursor)], i);
252            for d in (0..shape.len()).rev() {
253                position[d] += 1;
254                cursor += strides[d];
255                if position[d] < shape[d] {
256                    break;
257                }
258                cursor -= strides[d] * shape[d] as isize;
259                position[d] = 0;
260            }
261        }
262        acc
263    };
264    for_rows(kept.0, [kept.1], [offset], out, |row, [base], [step]| {
265        for (j, o) in row.iter_mut().enumerate() {
266            *o = fold(base + j as isize * step);
267        }
268    });
269}
270
271fn index(offset: isize) -> usize {
272    usize::try_from(offset).expect("view offset is negative")
273}
274
275/// Merges adjacent dimensions that are contiguous for every operand and drops unit
276/// dimensions, so that common layouts iterate as one flat loop.
277///
278/// Dimension `d` merges into `d + 1` when `strides[d] == strides[d + 1] * shape[d + 1]`
279/// holds for every operand.
280pub fn collapse_dims<const N: usize>(
281    shape: &[usize],
282    strides: [&[isize]; N],
283) -> (Vec<usize>, [Vec<isize>; N]) {
284    let mut out_shape: Vec<usize> = Vec::with_capacity(shape.len());
285    let mut out_strides: [Vec<isize>; N] = std::array::from_fn(|_| Vec::with_capacity(shape.len()));
286    for d in 0..shape.len() {
287        if shape[d] == 1 {
288            continue;
289        }
290        let mergeable = out_shape.last().is_some() && {
291            let last = out_strides[0].len() - 1;
292            (0..N).all(|k| out_strides[k][last] == strides[k][d] * shape[d] as isize)
293        };
294        if mergeable {
295            let last = out_shape.len() - 1;
296            out_shape[last] *= shape[d];
297            for k in 0..N {
298                out_strides[k][last] = strides[k][d];
299            }
300        } else {
301            out_shape.push(shape[d]);
302            for k in 0..N {
303                out_strides[k].push(strides[k][d]);
304            }
305        }
306    }
307    (out_shape, out_strides)
308}
309
310#[cfg(test)]
311mod tests {
312    use super::*;
313
314    #[test]
315    fn version_is_set() {
316        assert!(!VERSION.is_empty());
317    }
318
319    #[test]
320    fn contiguous_binary() {
321        let mut out = [0i32; 6];
322        let s = [3, 1];
323        let a = View {
324            data: &[1, 2, 3, 4, 5, 6],
325            offset: 0,
326            strides: &s,
327        };
328        let b = View {
329            data: &[10, 20, 30, 40, 50, 60],
330            offset: 0,
331            strides: &s,
332        };
333        map2(&[2, 3], a, b, &mut out, |x, y| x + y);
334        assert_eq!(out, [11, 22, 33, 44, 55, 66]);
335    }
336
337    #[test]
338    fn transposed_and_broadcast() {
339        // a = [[1,2,3],[4,5,6]] read transposed (3x2); b = [10, 20] broadcast over rows.
340        let mut out = [0i32; 6];
341        let a = View {
342            data: &[1, 2, 3, 4, 5, 6],
343            offset: 0,
344            strides: &[1, 3],
345        };
346        let b = View {
347            data: &[10, 20],
348            offset: 0,
349            strides: &[0, 1],
350        };
351        map2(&[3, 2], a, b, &mut out, |x, y| x + y);
352        assert_eq!(out, [11, 24, 12, 25, 13, 26]);
353    }
354
355    #[test]
356    fn negative_strides_and_offsets() {
357        // Reverse of [0, 1, 2, 3, 4], every second element: [4, 2, 0].
358        let mut out = [0u8; 3];
359        let a = View {
360            data: &[0, 1, 2, 3, 4],
361            offset: 4,
362            strides: &[-2],
363        };
364        map1(&[3], a, &mut out, |x| x);
365        assert_eq!(out, [4, 2, 0]);
366    }
367
368    #[test]
369    fn scalar_and_empty() {
370        let mut out = [0f32; 1];
371        map1(
372            &[],
373            View {
374                data: &[2.5],
375                offset: 0,
376                strides: &[],
377            },
378            &mut out,
379            |x| x * 2.0,
380        );
381        assert_eq!(out, [5.0]);
382        let mut none: [f32; 0] = [];
383        map1(
384            &[3, 0],
385            View {
386                data: &[],
387                offset: 0,
388                strides: &[0, 1],
389            },
390            &mut none,
391            |x: f32| x,
392        );
393    }
394
395    #[test]
396    fn ternary() {
397        let mut out = [0i32; 3];
398        let s = [1];
399        map3(
400            &[3],
401            View {
402                data: &[1u8, 0, 1],
403                offset: 0,
404                strides: &s,
405            },
406            View {
407                data: &[1, 2, 3],
408                offset: 0,
409                strides: &s,
410            },
411            View {
412                data: &[-1],
413                offset: 0,
414                strides: &[0],
415            },
416            &mut out,
417            |c, x, y| if c != 0 { x } else { y },
418        );
419        assert_eq!(out, [1, -1, 3]);
420    }
421
422    #[test]
423    fn reduce_over_middle_axis_of_transposed_layout() {
424        // Logical a[i][j][k] = data[k*6 + j*2 + i] for shape [2, 3, 4] (fully transposed).
425        let data: Vec<i32> = (0..24).collect();
426        let (shape, strides) = ([2usize, 3, 4], [1isize, 2, 6]);
427        let mut out = [0i32; 8];
428        // Keep axes 0 and 2, reduce axis 1.
429        reduce(
430            (&[shape[0], shape[2]], &[strides[0], strides[2]]),
431            (&[shape[1]], &[strides[1]]),
432            &data,
433            0,
434            &mut out,
435            0,
436            |acc, x, _| acc + x,
437            |acc, xs| xs.iter().fold(acc, |a, &x| a + x),
438        );
439        let expected: Vec<i32> = (0..2)
440            .flat_map(|i| (0..4).map(move |k| (0..3).map(|j| k * 6 + j * 2 + i).sum()))
441            .collect();
442        assert_eq!(out.to_vec(), expected);
443    }
444
445    #[test]
446    fn reduce_passes_block_index_and_handles_empty() {
447        let pick = |acc: (i32, usize), x: i32, i: usize| if x > acc.0 { (x, i) } else { acc };
448        let block = |acc, xs: &[i32]| xs.iter().enumerate().fold(acc, |a, (i, &x)| pick(a, x, i));
449        // Contiguous block (passed whole) and a strided one (element by element).
450        let mut out = [(0, 0usize); 2];
451        reduce(
452            (&[2], &[4]),
453            (&[4], &[1]),
454            &[5, 9, 1, 7, 3, 2, 8, 0],
455            0,
456            &mut out,
457            (i32::MIN, 0),
458            pick,
459            block,
460        );
461        assert_eq!(out, [(9, 1), (8, 2)]);
462        reduce(
463            (&[2], &[1]),
464            (&[4], &[2]),
465            &[5, 9, 1, 7, 3, 2, 8, 0],
466            0,
467            &mut out,
468            (i32::MIN, 0),
469            pick,
470            block,
471        );
472        assert_eq!(out, [(8, 3), (9, 0)]);
473        let mut sums = [7i32; 3];
474        let add = |a: i32, x: i32, _| a + x;
475        reduce(
476            (&[3], &[1]),
477            (&[0], &[1]),
478            &[0i32; 0],
479            0,
480            &mut sums,
481            0,
482            add,
483            |a, xs| xs.iter().fold(a, |a, &x| a + x),
484        );
485        assert_eq!(sums, [0, 0, 0]);
486    }
487
488    #[test]
489    fn collapse_merges_contiguous_dims() {
490        let (shape, [s]) = collapse_dims(&[2, 3, 4], [&[12, 4, 1]]);
491        assert_eq!((shape, s), (vec![24], vec![1]));
492        let (shape, [s]) = collapse_dims(&[2, 1, 3], [&[3, 99, 1]]);
493        assert_eq!((shape, s), (vec![6], vec![1]));
494        let (shape, [a, b]) = collapse_dims(&[2, 3], [&[3, 1], &[0, 1]]);
495        assert_eq!((shape, a, b), (vec![2, 3], vec![3, 1], vec![0, 1]));
496    }
497}