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;