Skip to main content

strided_basic/
raw_ops.rs

1//! Allocation-free copy/axpy over borrowed raw strided layouts.
2//!
3//! The map/zip kernels build a traversal plan per call; for small replay
4//! copies that fixed cost dominates. These entry points take
5//! [`RawStridedRef`]/[`RawStridedMut`], fuse the stride pair into a
6//! stack-allocated loop nest, and run plain loops - no heap allocation on any
7//! call path with rank at most [`RAW_FUSED_RANK_LIMIT`]. Higher ranks fall
8//! back to the view-based kernels.
9
10use crate::ops_view::{axpy, copy_scale};
11use crate::{ElementOpApply, RawStridedMut, RawStridedRef, Result};
12use core::ops::{Add, Mul};
13
14use crate::maybe_sync::MaybeSendSync;
15
16/// Maximum rank fused on the stack before falling back to the view kernels.
17pub const RAW_FUSED_RANK_LIMIT: usize = 8;
18
19/// Stack-allocated fused stride pair (dims ordered by destination stride,
20/// adjacent contiguous axes merged). Built once and replayed by both the
21/// per-call raw kernels and the prepared [`crate::CopyPlan`].
22#[derive(Clone, Copy, Debug)]
23pub(crate) struct FusedPairLayout {
24    pub(crate) rank: usize,
25    pub(crate) dims: [usize; RAW_FUSED_RANK_LIMIT],
26    pub(crate) dst_strides: [isize; RAW_FUSED_RANK_LIMIT],
27    pub(crate) src_strides: [isize; RAW_FUSED_RANK_LIMIT],
28}
29
30pub(crate) fn fuse_pair_layout(
31    dims: &[usize],
32    dst_strides: &[isize],
33    src_strides: &[isize],
34) -> Option<FusedPairLayout> {
35    if dims.len() > RAW_FUSED_RANK_LIMIT {
36        return None;
37    }
38    let mut layout = FusedPairLayout {
39        rank: 0,
40        dims: [1; RAW_FUSED_RANK_LIMIT],
41        dst_strides: [0; RAW_FUSED_RANK_LIMIT],
42        src_strides: [0; RAW_FUSED_RANK_LIMIT],
43    };
44    for axis in 0..dims.len() {
45        if dims[axis] == 1 {
46            continue;
47        }
48        if dims[axis] == 0 {
49            return Some(FusedPairLayout {
50                rank: 1,
51                dims: [0; RAW_FUSED_RANK_LIMIT],
52                dst_strides: [0; RAW_FUSED_RANK_LIMIT],
53                src_strides: [0; RAW_FUSED_RANK_LIMIT],
54            });
55        }
56        let mut position = layout.rank;
57        while position > 0 && layout.dst_strides[position - 1] > dst_strides[axis] {
58            layout.dims[position] = layout.dims[position - 1];
59            layout.dst_strides[position] = layout.dst_strides[position - 1];
60            layout.src_strides[position] = layout.src_strides[position - 1];
61            position -= 1;
62        }
63        layout.dims[position] = dims[axis];
64        layout.dst_strides[position] = dst_strides[axis];
65        layout.src_strides[position] = src_strides[axis];
66        layout.rank += 1;
67    }
68    if layout.rank == 0 {
69        layout.rank = 1;
70        layout.dims[0] = 1;
71    }
72    let mut fused = 0usize;
73    for axis in 1..layout.rank {
74        // A merged extent that overflows (possible for a stride-0 broadcast
75        // whose element count exceeds usize) leaves the axes unfused.
76        let merged = isize::try_from(layout.dims[fused])
77            .ok()
78            .filter(|&extent| {
79                layout.dst_strides[fused].checked_mul(extent) == Some(layout.dst_strides[axis])
80                    && layout.src_strides[fused].checked_mul(extent)
81                        == Some(layout.src_strides[axis])
82            })
83            .and_then(|_| layout.dims[fused].checked_mul(layout.dims[axis]));
84        if let Some(merged) = merged {
85            layout.dims[fused] = merged;
86        } else {
87            fused += 1;
88            layout.dims[fused] = layout.dims[axis];
89            layout.dst_strides[fused] = layout.dst_strides[axis];
90            layout.src_strides[fused] = layout.src_strides[axis];
91        }
92    }
93    layout.rank = fused + 1;
94    Some(layout)
95}
96
97/// Number of logical elements covered by a fused layout.
98///
99/// Every constructor of a [`FusedPairLayout`] starts from dims whose product
100/// was checked by [`crate::kernel::total_len`], and fusion only merges axes
101/// with a checked product, so the product cannot overflow.
102#[inline]
103pub(crate) fn fused_total(layout: &FusedPairLayout) -> usize {
104    layout.dims[..layout.rank].iter().product()
105}
106
107/// Serial replay of a fused pair layout over borrowed raw views.
108///
109/// The destination layout need not be injective: runs are replayed in order,
110/// so accumulating callers such as [`axpy_raw`] keep their sequential
111/// semantics. Parallel replay is only offered by [`crate::CopyPlan`], whose
112/// compile step proves destination injectivity.
113pub(crate) fn apply_fused_pair<D, S, Apply, Op>(
114    dst: &mut RawStridedMut<'_, D>,
115    src: &RawStridedRef<'_, S>,
116    layout: &FusedPairLayout,
117    apply: Apply,
118    op: Op,
119) where
120    D: Copy,
121    S: Copy,
122    Apply: Fn(&mut D, S),
123    Op: Fn(S) -> S,
124{
125    let total = fused_total(layout);
126    if total == 0 {
127        return;
128    }
129    let src_ptr = src.data().as_ptr();
130    let src_base = src.offset();
131    let dst_base = dst.offset();
132    let dst_ptr = dst.data_mut().as_mut_ptr();
133    // SAFETY: `RawStridedRef`/`RawStridedMut` guarantee (by `new`, or by the
134    // caller of `new_unchecked`) that every offset reachable from their offset
135    // through their dims/strides lies inside their data. Callers pass a layout
136    // fused from exactly those dims/strides, so every logical index in
137    // `0..total` maps to in-bounds source and destination slots. The shared
138    // and exclusive borrows cannot overlap, and this is the only writer.
139    unsafe {
140        apply_fused_range(
141            dst_ptr, dst_base, src_ptr, src_base, layout, 0, total, &apply, &op,
142        );
143    }
144}
145
146/// Replay logical indices `start..start + len` of a fused pair layout, in
147/// column-major order of the fused axes (axis 0 fastest).
148///
149/// The start coordinate is decoded once; runs along axis 0 then advance the
150/// outer coordinates incrementally, so no per-element range checks or
151/// coordinate rebuilds remain in the hot loop.
152///
153/// # Safety
154///
155/// - `start + len` must not exceed [`fused_total`] of `layout`.
156/// - For every logical index `i` in the range, `dst_base` plus the fused
157///   destination offset of `i` must be an in-bounds, writable slot of the
158///   allocation behind `dst_ptr`, and likewise `src_base` plus the source
159///   offset must be an in-bounds, readable slot behind `src_ptr`.
160/// - No other thread may access the destination slots of this range while it
161///   runs, and the source slots must not be written concurrently. Destination
162///   and source slots must not overlap.
163#[allow(clippy::too_many_arguments)]
164pub(crate) unsafe fn apply_fused_range<D, S, Apply, Op>(
165    dst_ptr: *mut D,
166    dst_base: isize,
167    src_ptr: *const S,
168    src_base: isize,
169    layout: &FusedPairLayout,
170    start: usize,
171    len: usize,
172    apply: &Apply,
173    op: &Op,
174) where
175    D: Copy,
176    S: Copy,
177    Apply: Fn(&mut D, S),
178    Op: Fn(S) -> S,
179{
180    if len == 0 {
181        return;
182    }
183    let rank = layout.rank;
184    let inner_len = layout.dims[0];
185    let inner_dst = layout.dst_strides[0];
186    let inner_src = layout.src_strides[0];
187
188    // Decode the start coordinate once per range. `dst_offset`/`src_offset`
189    // track the offset of the current run start (outer coordinates plus the
190    // inner coordinate `inner_start`); every value is a reachable offset.
191    let mut index = [0usize; RAW_FUSED_RANK_LIMIT];
192    let mut rest = start;
193    let mut dst_outer = dst_base;
194    let mut src_outer = src_base;
195    for axis in 0..rank {
196        let dim = layout.dims[axis];
197        index[axis] = rest % dim;
198        rest /= dim;
199        if axis > 0 {
200            dst_outer += index[axis] as isize * layout.dst_strides[axis];
201            src_outer += index[axis] as isize * layout.src_strides[axis];
202        }
203    }
204    let mut inner_start = index[0];
205    let mut remaining = len;
206    loop {
207        let run = (inner_len - inner_start).min(remaining);
208        let dst_run = dst_outer + inner_start as isize * inner_dst;
209        let src_run = src_outer + inner_start as isize * inner_src;
210        // SAFETY: the run covers logical indices inside the caller's range, so
211        // by this function's contract every slot touched below is in-bounds,
212        // exclusively owned by this call (destination) and unaliased.
213        unsafe {
214            apply_run(
215                dst_ptr, dst_run, inner_dst, src_ptr, src_run, inner_src, run, apply, op,
216            )
217        };
218        remaining -= run;
219        if remaining == 0 {
220            return;
221        }
222        inner_start = 0;
223        // Advance an axis only when another position along it follows, and
224        // rewind it from its last position, so every intermediate base is a
225        // reachable offset of a validated layout (issue #243 follow-up): a
226        // layout ending within one stride of `isize::MAX` must not overflow.
227        // `remaining > 0` guarantees a following run exists, so the loop
228        // always finds an axis to advance before running out of rank.
229        let mut axis = 1;
230        while axis < rank {
231            if index[axis] + 1 < layout.dims[axis] {
232                index[axis] += 1;
233                dst_outer += layout.dst_strides[axis];
234                src_outer += layout.src_strides[axis];
235                break;
236            }
237            let last = (layout.dims[axis] - 1) as isize;
238            dst_outer -= last * layout.dst_strides[axis];
239            src_outer -= last * layout.src_strides[axis];
240            index[axis] = 0;
241            axis += 1;
242        }
243    }
244}
245
246/// One run along the fused inner axis.
247///
248/// # Safety
249///
250/// Every `dst_offset + k * dst_stride` and `src_offset + k * src_stride` for
251/// `k < len` must be an in-bounds slot as described in [`apply_fused_range`],
252/// with exclusive access to the destination slots.
253#[allow(clippy::too_many_arguments)]
254#[inline(always)]
255unsafe fn apply_run<D, S, Apply, Op>(
256    dst_ptr: *mut D,
257    dst_offset: isize,
258    dst_stride: isize,
259    src_ptr: *const S,
260    src_offset: isize,
261    src_stride: isize,
262    len: usize,
263    apply: &Apply,
264    op: &Op,
265) where
266    D: Copy,
267    S: Copy,
268    Apply: Fn(&mut D, S),
269    Op: Fn(S) -> S,
270{
271    if dst_stride == 1 {
272        // SAFETY: a unit destination stride makes the run `len` consecutive
273        // in-bounds slots starting at `dst_offset`, exclusively owned here.
274        let dst_run = unsafe { core::slice::from_raw_parts_mut(dst_ptr.offset(dst_offset), len) };
275        match src_stride {
276            1 => {
277                // SAFETY: `len` consecutive in-bounds source slots.
278                let src_run =
279                    unsafe { core::slice::from_raw_parts(src_ptr.offset(src_offset), len) };
280                for (dst, &value) in dst_run.iter_mut().zip(src_run) {
281                    apply(dst, op(value));
282                }
283            }
284            -1 => {
285                // SAFETY: the run reads `src_offset - (len - 1) ..= src_offset`,
286                // all in-bounds; the lowest one is the run's last source slot.
287                let src_run = unsafe {
288                    core::slice::from_raw_parts(
289                        src_ptr.offset(src_offset - (len as isize - 1)),
290                        len,
291                    )
292                };
293                for (dst, &value) in dst_run.iter_mut().zip(src_run.iter().rev()) {
294                    apply(dst, op(value));
295                }
296            }
297            _ => {
298                let src_start = unsafe { src_ptr.offset(src_offset) };
299                for (position, dst) in dst_run.iter_mut().enumerate() {
300                    // SAFETY: `position < len`, so this is a run source slot.
301                    let value = unsafe { *src_start.offset(position as isize * src_stride) };
302                    apply(dst, op(value));
303                }
304            }
305        }
306        return;
307    }
308    // SAFETY: the run start is an in-bounds slot of each allocation.
309    let dst_start = unsafe { dst_ptr.offset(dst_offset) };
310    let src_start = unsafe { src_ptr.offset(src_offset) };
311    for position in 0..len as isize {
312        // SAFETY: `position < len`, so both are slots of this run.
313        unsafe {
314            let value = *src_start.offset(position * src_stride);
315            apply(&mut *dst_start.offset(position * dst_stride), op(value));
316        }
317    }
318}
319
320fn ensure_same_dims(dst: &[usize], src: &[usize]) -> Result<()> {
321    if dst != src {
322        return Err(crate::StridedError::ShapeMismatch(
323            dst.to_vec(),
324            src.to_vec(),
325        ));
326    }
327    // Reject an element count beyond usize (a huge stride-0 broadcast)
328    // instead of replaying it.
329    crate::kernel::total_len(dst)?;
330    Ok(())
331}
332
333/// `dest = scale * src` over borrowed raw strided layouts.
334pub fn copy_scale_raw<T>(
335    dest: &mut RawStridedMut<'_, T>,
336    src: &RawStridedRef<'_, T>,
337    scale: T,
338) -> Result<()>
339where
340    T: Copy + Mul<T, Output = T> + ElementOpApply + MaybeSendSync,
341{
342    ensure_same_dims(dest.dims(), src.dims())?;
343    match fuse_pair_layout(dest.dims(), dest.strides(), src.strides()) {
344        Some(layout) => {
345            apply_fused_pair(
346                dest,
347                src,
348                &layout,
349                |dst, value| *dst = value,
350                |value: T| scale * value,
351            );
352            Ok(())
353        }
354        None => copy_scale(&mut dest.as_view_mut(), &src.as_view(), scale),
355    }
356}
357
358/// `dest = scale * conj(src)` over borrowed raw strided layouts.
359pub fn copy_scale_conj_raw<T>(
360    dest: &mut RawStridedMut<'_, T>,
361    src: &RawStridedRef<'_, T>,
362    scale: T,
363) -> Result<()>
364where
365    T: Copy + Mul<T, Output = T> + ElementOpApply + MaybeSendSync,
366{
367    ensure_same_dims(dest.dims(), src.dims())?;
368    match fuse_pair_layout(dest.dims(), dest.strides(), src.strides()) {
369        Some(layout) => {
370            apply_fused_pair(
371                dest,
372                src,
373                &layout,
374                |dst, value| *dst = value,
375                |value: T| scale * value.conj(),
376            );
377            Ok(())
378        }
379        None => copy_scale(&mut dest.as_view_mut(), &src.as_view().conj(), scale),
380    }
381}
382
383/// `dest = alpha * src + dest` over borrowed raw strided layouts.
384pub fn axpy_raw<T>(
385    dest: &mut RawStridedMut<'_, T>,
386    src: &RawStridedRef<'_, T>,
387    alpha: T,
388) -> Result<()>
389where
390    T: Copy + Add<T, Output = T> + Mul<T, Output = T> + ElementOpApply + MaybeSendSync,
391{
392    ensure_same_dims(dest.dims(), src.dims())?;
393    match fuse_pair_layout(dest.dims(), dest.strides(), src.strides()) {
394        Some(layout) => {
395            apply_fused_pair(
396                dest,
397                src,
398                &layout,
399                |dst, value| *dst = *dst + value,
400                |value: T| alpha * value,
401            );
402            Ok(())
403        }
404        None => axpy(&mut dest.as_view_mut(), &src.as_view(), alpha),
405    }
406}
407
408/// `dest = alpha * conj(src) + dest` over borrowed raw strided layouts.
409pub fn axpy_conj_raw<T>(
410    dest: &mut RawStridedMut<'_, T>,
411    src: &RawStridedRef<'_, T>,
412    alpha: T,
413) -> Result<()>
414where
415    T: Copy + Add<T, Output = T> + Mul<T, Output = T> + ElementOpApply + MaybeSendSync,
416{
417    ensure_same_dims(dest.dims(), src.dims())?;
418    match fuse_pair_layout(dest.dims(), dest.strides(), src.strides()) {
419        Some(layout) => {
420            apply_fused_pair(
421                dest,
422                src,
423                &layout,
424                |dst, value| *dst = *dst + value,
425                |value: T| alpha * value.conj(),
426            );
427            Ok(())
428        }
429        None => axpy(&mut dest.as_view_mut(), &src.as_view().conj(), alpha),
430    }
431}
432
433#[cfg(test)]
434#[path = "raw_ops/tests/tests.rs"]
435mod tests;