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 num_traits::{One, Zero};
19
20use crate::raw_ops::{
21    apply_fused_range, apply_spilled_range, fuse_pair_traversal, fused_total, FusedAxis,
22    FusedPairLayout, PairTraversal,
23};
24use crate::{ElementOpApply, MaybeSendSync, RawStridedMut, RawStridedRef, Result, StridedError};
25
26// Same pattern as map_view.rs / outer_product.rs: inline storage in both
27// feature configurations. Only `compile` touches these; `execute*` never
28// allocates for them. (A spilled fused layout needs one cursor allocation per
29// replayed range, so an `execute*` can allocate once per worker range above the
30// inline rank bound.)
31type AxisVec<T> = crate::inline::InlineVec<T>;
32
33pub(crate) trait OverwriteWriter<T> {
34    fn dims(&self) -> &[usize];
35    fn strides(&self) -> &[isize];
36    fn offset(&self) -> isize;
37    /// # Safety
38    /// The caller must use the pointer only for the validated allocation and
39    /// logical layout represented by this writer.
40    unsafe fn data_ptr(&mut self) -> *mut T;
41    /// # Safety
42    /// The offset must be an in-bounds logical destination proven by layout.
43    unsafe fn write_at(&mut self, offset: isize, value: T);
44}
45
46pub(crate) trait ReadModifyWrite<T>: OverwriteWriter<T> {
47    /// # Safety
48    /// The offset must be an in-bounds initialized slot covered by the
49    /// traversal's copy and disjointness proof.
50    unsafe fn add_at<F>(&mut self, offset: isize, value: T, combine: F)
51    where
52        F: FnOnce(T, T) -> T;
53}
54
55impl<'a, T> OverwriteWriter<T> for RawStridedMut<'a, T> {
56    fn dims(&self) -> &[usize] {
57        self.dims()
58    }
59    fn strides(&self) -> &[isize] {
60        self.strides()
61    }
62    fn offset(&self) -> isize {
63        self.offset()
64    }
65    unsafe fn data_ptr(&mut self) -> *mut T {
66        self.data_mut().as_mut_ptr()
67    }
68    unsafe fn write_at(&mut self, offset: isize, value: T) {
69        // SAFETY: the prepared layout validates every logical destination.
70        unsafe { self.data_mut().as_mut_ptr().offset(offset).write(value) }
71    }
72}
73
74impl<'a, T> ReadModifyWrite<T> for RawStridedMut<'a, T>
75where
76    T: Add<Output = T>,
77{
78    #[inline(always)]
79    unsafe fn add_at<F>(&mut self, offset: isize, value: T, combine: F)
80    where
81        F: FnOnce(T, T) -> T,
82    {
83        // SAFETY: the copy or initialized caller proves this logical slot.
84        unsafe {
85            let ptr = self.data_mut().as_mut_ptr().offset(offset);
86            ptr.write(combine(ptr.read(), value));
87        }
88    }
89}
90
91impl<'a, T> OverwriteWriter<T> for RawStridedMut<'a, MaybeUninit<T>> {
92    fn dims(&self) -> &[usize] {
93        self.dims()
94    }
95    fn strides(&self) -> &[isize] {
96        self.strides()
97    }
98    fn offset(&self) -> isize {
99        self.offset()
100    }
101    unsafe fn data_ptr(&mut self) -> *mut T {
102        self.data_mut().as_mut_ptr().cast()
103    }
104    unsafe fn write_at(&mut self, offset: isize, value: T) {
105        // SAFETY: the prepared layout validates every logical destination.
106        unsafe {
107            self.data_mut()
108                .as_mut_ptr()
109                .offset(offset)
110                .write(MaybeUninit::new(value))
111        }
112    }
113}
114
115pub(crate) struct InitializedRawDest<'a, T> {
116    ptr: *mut T,
117    extent: usize,
118    dims: &'a [usize],
119    strides: &'a [isize],
120    offset: isize,
121    _marker: PhantomData<&'a mut [MaybeUninit<T>]>,
122}
123
124impl<'a, T> OverwriteWriter<T> for InitializedRawDest<'a, T> {
125    fn dims(&self) -> &[usize] {
126        self.dims
127    }
128    fn strides(&self) -> &[isize] {
129        self.strides
130    }
131    fn offset(&self) -> isize {
132        self.offset
133    }
134    unsafe fn data_ptr(&mut self) -> *mut T {
135        self.ptr
136    }
137    unsafe fn write_at(&mut self, offset: isize, value: T) {
138        debug_assert!(offset >= 0 && (offset as usize) < self.extent);
139        // SAFETY: the copy proof and extent check cover this logical slot.
140        unsafe { self.ptr.offset(offset).write(value) }
141    }
142}
143
144impl<'a, T> ReadModifyWrite<T> for InitializedRawDest<'a, T>
145where
146    T: Add<Output = T>,
147{
148    #[inline(always)]
149    unsafe fn add_at<F>(&mut self, offset: isize, value: T, combine: F)
150    where
151        F: FnOnce(T, T) -> T,
152    {
153        debug_assert!(offset >= 0 && (offset as usize) < self.extent);
154        // SAFETY: the copy proof and extent check cover this logical slot.
155        unsafe {
156            let ptr = self.ptr.offset(offset);
157            ptr.write(combine(ptr.read(), value));
158        }
159    }
160}
161
162/// A compiled copy traversal for one `(dims, dst_strides, src_strides)`
163/// layout pair.
164///
165/// `compile` proves the layout facts once (rank agreement, extent overflow,
166/// destination injectivity) and fuses/orders the loop nest; each `execute*`
167/// call then only re-checks the per-call facts (that the supplied views carry
168/// exactly the compiled layout) before replaying the prepared loops.
169///
170/// Overlapping `src`/`dest` memory is not supported, matching the rest of the
171/// crate.
172///
173/// Ranks above [`RAW_FUSED_RANK_LIMIT`](crate::RAW_FUSED_RANK_LIMIT) are supported by the same fused
174/// traversal, which then stores its axes in one rank-bounded allocation.
175///
176/// # Example
177///
178/// ```rust
179/// use strided_basic::{CopyPlan, RawStridedMut, RawStridedRef};
180///
181/// let dims = [2usize, 3];
182/// let src_strides = [3isize, 1];
183/// let dst_strides = [1isize, 2]; // transposed destination
184/// let plan = CopyPlan::compile(&dims, &dst_strides, &src_strides).unwrap();
185///
186/// let src = [0.0f64, 1.0, 2.0, 10.0, 11.0, 12.0];
187/// let mut dst = [0.0f64; 6];
188/// let src_ref = RawStridedRef::new(&src, &dims, &src_strides, 0).unwrap();
189/// let mut dst_mut = RawStridedMut::new(&mut dst, &dims, &dst_strides, 0).unwrap();
190/// plan.execute(&mut dst_mut, &src_ref).unwrap();
191/// assert_eq!(dst, [0.0, 10.0, 1.0, 11.0, 2.0, 12.0]);
192/// ```
193#[derive(Clone, Debug, Default)]
194pub struct CopyPlan {
195    dims: AxisVec<usize>,
196    dst_strides: AxisVec<isize>,
197    src_strides: AxisVec<isize>,
198    fused: PairTraversal,
199}
200
201impl CopyPlan {
202    /// `None` when the fused traversal is stored in the rank-bounded spill.
203    #[cfg(feature = "parallel")]
204    #[inline]
205    pub(crate) fn fused_layout(&self) -> Option<&FusedPairLayout> {
206        self.fused.inline()
207    }
208
209    pub(crate) fn execute_uninit_then<'a, T, R>(
210        &self,
211        dest: &'a mut RawStridedMut<'a, MaybeUninit<T>>,
212        src: &RawStridedRef<'_, T>,
213        f: impl for<'b> FnOnce(InitializedRawDest<'b, T>) -> R,
214    ) -> Result<R>
215    where
216        T: Copy + MaybeSendSync,
217    {
218        self.execute_uninit(dest, src)?;
219        let data = dest.data_mut();
220        let receipt = InitializedRawDest {
221            ptr: data.as_mut_ptr().cast(),
222            extent: data.len(),
223            dims: dest.dims(),
224            strides: dest.strides(),
225            offset: dest.offset(),
226            _marker: PhantomData,
227        };
228        Ok(f(receipt))
229    }
230
231    /// Compile a copy plan for the given layout pair.
232    ///
233    /// Performs the layout validation and traversal construction
234    /// (fuse + order) once:
235    ///
236    /// - `dims`, `dst_strides`, and `src_strides` must have equal length
237    ///   ([`StridedError::StrideLengthMismatch`]);
238    /// - the total element count must not overflow `usize`
239    ///   ([`StridedError::OffsetOverflow`]);
240    /// - the destination layout must be injective, i.e. map distinct logical
241    ///   indices to distinct offsets
242    ///   ([`StridedError::NonInjectiveOutputLayout`]).
243    pub fn compile(dims: &[usize], dst_strides: &[isize], src_strides: &[isize]) -> Result<Self> {
244        if dims.len() != dst_strides.len() || dims.len() != src_strides.len() {
245            return Err(StridedError::StrideLengthMismatch);
246        }
247        // A zero-sized layout has zero elements even when its other extents
248        // overflow; only a nonempty overflowing count is rejected.
249        crate::kernel::total_len(dims)?;
250        if !crate::layout_check::is_injective_layout(dims, dst_strides) {
251            return Err(StridedError::NonInjectiveOutputLayout);
252        }
253        Ok(Self {
254            dims: dims.into(),
255            dst_strides: dst_strides.into(),
256            src_strides: src_strides.into(),
257            fused: fuse_pair_traversal(dims, dst_strides, src_strides),
258        })
259    }
260
261    /// Check the per-call facts: the supplied views must carry exactly the
262    /// compiled layout. Buffer bounds against that layout were already proven
263    /// by [`RawStridedRef::new`]/[`RawStridedMut::new`] (or asserted by the
264    /// caller of the `new_unchecked` constructors), so layout equality is the
265    /// complete precondition for the pointer-based fused replay below.
266    fn check_call<D, S>(
267        &self,
268        dest: &RawStridedMut<'_, D>,
269        src: &RawStridedRef<'_, S>,
270    ) -> Result<()> {
271        check_layout_pair(&self.dims, &self.dst_strides, &self.src_strides, dest, src)
272    }
273
274    /// `dest = src` into a potentially uninitialized destination.
275    ///
276    /// On success every reachable logical destination element is initialized.
277    /// Non-reachable holes in the backing allocation are not written.
278    pub fn execute_uninit<T>(
279        &self,
280        dest: &mut RawStridedMut<'_, MaybeUninit<T>>,
281        src: &RawStridedRef<'_, T>,
282    ) -> Result<()>
283    where
284        T: Copy + MaybeSendSync,
285    {
286        self.check_call(dest, src)?;
287        replay_fused(
288            dest,
289            src,
290            &self.fused,
291            |dst, value| {
292                dst.write(value);
293            },
294            |value| value,
295        );
296        Ok(())
297    }
298
299    /// `dest = src`. Allocation-free for fused ranks at most
300    /// [`RAW_FUSED_RANK_LIMIT`](crate::RAW_FUSED_RANK_LIMIT).
301    pub fn execute<T>(
302        &self,
303        dest: &mut RawStridedMut<'_, T>,
304        src: &RawStridedRef<'_, T>,
305    ) -> Result<()>
306    where
307        T: Copy + MaybeSendSync,
308    {
309        self.check_call(dest, src)?;
310        replay_fused(
311            dest,
312            src,
313            &self.fused,
314            |dst, value| *dst = value,
315            |value: T| value,
316        );
317        Ok(())
318    }
319
320    /// `dest = scale * src`. Allocation-free for fused ranks at most
321    /// [`RAW_FUSED_RANK_LIMIT`](crate::RAW_FUSED_RANK_LIMIT).
322    pub fn execute_scale<T>(
323        &self,
324        dest: &mut RawStridedMut<'_, T>,
325        src: &RawStridedRef<'_, T>,
326        scale: T,
327    ) -> Result<()>
328    where
329        T: Copy + Mul<T, Output = T> + MaybeSendSync,
330    {
331        self.check_call(dest, src)?;
332        replay_fused(
333            dest,
334            src,
335            &self.fused,
336            |dst, value| *dst = value,
337            |value: T| scale * value,
338        );
339        Ok(())
340    }
341
342    /// `dest = conj(src)`. Allocation-free for fused ranks at most
343    /// [`RAW_FUSED_RANK_LIMIT`](crate::RAW_FUSED_RANK_LIMIT).
344    pub fn execute_conj<T>(
345        &self,
346        dest: &mut RawStridedMut<'_, T>,
347        src: &RawStridedRef<'_, T>,
348    ) -> Result<()>
349    where
350        T: Copy + ElementOpApply + MaybeSendSync,
351    {
352        self.check_call(dest, src)?;
353        replay_fused(
354            dest,
355            src,
356            &self.fused,
357            |dst, value| *dst = value,
358            |value: T| value.conj(),
359        );
360        Ok(())
361    }
362}
363
364/// Verify that a pair of raw views carries exactly a compiled layout.
365///
366/// Shared by [`CopyPlan`] and [`UpdatePlan`], whose `compile` methods prove the
367/// layout facts once. Buffer bounds against that layout were already proven by
368/// [`RawStridedRef::new`]/[`RawStridedMut::new`] (or asserted by the caller of
369/// the `new_unchecked` constructors), so layout equality is the complete
370/// per-call precondition for the pointer-based fused replay.
371fn check_layout_pair<D, S>(
372    dims: &[usize],
373    dst_strides: &[isize],
374    src_strides: &[isize],
375    dest: &RawStridedMut<'_, D>,
376    src: &RawStridedRef<'_, S>,
377) -> Result<()> {
378    if dest.dims() != dims
379        || src.dims() != dims
380        || dest.strides() != dst_strides
381        || src.strides() != src_strides
382    {
383        return Err(StridedError::PlanLayoutMismatch);
384    }
385    Ok(())
386}
387
388/// A compiled read-modify-write traversal for one `(dims, dst_strides,
389/// src_strides)` layout pair.
390///
391/// [`execute`](Self::execute) computes `dest = alpha * f(src) + beta * dest` by
392/// replaying the same fused pair traversal as [`CopyPlan`]: `compile` proves
393/// the layout facts once (rank agreement, extent overflow, destination
394/// injectivity) and each call only re-checks that the supplied views carry the
395/// compiled layout before running monomorphized loops. Execute is
396/// allocation-free for fused ranks at most
397/// [`RAW_FUSED_RANK_LIMIT`](crate::RAW_FUSED_RANK_LIMIT), and the injective
398/// destination makes a large replay parallel-safe exactly as in [`CopyPlan`].
399///
400/// Overlapping `src`/`dest` memory is not supported, matching the rest of the
401/// crate.
402///
403/// # Example
404///
405/// ```rust
406/// use strided_basic::{RawStridedMut, RawStridedRef, UpdatePlan};
407///
408/// let dims = [2usize, 2];
409/// let dst_strides = [1isize, 2];
410/// let src_strides = [1isize, 2];
411/// let plan = UpdatePlan::compile(&dims, &dst_strides, &src_strides).unwrap();
412///
413/// let src = [1.0f64, 2.0, 3.0, 4.0];
414/// let mut dst = [10.0f64, 20.0, 30.0, 40.0];
415/// let src_ref = RawStridedRef::new(&src, &dims, &src_strides, 0).unwrap();
416/// let mut dst_mut = RawStridedMut::new(&mut dst, &dims, &dst_strides, 0).unwrap();
417/// // dst = 2 * src + 3 * dst
418/// plan.execute(&mut dst_mut, &src_ref, 2.0, 3.0, |x| x).unwrap();
419/// assert_eq!(dst, [32.0, 64.0, 96.0, 128.0]);
420/// ```
421#[derive(Clone, Debug)]
422pub struct UpdatePlan {
423    dims: AxisVec<usize>,
424    dst_strides: AxisVec<isize>,
425    src_strides: AxisVec<isize>,
426    fused: PairTraversal,
427}
428
429impl UpdatePlan {
430    /// Compile an update plan for the given layout pair.
431    ///
432    /// Performs the same layout validation as [`CopyPlan::compile`]:
433    ///
434    /// - `dims`, `dst_strides`, and `src_strides` must have equal length
435    ///   ([`StridedError::StrideLengthMismatch`]);
436    /// - the total element count must not overflow `usize`
437    ///   ([`StridedError::OffsetOverflow`]);
438    /// - the destination layout must be injective
439    ///   ([`StridedError::NonInjectiveOutputLayout`]), so two logical indices
440    ///   never name one slot and a parallel replay writes each slot once.
441    pub fn compile(dims: &[usize], dst_strides: &[isize], src_strides: &[isize]) -> Result<Self> {
442        if dims.len() != dst_strides.len() || dims.len() != src_strides.len() {
443            return Err(StridedError::StrideLengthMismatch);
444        }
445        crate::kernel::total_len(dims)?;
446        if !crate::layout_check::is_injective_layout(dims, dst_strides) {
447            return Err(StridedError::NonInjectiveOutputLayout);
448        }
449        Ok(Self {
450            dims: dims.into(),
451            dst_strides: dst_strides.into(),
452            src_strides: src_strides.into(),
453            fused: fuse_pair_traversal(dims, dst_strides, src_strides),
454        })
455    }
456
457    /// `dest = alpha * f(src) + beta * dest`.
458    ///
459    /// `f` is applied to the source element before scaling. When `beta` is
460    /// zero (`-0.0` included; `NaN` is not zero) the replay is write-only: the
461    /// old destination element is never read, so a NaN-filled or uninitialized
462    /// destination cannot contaminate the result, matching BLAS `beta = 0`
463    /// semantics. Otherwise the old destination element is read through the
464    /// destination layout. Allocation-free for fused ranks at most
465    /// [`RAW_FUSED_RANK_LIMIT`](crate::RAW_FUSED_RANK_LIMIT).
466    ///
467    /// # Errors
468    ///
469    /// [`StridedError::PlanLayoutMismatch`] when the supplied views do not
470    /// carry the compiled layout, before any write.
471    pub fn execute<T, F>(
472        &self,
473        dest: &mut RawStridedMut<'_, T>,
474        src: &RawStridedRef<'_, T>,
475        alpha: T,
476        beta: T,
477        f: F,
478    ) -> Result<()>
479    where
480        T: Copy + MaybeSendSync + Add<Output = T> + Mul<Output = T> + Zero + One + PartialEq,
481        F: Fn(T) -> T + MaybeSendSync,
482    {
483        check_layout_pair(&self.dims, &self.dst_strides, &self.src_strides, dest, src)?;
484        if beta.is_zero() {
485            // Write-only: never form a read of the old destination.
486            if alpha.is_one() {
487                replay_fused(dest, src, &self.fused, |dst, value| *dst = value, f);
488            } else {
489                replay_fused(dest, src, &self.fused, |dst, value| *dst = alpha * value, f);
490            }
491        } else {
492            replay_fused(
493                dest,
494                src,
495                &self.fused,
496                |dst, value| *dst = alpha * value + beta * *dst,
497                f,
498            );
499        }
500        Ok(())
501    }
502
503    /// `dest = alpha * f(src)` into a potentially uninitialized destination.
504    ///
505    /// This is [`execute`](Self::execute) with `beta = 0`: the destination is
506    /// never read. On success every reachable logical destination element is
507    /// initialized; non-reachable holes in the backing allocation are not
508    /// written. Allocation-free for fused ranks at most
509    /// [`RAW_FUSED_RANK_LIMIT`](crate::RAW_FUSED_RANK_LIMIT).
510    ///
511    /// # Errors
512    ///
513    /// [`StridedError::PlanLayoutMismatch`] when the supplied views do not
514    /// carry the compiled layout, before any write.
515    ///
516    /// # Examples
517    ///
518    /// ```rust
519    /// use core::mem::MaybeUninit;
520    /// use strided_basic::{RawStridedMut, RawStridedRef, UpdatePlan};
521    ///
522    /// let dims = [2usize, 2];
523    /// let strides = [1isize, 2];
524    /// let plan = UpdatePlan::compile(&dims, &strides, &strides).unwrap();
525    /// let src = [1.0f64, 2.0, 3.0, 4.0];
526    /// let mut storage = [MaybeUninit::<f64>::uninit(); 4];
527    /// let mut dst = RawStridedMut::new(&mut storage, &dims, &strides, 0).unwrap();
528    /// let source = RawStridedRef::new(&src, &dims, &strides, 0).unwrap();
529    /// // dst = 2 * src
530    /// plan.execute_uninit(&mut dst, &source, 2.0, |x| x).unwrap();
531    /// drop(dst);
532    /// // SAFETY: every reachable destination slot was initialized above.
533    /// let values: Vec<f64> = storage.iter().map(|v| unsafe { v.assume_init() }).collect();
534    /// assert_eq!(values, [2.0, 4.0, 6.0, 8.0]);
535    /// ```
536    pub fn execute_uninit<T, F>(
537        &self,
538        dest: &mut RawStridedMut<'_, MaybeUninit<T>>,
539        src: &RawStridedRef<'_, T>,
540        alpha: T,
541        f: F,
542    ) -> Result<()>
543    where
544        T: Copy + MaybeSendSync + Mul<Output = T> + One + PartialEq,
545        F: Fn(T) -> T + MaybeSendSync,
546    {
547        check_layout_pair(&self.dims, &self.dst_strides, &self.src_strides, dest, src)?;
548        if alpha.is_one() {
549            replay_fused(
550                dest,
551                src,
552                &self.fused,
553                |dst, value| {
554                    dst.write(value);
555                },
556                f,
557            );
558        } else {
559            replay_fused(
560                dest,
561                src,
562                &self.fused,
563                |dst, value| {
564                    dst.write(alpha * value);
565                },
566                f,
567            );
568        }
569        Ok(())
570    }
571}
572
573/// Replay a compiled fused layout, splitting it across worker threads when the
574/// repository threshold and the active execution policy allow it.
575///
576/// Parallel replay is sound only because [`CopyPlan::compile`] proved the
577/// destination layout injective: disjoint logical ranges then write disjoint
578/// destination slots. The logical index space `0..total` (column-major over
579/// the fused axes) is split into contiguous worker ranges, which divides the
580/// outer fused axes and, when there are fewer outer positions than workers,
581/// also chunks long inner runs (for example a rank-1 contiguous copy).
582fn replay_fused<D, S, Apply, Op>(
583    dest: &mut RawStridedMut<'_, D>,
584    src: &RawStridedRef<'_, S>,
585    traversal: &PairTraversal,
586    apply: Apply,
587    op: Op,
588) where
589    D: Copy + MaybeSendSync,
590    S: Copy + MaybeSendSync,
591    Apply: Fn(&mut D, S) + MaybeSendSync,
592    Op: Fn(S) -> S + MaybeSendSync,
593{
594    let src_ptr = src.data().as_ptr();
595    let src_base = src.offset();
596    let dst_base = dest.offset();
597    let dst_ptr = dest.data_mut().as_mut_ptr();
598    // SAFETY: `check_call` proved both views carry exactly the compiled
599    // layout, and `RawStridedRef`/`RawStridedMut` guarantee every offset
600    // reachable through that layout lies inside their data. `traversal` is the
601    // fusion of that layout, so every logical index in `0..total` is an
602    // in-bounds slot of both. The compiled destination layout is injective
603    // and the exclusive destination borrow cannot overlap the shared source
604    // borrow.
605    unsafe {
606        match traversal {
607            PairTraversal::Inline(layout) => {
608                replay_fused_raw(dst_ptr, dst_base, src_ptr, src_base, layout, apply, op)
609            }
610            PairTraversal::Spilled(axes) => {
611                replay_spilled_raw(dst_ptr, dst_base, src_ptr, src_base, axes, apply, op)
612            }
613        }
614    }
615}
616
617/// Pointer-level fused replay shared by [`CopyPlan`] and the dynamic slice
618/// plans, which replay a fused window layout at a runtime base offset.
619///
620/// Splits `0..total` across workers when the repository threshold and the
621/// active execution policy allow more than one thread, otherwise runs the
622/// serial kernel directly.
623///
624/// # Safety
625///
626/// Every logical index of `layout`, mapped through its destination strides
627/// from `dst_base` and its source strides from `src_base`, must be an
628/// in-bounds slot of the destination and source allocations. The
629/// destination strides must be injective over `layout`, the destination
630/// must not overlap the source, and nothing else may access the destination
631/// slots during the call.
632pub(crate) unsafe fn replay_fused_raw<D, S, Apply, Op>(
633    dst_ptr: *mut D,
634    dst_base: isize,
635    src_ptr: *const S,
636    src_base: isize,
637    layout: &FusedPairLayout,
638    apply: Apply,
639    op: Op,
640) where
641    D: Copy + MaybeSendSync,
642    S: Copy + MaybeSendSync,
643    Apply: Fn(&mut D, S) + MaybeSendSync,
644    Op: Fn(S) -> S + MaybeSendSync,
645{
646    let total = fused_total(layout);
647    if total == 0 {
648        return;
649    }
650    #[cfg(feature = "parallel")]
651    {
652        let nthreads = crate::threading::parallel_threads_for_len(total);
653        if nthreads > 1 {
654            let dst_ptr = crate::threading::SendPtr(dst_ptr);
655            let src_ptr = crate::threading::SendPtr(src_ptr as *mut S);
656            crate::threading::parallel_for_each(0..total, nthreads, &|range| {
657                // SAFETY: the caller contract makes every index in bounds; the
658                // worker ranges are disjoint and the destination strides
659                // injective, so no two workers write the same slot, and the
660                // source is only read.
661                unsafe {
662                    apply_fused_range(
663                        dst_ptr.as_ptr(),
664                        dst_base,
665                        src_ptr.as_const(),
666                        src_base,
667                        layout,
668                        range.start,
669                        range.len(),
670                        &apply,
671                        &op,
672                    );
673                }
674            });
675            return;
676        }
677    }
678    // SAFETY: forwarded caller contract over the full range `0..total`.
679    unsafe {
680        apply_fused_range(
681            dst_ptr, dst_base, src_ptr, src_base, layout, 0, total, &apply, &op,
682        );
683    }
684}
685
686/// Pointer-level replay of a spilled axis list.
687///
688/// Mirrors [`replay_fused_raw`] including its worker split; kept separate so
689/// that the inline replay keeps the shape it had before spilled layouts
690/// existed.
691///
692/// # Safety
693///
694/// Every logical index of `axes`, mapped through its destination strides from
695/// `dst_base` and its source strides from `src_base`, must be an in-bounds slot
696/// of the destination and source allocations. The destination strides must be
697/// injective over `axes`, the destination must not overlap the source, and
698/// nothing else may access the destination slots during the call.
699pub(crate) unsafe fn replay_spilled_raw<D, S, Apply, Op>(
700    dst_ptr: *mut D,
701    dst_base: isize,
702    src_ptr: *const S,
703    src_base: isize,
704    axes: &[FusedAxis],
705    apply: Apply,
706    op: Op,
707) where
708    D: Copy + MaybeSendSync,
709    S: Copy + MaybeSendSync,
710    Apply: Fn(&mut D, S) + MaybeSendSync,
711    Op: Fn(S) -> S + MaybeSendSync,
712{
713    let total = axes.iter().map(|axis| axis.dim).product::<usize>();
714    if total == 0 {
715        return;
716    }
717    #[cfg(feature = "parallel")]
718    {
719        let nthreads = crate::threading::parallel_threads_for_len(total);
720        if nthreads > 1 {
721            let dst_ptr = crate::threading::SendPtr(dst_ptr);
722            let src_ptr = crate::threading::SendPtr(src_ptr as *mut S);
723            crate::threading::parallel_for_each(0..total, nthreads, &|range| {
724                // SAFETY: the caller contract makes every index in bounds; the
725                // worker ranges are disjoint and the destination strides
726                // injective, so no two workers write the same slot, and the
727                // source is only read.
728                unsafe {
729                    apply_spilled_range(
730                        dst_ptr.as_ptr(),
731                        dst_base,
732                        src_ptr.as_const(),
733                        src_base,
734                        axes,
735                        range.start,
736                        range.len(),
737                        &apply,
738                        &op,
739                    );
740                }
741            });
742            return;
743        }
744    }
745    // SAFETY: forwarded caller contract over the full range `0..total`.
746    unsafe {
747        apply_spilled_range(
748            dst_ptr, dst_base, src_ptr, src_base, axes, 0, total, &apply, &op,
749        );
750    }
751}
752
753#[cfg(test)]
754#[path = "copy_plan/tests/tests.rs"]
755mod tests;
756
757#[cfg(all(test, feature = "parallel"))]
758#[path = "copy_plan/tests/parallel_tests.rs"]
759mod parallel_tests;