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