Skip to main content

strided_basic/
copy_plan.rs

1//! Prepared (compile-once, execute-many) copy plans over raw strided layouts.
2//!
3//! [`copy_scale_raw`](crate::copy_scale_raw) and friends rebuild the fused
4//! loop nest on every call; for prepared-replay consumers that issue many
5//! small copies with a fixed layout, that per-call planning dominates
6//! (see issue #139). [`CopyPlan`] splits the work: [`CopyPlan::compile`]
7//! validates the layout pair and builds the fused traversal once,
8//! [`CopyPlan::execute`]/[`CopyPlan::execute_scale`]/[`CopyPlan::execute_conj`]
9//! replay it with no planning and no heap allocation for fused ranks at most
10//! [`RAW_FUSED_RANK_LIMIT`](crate::RAW_FUSED_RANK_LIMIT).
11
12use core::{
13    marker::PhantomData,
14    mem::MaybeUninit,
15    ops::{Add, Mul},
16};
17
18use crate::raw_ops::{
19    apply_fused_range, apply_spilled_range, fuse_pair_traversal, fused_total, FusedAxis,
20    FusedPairLayout, PairTraversal,
21};
22use crate::{ElementOpApply, MaybeSendSync, RawStridedMut, RawStridedRef, Result, StridedError};
23
24// Same pattern as map_view.rs / outer_product.rs: stack storage when the
25// parallel feature pulls in smallvec, plain Vec otherwise. Only `compile`
26// touches these; `execute*` never allocates for them. (A spilled fused layout
27// needs one cursor allocation per replayed range, so a parallel `execute*` can
28// allocate once per worker range.)
29#[cfg(feature = "parallel")]
30type AxisVec<T> = smallvec::SmallVec<[T; crate::RAW_FUSED_RANK_LIMIT]>;
31#[cfg(not(feature = "parallel"))]
32type AxisVec<T> = Vec<T>;
33
34pub(crate) trait OverwriteWriter<T> {
35    fn dims(&self) -> &[usize];
36    fn strides(&self) -> &[isize];
37    fn offset(&self) -> isize;
38    /// # Safety
39    /// The caller must use the pointer only for the validated allocation and
40    /// logical layout represented by this writer.
41    unsafe fn data_ptr(&mut self) -> *mut T;
42    /// # Safety
43    /// The offset must be an in-bounds logical destination proven by layout.
44    unsafe fn write_at(&mut self, offset: isize, value: T);
45}
46
47pub(crate) trait ReadModifyWrite<T>: OverwriteWriter<T> {
48    /// # Safety
49    /// The offset must be an in-bounds initialized slot covered by the
50    /// traversal's copy and disjointness proof.
51    unsafe fn add_at<F>(&mut self, offset: isize, value: T, combine: F)
52    where
53        F: FnOnce(T, T) -> T;
54}
55
56impl<'a, T> OverwriteWriter<T> for RawStridedMut<'a, T> {
57    fn dims(&self) -> &[usize] {
58        self.dims()
59    }
60    fn strides(&self) -> &[isize] {
61        self.strides()
62    }
63    fn offset(&self) -> isize {
64        self.offset()
65    }
66    unsafe fn data_ptr(&mut self) -> *mut T {
67        self.data_mut().as_mut_ptr()
68    }
69    unsafe fn write_at(&mut self, offset: isize, value: T) {
70        // SAFETY: the prepared layout validates every logical destination.
71        unsafe { self.data_mut().as_mut_ptr().offset(offset).write(value) }
72    }
73}
74
75impl<'a, T> ReadModifyWrite<T> for RawStridedMut<'a, T>
76where
77    T: Add<Output = T>,
78{
79    #[inline(always)]
80    unsafe fn add_at<F>(&mut self, offset: isize, value: T, combine: F)
81    where
82        F: FnOnce(T, T) -> T,
83    {
84        // SAFETY: the copy or initialized caller proves this logical slot.
85        unsafe {
86            let ptr = self.data_mut().as_mut_ptr().offset(offset);
87            ptr.write(combine(ptr.read(), value));
88        }
89    }
90}
91
92impl<'a, T> OverwriteWriter<T> for RawStridedMut<'a, MaybeUninit<T>> {
93    fn dims(&self) -> &[usize] {
94        self.dims()
95    }
96    fn strides(&self) -> &[isize] {
97        self.strides()
98    }
99    fn offset(&self) -> isize {
100        self.offset()
101    }
102    unsafe fn data_ptr(&mut self) -> *mut T {
103        self.data_mut().as_mut_ptr().cast()
104    }
105    unsafe fn write_at(&mut self, offset: isize, value: T) {
106        // SAFETY: the prepared layout validates every logical destination.
107        unsafe {
108            self.data_mut()
109                .as_mut_ptr()
110                .offset(offset)
111                .write(MaybeUninit::new(value))
112        }
113    }
114}
115
116pub(crate) struct InitializedRawDest<'a, T> {
117    ptr: *mut T,
118    extent: usize,
119    dims: &'a [usize],
120    strides: &'a [isize],
121    offset: isize,
122    _marker: PhantomData<&'a mut [MaybeUninit<T>]>,
123}
124
125impl<'a, T> OverwriteWriter<T> for InitializedRawDest<'a, T> {
126    fn dims(&self) -> &[usize] {
127        self.dims
128    }
129    fn strides(&self) -> &[isize] {
130        self.strides
131    }
132    fn offset(&self) -> isize {
133        self.offset
134    }
135    unsafe fn data_ptr(&mut self) -> *mut T {
136        self.ptr
137    }
138    unsafe fn write_at(&mut self, offset: isize, value: T) {
139        debug_assert!(offset >= 0 && (offset as usize) < self.extent);
140        // SAFETY: the copy proof and extent check cover this logical slot.
141        unsafe { self.ptr.offset(offset).write(value) }
142    }
143}
144
145impl<'a, T> ReadModifyWrite<T> for InitializedRawDest<'a, T>
146where
147    T: Add<Output = T>,
148{
149    #[inline(always)]
150    unsafe fn add_at<F>(&mut self, offset: isize, value: T, combine: F)
151    where
152        F: FnOnce(T, T) -> T,
153    {
154        debug_assert!(offset >= 0 && (offset as usize) < self.extent);
155        // SAFETY: the copy proof and extent check cover this logical slot.
156        unsafe {
157            let ptr = self.ptr.offset(offset);
158            ptr.write(combine(ptr.read(), value));
159        }
160    }
161}
162
163/// A compiled copy traversal for one `(dims, dst_strides, src_strides)`
164/// layout pair.
165///
166/// `compile` proves the layout facts once (rank agreement, extent overflow,
167/// destination injectivity) and fuses/orders the loop nest; each `execute*`
168/// call then only re-checks the per-call facts (that the supplied views carry
169/// exactly the compiled layout) before replaying the prepared loops.
170///
171/// Overlapping `src`/`dest` memory is not supported, matching the rest of the
172/// crate.
173///
174/// Ranks above [`RAW_FUSED_RANK_LIMIT`](crate::RAW_FUSED_RANK_LIMIT) are supported by the same fused
175/// traversal, which then stores its axes in one rank-bounded allocation.
176///
177/// # Example
178///
179/// ```rust
180/// use strided_basic::{CopyPlan, RawStridedMut, RawStridedRef};
181///
182/// let dims = [2usize, 3];
183/// let src_strides = [3isize, 1];
184/// let dst_strides = [1isize, 2]; // transposed destination
185/// let plan = CopyPlan::compile(&dims, &dst_strides, &src_strides).unwrap();
186///
187/// let src = [0.0f64, 1.0, 2.0, 10.0, 11.0, 12.0];
188/// let mut dst = [0.0f64; 6];
189/// let src_ref = RawStridedRef::new(&src, &dims, &src_strides, 0).unwrap();
190/// let mut dst_mut = RawStridedMut::new(&mut dst, &dims, &dst_strides, 0).unwrap();
191/// plan.execute(&mut dst_mut, &src_ref).unwrap();
192/// assert_eq!(dst, [0.0, 10.0, 1.0, 11.0, 2.0, 12.0]);
193/// ```
194#[derive(Clone, Debug)]
195pub struct CopyPlan {
196    dims: AxisVec<usize>,
197    dst_strides: AxisVec<isize>,
198    src_strides: AxisVec<isize>,
199    fused: PairTraversal,
200}
201
202impl CopyPlan {
203    /// `None` when the fused traversal is stored in the rank-bounded spill.
204    #[cfg(feature = "parallel")]
205    #[inline]
206    pub(crate) fn fused_layout(&self) -> Option<&FusedPairLayout> {
207        self.fused.inline()
208    }
209
210    pub(crate) fn execute_uninit_then<'a, T, R>(
211        &self,
212        dest: &'a mut RawStridedMut<'a, MaybeUninit<T>>,
213        src: &RawStridedRef<'_, T>,
214        f: impl for<'b> FnOnce(InitializedRawDest<'b, T>) -> R,
215    ) -> Result<R>
216    where
217        T: Copy + MaybeSendSync,
218    {
219        self.execute_uninit(dest, src)?;
220        let data = dest.data_mut();
221        let receipt = InitializedRawDest {
222            ptr: data.as_mut_ptr().cast(),
223            extent: data.len(),
224            dims: dest.dims(),
225            strides: dest.strides(),
226            offset: dest.offset(),
227            _marker: PhantomData,
228        };
229        Ok(f(receipt))
230    }
231
232    /// Compile a copy plan for the given layout pair.
233    ///
234    /// Performs the layout validation and traversal construction
235    /// (fuse + order) once:
236    ///
237    /// - `dims`, `dst_strides`, and `src_strides` must have equal length
238    ///   ([`StridedError::StrideLengthMismatch`]);
239    /// - the total element count must not overflow `usize`
240    ///   ([`StridedError::OffsetOverflow`]);
241    /// - the destination layout must be injective, i.e. map distinct logical
242    ///   indices to distinct offsets
243    ///   ([`StridedError::NonInjectiveOutputLayout`]).
244    pub fn compile(dims: &[usize], dst_strides: &[isize], src_strides: &[isize]) -> Result<Self> {
245        if dims.len() != dst_strides.len() || dims.len() != src_strides.len() {
246            return Err(StridedError::StrideLengthMismatch);
247        }
248        // A zero-sized layout has zero elements even when its other extents
249        // overflow; only a nonempty overflowing count is rejected.
250        crate::kernel::total_len(dims)?;
251        if !crate::layout_check::is_injective_layout(dims, dst_strides) {
252            return Err(StridedError::NonInjectiveOutputLayout);
253        }
254        Ok(Self {
255            dims: dims.into(),
256            dst_strides: dst_strides.into(),
257            src_strides: src_strides.into(),
258            fused: fuse_pair_traversal(dims, dst_strides, src_strides),
259        })
260    }
261
262    /// Check the per-call facts: the supplied views must carry exactly the
263    /// compiled layout. Buffer bounds against that layout were already proven
264    /// by [`RawStridedRef::new`]/[`RawStridedMut::new`] (or asserted by the
265    /// caller of the `new_unchecked` constructors), so layout equality is the
266    /// complete precondition for the pointer-based fused replay below.
267    fn check_call<D, S>(
268        &self,
269        dest: &RawStridedMut<'_, D>,
270        src: &RawStridedRef<'_, S>,
271    ) -> Result<()> {
272        if dest.dims() != &self.dims[..]
273            || src.dims() != &self.dims[..]
274            || dest.strides() != &self.dst_strides[..]
275            || src.strides() != &self.src_strides[..]
276        {
277            return Err(StridedError::PlanLayoutMismatch);
278        }
279        Ok(())
280    }
281
282    /// `dest = src` into a potentially uninitialized destination.
283    ///
284    /// On success every reachable logical destination element is initialized.
285    /// Non-reachable holes in the backing allocation are not written.
286    pub fn execute_uninit<T>(
287        &self,
288        dest: &mut RawStridedMut<'_, MaybeUninit<T>>,
289        src: &RawStridedRef<'_, T>,
290    ) -> Result<()>
291    where
292        T: Copy + MaybeSendSync,
293    {
294        self.check_call(dest, src)?;
295        replay_fused(
296            dest,
297            src,
298            &self.fused,
299            |dst, value| {
300                dst.write(value);
301            },
302            |value| value,
303        );
304        Ok(())
305    }
306
307    /// `dest = src`. Allocation-free for fused ranks at most
308    /// [`RAW_FUSED_RANK_LIMIT`](crate::RAW_FUSED_RANK_LIMIT).
309    pub fn execute<T>(
310        &self,
311        dest: &mut RawStridedMut<'_, T>,
312        src: &RawStridedRef<'_, T>,
313    ) -> Result<()>
314    where
315        T: Copy + MaybeSendSync,
316    {
317        self.check_call(dest, src)?;
318        replay_fused(
319            dest,
320            src,
321            &self.fused,
322            |dst, value| *dst = value,
323            |value: T| value,
324        );
325        Ok(())
326    }
327
328    /// `dest = scale * src`. Allocation-free for fused ranks at most
329    /// [`RAW_FUSED_RANK_LIMIT`](crate::RAW_FUSED_RANK_LIMIT).
330    pub fn execute_scale<T>(
331        &self,
332        dest: &mut RawStridedMut<'_, T>,
333        src: &RawStridedRef<'_, T>,
334        scale: T,
335    ) -> Result<()>
336    where
337        T: Copy + Mul<T, Output = T> + MaybeSendSync,
338    {
339        self.check_call(dest, src)?;
340        replay_fused(
341            dest,
342            src,
343            &self.fused,
344            |dst, value| *dst = value,
345            |value: T| scale * value,
346        );
347        Ok(())
348    }
349
350    /// `dest = conj(src)`. Allocation-free for fused ranks at most
351    /// [`RAW_FUSED_RANK_LIMIT`](crate::RAW_FUSED_RANK_LIMIT).
352    pub fn execute_conj<T>(
353        &self,
354        dest: &mut RawStridedMut<'_, T>,
355        src: &RawStridedRef<'_, T>,
356    ) -> Result<()>
357    where
358        T: Copy + ElementOpApply + MaybeSendSync,
359    {
360        self.check_call(dest, src)?;
361        replay_fused(
362            dest,
363            src,
364            &self.fused,
365            |dst, value| *dst = value,
366            |value: T| value.conj(),
367        );
368        Ok(())
369    }
370}
371
372/// Replay a compiled fused layout, splitting it across worker threads when the
373/// repository threshold and the active execution policy allow it.
374///
375/// Parallel replay is sound only because [`CopyPlan::compile`] proved the
376/// destination layout injective: disjoint logical ranges then write disjoint
377/// destination slots. The logical index space `0..total` (column-major over
378/// the fused axes) is split into contiguous worker ranges, which divides the
379/// outer fused axes and, when there are fewer outer positions than workers,
380/// also chunks long inner runs (for example a rank-1 contiguous copy).
381fn replay_fused<D, S, Apply, Op>(
382    dest: &mut RawStridedMut<'_, D>,
383    src: &RawStridedRef<'_, S>,
384    traversal: &PairTraversal,
385    apply: Apply,
386    op: Op,
387) where
388    D: Copy + MaybeSendSync,
389    S: Copy + MaybeSendSync,
390    Apply: Fn(&mut D, S) + MaybeSendSync,
391    Op: Fn(S) -> S + MaybeSendSync,
392{
393    let src_ptr = src.data().as_ptr();
394    let src_base = src.offset();
395    let dst_base = dest.offset();
396    let dst_ptr = dest.data_mut().as_mut_ptr();
397    // SAFETY: `check_call` proved both views carry exactly the compiled
398    // layout, and `RawStridedRef`/`RawStridedMut` guarantee every offset
399    // reachable through that layout lies inside their data. `traversal` is the
400    // fusion of that layout, so every logical index in `0..total` is an
401    // in-bounds slot of both. The compiled destination layout is injective
402    // and the exclusive destination borrow cannot overlap the shared source
403    // borrow.
404    unsafe {
405        match traversal {
406            PairTraversal::Inline(layout) => {
407                replay_fused_raw(dst_ptr, dst_base, src_ptr, src_base, layout, apply, op)
408            }
409            PairTraversal::Spilled(axes) => {
410                replay_spilled_raw(dst_ptr, dst_base, src_ptr, src_base, axes, apply, op)
411            }
412        }
413    }
414}
415
416/// Pointer-level fused replay shared by [`CopyPlan`] and the dynamic slice
417/// plans, which replay a fused window layout at a runtime base offset.
418///
419/// Splits `0..total` across workers when the repository threshold and the
420/// active execution policy allow more than one thread, otherwise runs the
421/// serial kernel directly.
422///
423/// # Safety
424///
425/// Every logical index of `layout`, mapped through its destination strides
426/// from `dst_base` and its source strides from `src_base`, must be an
427/// in-bounds slot of the destination and source allocations. The
428/// destination strides must be injective over `layout`, the destination
429/// must not overlap the source, and nothing else may access the destination
430/// slots during the call.
431pub(crate) unsafe fn replay_fused_raw<D, S, Apply, Op>(
432    dst_ptr: *mut D,
433    dst_base: isize,
434    src_ptr: *const S,
435    src_base: isize,
436    layout: &FusedPairLayout,
437    apply: Apply,
438    op: Op,
439) where
440    D: Copy + MaybeSendSync,
441    S: Copy + MaybeSendSync,
442    Apply: Fn(&mut D, S) + MaybeSendSync,
443    Op: Fn(S) -> S + MaybeSendSync,
444{
445    let total = fused_total(layout);
446    if total == 0 {
447        return;
448    }
449    #[cfg(feature = "parallel")]
450    {
451        let nthreads = crate::threading::parallel_threads_for_len(total);
452        if nthreads > 1 {
453            let dst_ptr = crate::threading::SendPtr(dst_ptr);
454            let src_ptr = crate::threading::SendPtr(src_ptr as *mut S);
455            crate::threading::parallel_for_each(0..total, nthreads, &|range| {
456                // SAFETY: the caller contract makes every index in bounds; the
457                // worker ranges are disjoint and the destination strides
458                // injective, so no two workers write the same slot, and the
459                // source is only read.
460                unsafe {
461                    apply_fused_range(
462                        dst_ptr.as_ptr(),
463                        dst_base,
464                        src_ptr.as_const(),
465                        src_base,
466                        layout,
467                        range.start,
468                        range.len(),
469                        &apply,
470                        &op,
471                    );
472                }
473            });
474            return;
475        }
476    }
477    // SAFETY: forwarded caller contract over the full range `0..total`.
478    unsafe {
479        apply_fused_range(
480            dst_ptr, dst_base, src_ptr, src_base, layout, 0, total, &apply, &op,
481        );
482    }
483}
484
485/// Pointer-level replay of a spilled axis list.
486///
487/// Mirrors [`replay_fused_raw`] including its worker split; kept separate so
488/// that the inline replay keeps the shape it had before spilled layouts
489/// existed.
490///
491/// # Safety
492///
493/// Every logical index of `axes`, mapped through its destination strides from
494/// `dst_base` and its source strides from `src_base`, must be an in-bounds slot
495/// of the destination and source allocations. The destination strides must be
496/// injective over `axes`, the destination must not overlap the source, and
497/// nothing else may access the destination slots during the call.
498pub(crate) unsafe fn replay_spilled_raw<D, S, Apply, Op>(
499    dst_ptr: *mut D,
500    dst_base: isize,
501    src_ptr: *const S,
502    src_base: isize,
503    axes: &[FusedAxis],
504    apply: Apply,
505    op: Op,
506) where
507    D: Copy + MaybeSendSync,
508    S: Copy + MaybeSendSync,
509    Apply: Fn(&mut D, S) + MaybeSendSync,
510    Op: Fn(S) -> S + MaybeSendSync,
511{
512    let total = axes.iter().map(|axis| axis.dim).product::<usize>();
513    if total == 0 {
514        return;
515    }
516    #[cfg(feature = "parallel")]
517    {
518        let nthreads = crate::threading::parallel_threads_for_len(total);
519        if nthreads > 1 {
520            let dst_ptr = crate::threading::SendPtr(dst_ptr);
521            let src_ptr = crate::threading::SendPtr(src_ptr as *mut S);
522            crate::threading::parallel_for_each(0..total, nthreads, &|range| {
523                // SAFETY: the caller contract makes every index in bounds; the
524                // worker ranges are disjoint and the destination strides
525                // injective, so no two workers write the same slot, and the
526                // source is only read.
527                unsafe {
528                    apply_spilled_range(
529                        dst_ptr.as_ptr(),
530                        dst_base,
531                        src_ptr.as_const(),
532                        src_base,
533                        axes,
534                        range.start,
535                        range.len(),
536                        &apply,
537                        &op,
538                    );
539                }
540            });
541            return;
542        }
543    }
544    // SAFETY: forwarded caller contract over the full range `0..total`.
545    unsafe {
546        apply_spilled_range(
547            dst_ptr, dst_base, src_ptr, src_base, axes, 0, total, &apply, &op,
548        );
549    }
550}
551
552#[cfg(test)]
553#[path = "copy_plan/tests/tests.rs"]
554mod tests;
555
556#[cfg(all(test, feature = "parallel"))]
557#[path = "copy_plan/tests/parallel_tests.rs"]
558mod parallel_tests;