Skip to main content

strided_basic/
dense_update.rs

1//! Dense column-major update and structural kernels.
2//!
3//! AXPBY and triangular masking are adapted from tenferro-rs d8759f43,
4//! `crates/tenferro-cpu/src/{blas1,structural}.rs` (MIT OR Apache-2.0).
5//! Tensor allocation/placement stay with the caller. Diagonal embedding uses
6//! the existing strided map traversal rather than tenferro's coordinate loop.
7
8use core::mem::MaybeUninit;
9use core::ops::{Add, Mul};
10
11use crate::{MaybeSendSync, Result, StridedError, StridedView, StridedViewMut};
12
13fn same_len(actual: usize, expected: usize) -> Result<()> {
14    if actual == expected {
15        Ok(())
16    } else {
17        Err(StridedError::ShapeMismatch(vec![actual], vec![expected]))
18    }
19}
20
21fn product(shape: &[usize]) -> Result<usize> {
22    if shape.contains(&0) {
23        return Ok(0);
24    }
25    shape.iter().try_fold(1usize, |n, &d| {
26        n.checked_mul(d).ok_or(StridedError::OffsetOverflow)
27    })
28}
29
30fn for_each_chunk<T: MaybeSendSync, F: Fn(usize, &mut [T]) + crate::MaybeSync>(
31    output: &mut [T],
32    operation: F,
33) {
34    #[cfg(feature = "parallel")]
35    {
36        let threads = crate::threading::parallel_threads_for_len(output.len());
37        if threads > 1 {
38            let ptr = crate::threading::SendPtr(output.as_mut_ptr());
39            crate::threading::parallel_for_each(0..output.len(), threads, &|range| {
40                // SAFETY: the scoped scheduler partitions the exclusive output
41                // borrow into disjoint ranges; all tasks finish before return.
42                let chunk = unsafe {
43                    core::slice::from_raw_parts_mut(ptr.as_ptr().add(range.start), range.len())
44                };
45                operation(range.start, chunk);
46            });
47            return;
48        }
49    }
50    operation(0, output);
51}
52
53/// Source offset of a linear batch index within `shape[2..]`.
54fn batch_source_offset(mut batch: usize, shape: &[usize], strides: &[isize]) -> isize {
55    let mut offset = 0isize;
56    for (&dim, &stride) in shape.iter().zip(strides) {
57        let coord = batch % dim;
58        batch /= dim;
59        offset += coord as isize * stride;
60    }
61    offset
62}
63
64/// Borrow `data` as a dense column-major view of `shape`.
65fn dense_view<'a, T>(data: &'a [T], shape: &[usize]) -> Result<StridedView<'a, T>> {
66    let len = product(shape)?;
67    same_len(data.len(), len)?;
68    let strides = crate::col_major_strides(shape);
69    StridedView::new(data, shape, &strides, 0)
70}
71
72/// Copy `dst.len()` source elements starting at reachable byte-free offset
73/// `base` with element step `stride`.
74fn copy_strided_run<T: Copy>(dst: &mut [MaybeUninit<T>], data: &[T], base: isize, stride: isize) {
75    if stride == 1 {
76        // INVARIANT: `base` is reachable inside a view-validated buffer, so
77        // the run start plus its length stays in bounds.
78        let start = base as usize;
79        let src = &data[start..start + dst.len()];
80        for (slot, &value) in dst.iter_mut().zip(src) {
81            slot.write(value);
82        }
83    } else {
84        let mut offset = base;
85        for slot in dst.iter_mut() {
86            slot.write(data[offset as usize]);
87            offset += stride;
88        }
89    }
90}
91
92/// Update caller-owned contiguous storage: `y = alpha * x + beta * y`.
93///
94/// This is an accumulation operation: it reads the old `y`, even for beta=0
95/// (including ordinary IEEE NaN propagation). No temporary tensor is allocated.
96/// Execution obeys the active execution policy and shared parallel threshold.
97///
98/// # Examples
99/// ```
100/// let mut y = [3.0, 4.0];
101/// strided_basic::axpby_accum(&mut y, &[1.0, 2.0], 2.0, 3.0).unwrap();
102/// assert_eq!(y, [11.0, 16.0]);
103/// ```
104/// # Errors
105/// Returns `ShapeMismatch` for unequal slice lengths, before modifying `y`.
106pub fn axpby_accum<T>(y: &mut [T], x: &[T], alpha: T, beta: T) -> Result<()>
107where
108    T: Copy + Send + Sync + Add<Output = T> + Mul<Output = T>,
109{
110    same_len(x.len(), y.len())?;
111    for_each_chunk(y, |start, dst| {
112        let len = dst.len();
113        for (out, &src) in dst.iter_mut().zip(&x[start..start + len]) {
114            *out = alpha * src + beta * *out;
115        }
116    });
117    Ok(())
118}
119
120/// Copy dense column-major matrices, replacing the masked triangle with `fill`.
121///
122/// The first two dimensions are rows and columns; remaining dimensions are
123/// batches. `upper=true` keeps `row <= column-k`, otherwise the lower triangle.
124/// Every output element is initialized on success; old output is never read.
125///
126/// # Examples
127/// ```
128/// use core::mem::MaybeUninit;
129/// let mut out = [MaybeUninit::uninit(); 4];
130/// strided_basic::triangular_mask_into_uninit(
131///     &mut out, &[1, 2, 3, 4], &[2, 2], 0, false, 0).unwrap();
132/// // SAFETY: successful full-overwrite kernel initialized every element.
133/// assert_eq!(out.map(|v| unsafe { v.assume_init() }), [1, 2, 0, 4]);
134/// ```
135/// # Errors
136/// Returns `RankMismatch`, `ShapeMismatch`, or `OffsetOverflow` for invalid
137/// shape/buffer lengths, before writing output.
138pub fn triangular_mask_into_uninit<T: Copy + MaybeSendSync>(
139    output: &mut [MaybeUninit<T>],
140    input: &[T],
141    shape: &[usize],
142    k: i64,
143    upper: bool,
144    fill: T,
145) -> Result<()> {
146    let src = dense_view(input, shape)?;
147    triangular_mask_view_into_uninit(output, &src, k, upper, fill)
148}
149
150/// Mask a triangular region of a strided input into caller-owned contiguous
151/// storage.
152///
153/// The source layout is given by `src`; reads follow its strides and offset, so
154/// a reversed, transposed or otherwise strided source needs no staging copy.
155/// Kept elements come from `src`; masked elements receive `fill`. Every output
156/// element is written exactly once and the masked region of the source is
157/// never read.
158///
159/// # Errors
160/// Returns `RankMismatch` or `ShapeMismatch` for invalid shapes and buffer
161/// lengths, and `OffsetOverflow` for unrepresentable layouts, before writing.
162pub fn triangular_mask_view_into_uninit<T: Copy + MaybeSendSync>(
163    output: &mut [MaybeUninit<T>],
164    src: &StridedView<'_, T>,
165    k: i64,
166    upper: bool,
167    fill: T,
168) -> Result<()> {
169    let shape = src.dims();
170    if shape.len() < 2 {
171        return Err(StridedError::RankMismatch(shape.len(), 2));
172    }
173    let len = product(shape)?;
174    same_len(output.len(), len)?;
175    if len == 0 {
176        return Ok(());
177    }
178    let rows = shape[0];
179    let cols = shape[1];
180    let data = src.data();
181    let strides = src.strides();
182    let offset = src.offset();
183    for_each_chunk(output, |start, chunk| {
184        let mut pos = 0;
185        let mut batch = usize::MAX;
186        let mut batch_offset = 0isize;
187        while pos < chunk.len() {
188            let flat = start + pos;
189            let row = flat % rows;
190            let col = (flat / rows) % cols;
191            // INVARIANT: `product(shape)` succeeded, so `rows * cols` fits.
192            let run_batch = flat / (rows * cols);
193            if run_batch != batch {
194                batch = run_batch;
195                batch_offset = batch_source_offset(run_batch, &shape[2..], &strides[2..]);
196            }
197            let count = (rows - row).min(chunk.len() - pos);
198            let boundary = col as i128 - k as i128;
199            let split = if upper {
200                boundary.saturating_add(1)
201            } else {
202                boundary
203            }
204            .clamp(row as i128, (row + count) as i128) as usize
205                - row;
206            let (low, high) = chunk[pos..pos + count].split_at_mut(split);
207            let (kept, first_row, masked) = if upper {
208                (low, row, high)
209            } else {
210                (high, row + split, low)
211            };
212            let base = offset
213                + batch_offset
214                + (col as isize) * strides[1]
215                + (first_row as isize) * strides[0];
216            // Full overwrite: never read masked input or copy it only to
217            // overwrite it a second time. Each output element is written once.
218            copy_strided_run(kept, data, base, strides[0]);
219            masked.fill(MaybeUninit::new(fill));
220            pos += count;
221        }
222    });
223    Ok(())
224}
225
226/// Embed a dense column-major input on a diagonal in a new axis.
227///
228/// Insert an axis of size `shape[axis]` at `insert_axis`. Its coordinate must
229/// equal the original `axis` coordinate; other entries receive `zero`.
230/// Output is fully initialized without reading its previous contents. The
231/// active execution policy governs fill and strided diagonal-copy traversal.
232///
233/// # Examples
234/// ```
235/// use core::mem::MaybeUninit;
236/// let mut out = [MaybeUninit::uninit(); 4];
237/// strided_basic::embed_diagonal_into_uninit(&mut out, &[3, 5], &[2], 0, 1, 0).unwrap();
238/// // SAFETY: successful full-overwrite kernel initialized every element.
239/// assert_eq!(out.map(|v| unsafe { v.assume_init() }), [3, 0, 0, 5]);
240/// ```
241/// # Errors
242/// Returns `InvalidAxis`, `ShapeMismatch`, or `OffsetOverflow` for invalid
243/// axes, lengths or unrepresentable layouts. Destination layout validation
244/// may return `NonInjectiveOutputLayout` if injectivity cannot be established.
245pub fn embed_diagonal_into_uninit<T: Copy + MaybeSendSync + 'static>(
246    output: &mut [MaybeUninit<T>],
247    input: &[T],
248    shape: &[usize],
249    axis: usize,
250    insert_axis: usize,
251    zero: T,
252) -> Result<()> {
253    if axis >= shape.len() {
254        return Err(StridedError::InvalidAxis {
255            axis,
256            rank: shape.len(),
257        });
258    }
259    if insert_axis > shape.len() {
260        return Err(StridedError::InvalidAxis {
261            axis: insert_axis,
262            rank: shape.len() + 1,
263        });
264    }
265    let len = product(shape)?;
266    same_len(input.len(), len)?;
267    let src = dense_view(input, shape)?;
268    embed_diagonal_view_into_uninit(output, &src, axis, insert_axis, zero)
269}
270
271/// Embed a strided input on a diagonal in a new axis of caller-owned
272/// contiguous storage.
273///
274/// The source layout is given by `src`; the diagonal copy follows its strides
275/// and offset, so a strided source needs no staging copy. Off-diagonal entries
276/// receive `zero`, which is part of the result rather than scratch
277/// initialization.
278///
279/// # Errors
280/// Returns `InvalidAxis`, `ShapeMismatch`, or `OffsetOverflow` for invalid
281/// axes, lengths or unrepresentable layouts. Destination layout validation may
282/// return `NonInjectiveOutputLayout` if injectivity cannot be established.
283pub fn embed_diagonal_view_into_uninit<T: Copy + MaybeSendSync + 'static>(
284    output: &mut [MaybeUninit<T>],
285    src: &StridedView<'_, T>,
286    axis: usize,
287    insert_axis: usize,
288    zero: T,
289) -> Result<()> {
290    let shape = src.dims();
291    if axis >= shape.len() {
292        return Err(StridedError::InvalidAxis {
293            axis,
294            rank: shape.len(),
295        });
296    }
297    if insert_axis > shape.len() {
298        return Err(StridedError::InvalidAxis {
299            axis: insert_axis,
300            rank: shape.len() + 1,
301        });
302    }
303    let len = product(shape)?;
304    let out_len = len
305        .checked_mul(shape[axis])
306        .ok_or(StridedError::OffsetOverflow)?;
307    same_len(output.len(), out_len)?;
308    if out_len == 0 {
309        return Ok(());
310    }
311    isize::try_from(out_len).map_err(|_| StridedError::OffsetOverflow)?;
312    let mut out_shape: crate::InlineVec<usize> = shape.iter().copied().collect();
313    out_shape.insert(insert_axis, shape[axis]);
314    let mut dst_strides = crate::col_major_strides(&out_shape);
315    let inserted_stride = dst_strides.remove(insert_axis);
316    dst_strides[axis] = dst_strides[axis]
317        .checked_add(inserted_stride)
318        .ok_or(StridedError::OffsetOverflow)?;
319    let mut dst = StridedViewMut::new(output, shape, &dst_strides, 0)?;
320    crate::map_view::validate_destination_layout_without_alloc(shape, &dst_strides)?;
321    // INVARIANT: off-diagonal zeros are part of the mathematical result, not
322    // scratch initialization. Only the diagonal subset is overwritten below.
323    for_each_chunk(dst.data_mut(), |_, chunk| {
324        chunk.fill(MaybeUninit::new(zero))
325    });
326    crate::map_into(&mut dst, src, MaybeUninit::new)
327}