Skip to main content

strided_perm/
copy.rs

1//! Copy/permutation operations on strided views.
2
3use crate::fuse::FusionPlanError;
4use crate::hptt::{build_permute_plan, execute_permute_blocked};
5use strided_view::{Result, StridedError, StridedView, StridedViewMut};
6use threadteam::Exec;
7
8/// Map a permutation planning failure onto the shared view error vocabulary.
9///
10/// Rank and shape are validated before planning, so a length mismatch cannot
11/// reach this point; it still maps to a typed error instead of a panic. An
12/// oversized (typically stride-0 broadcast) extent whose fused product cannot
13/// be represented is reported as an offset overflow (issue #263).
14fn plan_error(err: FusionPlanError) -> StridedError {
15    match err {
16        FusionPlanError::LengthMismatch { .. } => StridedError::StrideLengthMismatch,
17        FusionPlanError::DimensionOverflow => StridedError::OffsetOverflow,
18    }
19}
20
21fn total_len(dims: &[usize]) -> usize {
22    dims.iter().product()
23}
24
25/// Check if all strides indicate contiguous column-major or row-major layout.
26fn is_both_contiguous(dims: &[usize], dst_strides: &[isize], src_strides: &[isize]) -> bool {
27    if dims.is_empty() {
28        return true;
29    }
30
31    // Check col-major for both
32    let mut expected = 1isize;
33    let mut col_ok = true;
34    for (&d, (&ds, &ss)) in dims.iter().zip(dst_strides.iter().zip(src_strides.iter())) {
35        if d <= 1 {
36            continue;
37        }
38        if ds != expected || ss != expected {
39            col_ok = false;
40            break;
41        }
42        expected = expected.saturating_mul(d as isize);
43    }
44    if col_ok {
45        return true;
46    }
47
48    // Check row-major for both
49    let mut expected = 1isize;
50    let mut row_ok = true;
51    for (&d, (&ds, &ss)) in dims
52        .iter()
53        .rev()
54        .zip(dst_strides.iter().rev().zip(src_strides.iter().rev()))
55    {
56        if d <= 1 {
57            continue;
58        }
59        if ds != expected || ss != expected {
60            row_ok = false;
61            break;
62        }
63        expected = expected.saturating_mul(d as isize);
64    }
65    row_ok
66}
67
68/// The validated raw geometry of one copy: the shared dims, both stride
69/// runs and the two origins. `U` is the destination element type: `T` for
70/// an initialized destination, `MaybeUninit<T>` for a fresh one.
71struct RawCopy<'a, T, U> {
72    dims: &'a [usize],
73    dst_strides: &'a [isize],
74    src_strides: &'a [isize],
75    dst: *mut U,
76    src: *const T,
77}
78
79/// Validates rank and shape and extracts the raw geometry; `None` for a
80/// zero-sized copy, which has nothing to write.
81///
82/// A zero-sized view validates without its other extents or strides forming
83/// an in-range product, so neither the contiguous fast path nor the
84/// permutation planner may see it.
85fn prepare<'a, T, U>(
86    dest: &'a mut StridedViewMut<U>,
87    src: &'a StridedView<T>,
88) -> Result<Option<RawCopy<'a, T, U>>> {
89    let dst_dims = dest.dims();
90    let src_dims = src.dims();
91    if dst_dims.len() != src_dims.len() {
92        return Err(StridedError::RankMismatch(dst_dims.len(), src_dims.len()));
93    }
94    if dst_dims != src_dims {
95        return Err(StridedError::ShapeMismatch(
96            dst_dims.to_vec(),
97            src_dims.to_vec(),
98        ));
99    }
100    if dst_dims.contains(&0) {
101        return Ok(None);
102    }
103    Ok(Some(RawCopy {
104        dims: dst_dims,
105        dst_strides: dest.strides(),
106        src_strides: src.strides(),
107        dst: dest.as_mut_ptr(),
108        src: src.ptr(),
109    }))
110}
111
112/// The copy over validated raw geometry, run within the budget of `exec`.
113///
114/// # Safety
115///
116/// `raw.dst` must be valid for writes of `T` at every destination offset and
117/// `raw.src` valid for reads of initialized `T` at every source offset; the
118/// two ranges must not overlap. Every store goes through raw pointers, so the
119/// destination may be uninitialized storage.
120unsafe fn copy_raw<T: Copy + Send + Sync + 'static, U>(
121    raw: RawCopy<'_, T, U>,
122    exec: Exec<'_>,
123) -> Result<()> {
124    let dst_ptr = raw.dst.cast::<T>();
125    // The width is the token's decision: the serial time of the copy against the
126    // dispatch cost of the one section the engine opens (`for_ranges`).
127    let exec = match raw.dims.iter().try_fold(1usize, |n, &d| n.checked_mul(d)) {
128        Some(n) => {
129            let estimate =
130                std::time::Duration::from_secs_f64(n as f64 * crate::copy_ns_per_element() * 1e-9);
131            let width = exec.width_for(estimate, 1);
132            exec.limit(width)
133        }
134        // An overflowing count is reported by planning below; run it serially.
135        None => exec.limit(std::num::NonZeroUsize::MIN),
136    };
137    // Fast path: both contiguous. A contiguous layout is injective by
138    // construction, so it skips the validation below.
139    if is_both_contiguous(raw.dims, raw.dst_strides, raw.src_strides) {
140        let len = total_len(raw.dims);
141        if exec.is_serial() {
142            // SAFETY: the validated contiguous ranges are live and non-overlapping.
143            unsafe { std::ptr::copy_nonoverlapping(raw.src, dst_ptr, len) };
144        } else {
145            let src_addr = raw.src as usize;
146            let dst_addr = dst_ptr as usize;
147            exec.for_ranges(len, std::num::NonZeroUsize::MIN, |range| {
148                // SAFETY: `for_ranges` joins disjoint subranges of the validated
149                // contiguous buffers; raw stores also support uninitialized output.
150                unsafe {
151                    std::ptr::copy_nonoverlapping(
152                        (src_addr as *const T).add(range.start),
153                        (dst_addr as *mut T).add(range.start),
154                        range.len(),
155                    );
156                }
157            });
158        }
159        return Ok(());
160    }
161
162    // HPTT-inspired blocked permutation. Planning runs before the injectivity
163    // check so an unrepresentable layout keeps reporting `OffsetOverflow`
164    // (issue #263) even when its destination is also non-injective; planning
165    // only builds metadata and writes nothing.
166    let elem_size = std::mem::size_of::<T>();
167    let plan = build_permute_plan(raw.dims, raw.src_strides, raw.dst_strides, elem_size)
168        .map_err(plan_error)?;
169    // A non-injective destination maps distinct logical elements onto one
170    // offset, so the unsafe write loop would produce overlapping writes (and a
171    // data race when `exec` fans out). This matches the sibling output-writing
172    // APIs (issue #281). The check is global over the destination, so two
173    // worker partitions (disjoint logical positions) can never write one
174    // offset; no extra per-partition separation proof is needed.
175    if !strided_view::is_injective_layout(raw.dims, raw.dst_strides) {
176        return Err(StridedError::NonInjectiveOutputLayout);
177    }
178    execute_permute_blocked(raw.src, dst_ptr, &plan, exec);
179    Ok(())
180}
181
182/// Copy elements from source to destination: `dest[i] = src[i]`.
183///
184/// Uses HPTT-inspired blocked permutation with bilateral dimension fusion,
185/// cache-aware blocking, and optimal loop ordering. The permutation is split
186/// across the budget of `exec`, at the width the token's dispatch cost
187/// justifies for the copy ([`copy_ns_per_element`](crate::copy_ns_per_element)
188/// times its elements); a small copy stays on the calling thread. A caller that wants a large copy on one thread passes
189/// [`Exec::serial`] or narrows the token with [`Exec::limit`].
190/// A contiguous copy uses one `memcpy` per range of the narrowed token, or a
191/// single `memcpy` on the calling thread when that token is serial.
192///
193/// This is a simple copy without ElementOp support. For copies with
194/// element operations (conj, transpose, etc.), use `strided_kernel::copy_into`.
195///
196/// # Examples
197///
198/// ```
199/// use threadteam::Exec;
200/// use strided_perm::copy_into;
201/// use strided_view::StridedArray;
202///
203/// let src = StridedArray::<f64>::from_fn_col_major(&[2, 3], |idx| (idx[0] * 10 + idx[1]) as f64);
204/// let mut dst = StridedArray::<f64>::col_major(&[2, 3]);
205/// copy_into(&mut dst.view_mut(), &src.view(), Exec::serial()).unwrap();
206/// assert_eq!(dst.get(&[1, 2]), 12.0);
207/// ```
208pub fn copy_into<T: Copy + Send + Sync + 'static>(
209    dest: &mut StridedViewMut<T>,
210    src: &StridedView<T>,
211    exec: Exec<'_>,
212) -> Result<()> {
213    match prepare(dest, src)? {
214        // SAFETY: both views are validated layouts over live, non-overlapping
215        // storage (`StridedViewMut` is exclusive), and `prepare` matched them.
216        Some(raw) => unsafe { copy_raw(raw, exec) },
217        None => Ok(()),
218    }
219}
220
221/// Copy elements into an uninitialized destination: `dest[i] = src[i]`.
222///
223/// The same permutation engine as [`copy_into`], selected on `T` rather than
224/// on `MaybeUninit<T>`: the engine's native `f32`/`f64` kernels are chosen by
225/// element type, so a destination reinterpreted as `MaybeUninit<T>` would
226/// otherwise fall to the generic element path (about 2.5x slower on a
227/// 4096x4096 `f64` transpose). Every destination element the views describe
228/// is written exactly once; the stores go through raw pointers only, so no
229/// reference to uninitialized storage is formed.
230pub fn copy_into_uninit<T: Copy + Send + Sync + 'static>(
231    dest: &mut StridedViewMut<core::mem::MaybeUninit<T>>,
232    src: &StridedView<T>,
233    exec: Exec<'_>,
234) -> Result<()> {
235    match prepare(dest, src)? {
236        // SAFETY: as `copy_into`; `MaybeUninit<T>` has `T`'s layout and the
237        // engine only stores into the destination.
238        Some(raw) => unsafe { copy_raw(raw, exec) },
239        None => Ok(()),
240    }
241}
242
243/// Copy elements to a col-major destination.
244///
245/// This delegates to the same HPTT-inspired blocked permutation as
246/// [`copy_into`]. The HPTT planner handles the col-major destination case
247/// optimally.
248pub fn copy_into_col_major<T: Copy + Send + Sync + 'static>(
249    dst: &mut StridedViewMut<T>,
250    src: &StridedView<T>,
251    exec: Exec<'_>,
252) -> Result<()> {
253    copy_into(dst, src, exec)
254}
255
256#[cfg(test)]
257fn try_fuse_group(dims: &[usize], strides: &[isize]) -> Option<(usize, isize)> {
258    match dims.len() {
259        0 => Some((1, 0)),
260        1 => Some((dims[0], strides[0])),
261        _ => {
262            if dims.len() != strides.len() {
263                return None;
264            }
265            for (&d, &s) in dims.iter().zip(strides.iter()) {
266                if d > 1 && s == 0 {
267                    return None;
268                }
269            }
270            let mut base_idx: Option<usize> = None;
271            let mut base_abs = usize::MAX;
272            for (i, (&d, &s)) in dims.iter().zip(strides.iter()).enumerate() {
273                if d <= 1 {
274                    continue;
275                }
276                let abs = s.unsigned_abs();
277                if abs < base_abs {
278                    base_abs = abs;
279                    base_idx = Some(i);
280                }
281            }
282
283            let Some(base) = base_idx else {
284                let stride = *strides
285                    .iter()
286                    .min_by_key(|s| s.unsigned_abs())
287                    .unwrap_or(&0);
288                return Some((dims.iter().product(), stride));
289            };
290
291            let mut used = vec![false; dims.len()];
292            used[base] = true;
293            let mut expected_abs = base_abs.checked_mul(dims[base])?;
294
295            let non_singleton = dims.iter().filter(|&&d| d > 1).count();
296            for _ in 1..non_singleton {
297                let mut next = None;
298                for i in 0..dims.len() {
299                    if used[i] || dims[i] <= 1 {
300                        continue;
301                    }
302                    if strides[i].unsigned_abs() == expected_abs {
303                        next = Some(i);
304                        break;
305                    }
306                }
307                let i = next?;
308                used[i] = true;
309                expected_abs = expected_abs.checked_mul(dims[i])?;
310            }
311
312            Some((dims.iter().product(), strides[base]))
313        }
314    }
315}
316
317#[cfg(test)]
318mod tests {
319    use super::*;
320    use strided_view::StridedArray;
321
322    #[test]
323    fn test_copy_into_contiguous() {
324        let src =
325            StridedArray::<f64>::from_fn_col_major(&[2, 3], |idx| (idx[0] * 10 + idx[1]) as f64);
326        let mut dst = StridedArray::<f64>::col_major(&[2, 3]);
327        copy_into(&mut dst.view_mut(), &src.view(), Exec::serial()).unwrap();
328        assert_eq!(dst.get(&[0, 0]), 0.0);
329        assert_eq!(dst.get(&[1, 2]), 12.0);
330    }
331
332    #[test]
333    fn test_copy_into_transposed() {
334        // src is row-major [3,2], dst is col-major [3,2]
335        let src = StridedArray::<f64>::from_parts(
336            vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0],
337            &[3, 2],
338            &[2, 1], // row-major
339            0,
340        )
341        .unwrap();
342        let mut dst = StridedArray::<f64>::col_major(&[3, 2]);
343        copy_into(&mut dst.view_mut(), &src.view(), Exec::serial()).unwrap();
344        assert_eq!(dst.get(&[0, 0]), 0.0);
345        assert_eq!(dst.get(&[0, 1]), 1.0);
346        assert_eq!(dst.get(&[1, 0]), 2.0);
347        assert_eq!(dst.get(&[2, 1]), 5.0);
348    }
349
350    #[test]
351    fn test_copy_into_col_major_basic() {
352        let src =
353            StridedArray::<f64>::from_fn_col_major(&[4, 3], |idx| (idx[0] * 10 + idx[1]) as f64);
354        let mut dst = StridedArray::<f64>::col_major(&[4, 3]);
355        copy_into_col_major(&mut dst.view_mut(), &src.view(), Exec::serial()).unwrap();
356        assert_eq!(dst.get(&[0, 0]), 0.0);
357        assert_eq!(dst.get(&[3, 2]), 32.0);
358    }
359
360    #[test]
361    fn test_copy_into_col_major_permuted_src() {
362        // src has scattered strides, dst is col-major
363        let data: Vec<f64> = (0..24).map(|i| i as f64).collect();
364        let src = StridedArray::<f64>::from_parts(
365            data,
366            &[2, 3, 4],
367            &[12, 4, 1], // row-major
368            0,
369        )
370        .unwrap();
371        let mut dst = StridedArray::<f64>::col_major(&[2, 3, 4]);
372        copy_into_col_major(&mut dst.view_mut(), &src.view(), Exec::serial()).unwrap();
373        // Verify element-by-element
374        for i in 0..2 {
375            for j in 0..3 {
376                for k in 0..4 {
377                    assert_eq!(
378                        dst.get(&[i, j, k]),
379                        src.get(&[i, j, k]),
380                        "mismatch at [{},{},{}]",
381                        i,
382                        j,
383                        k
384                    );
385                }
386            }
387        }
388    }
389
390    #[test]
391    fn test_copy_into_grouped_high_rank_binary_permutation() {
392        let dims = vec![2usize; 12];
393        let total: usize = dims.iter().product();
394        let src = StridedArray::<u32>::from_fn_col_major(&dims, |idx| {
395            idx.iter()
396                .enumerate()
397                .map(|(axis, &coord)| (coord as u32) << axis)
398                .sum::<u32>()
399        });
400        let perm = vec![2, 4, 5, 6, 0, 3, 7, 1, 8, 9, 10, 11];
401        let src_view = src.view().permute(&perm).unwrap();
402        let mut dst = StridedArray::<u32>::col_major(&dims);
403
404        copy_into(&mut dst.view_mut(), &src_view, Exec::serial()).unwrap();
405
406        for flat in 0..total {
407            let mut rem = flat;
408            let mut src_offset = 0usize;
409            for &stride in src_view.strides() {
410                let coord = rem & 1;
411                rem >>= 1;
412                src_offset += coord * stride as usize;
413            }
414            assert_eq!(dst.data()[flat], src.data()[src_offset], "flat={flat}");
415        }
416    }
417
418    #[test]
419    fn test_try_fuse_group_empty() {
420        assert_eq!(try_fuse_group(&[], &[]), Some((1, 0)));
421    }
422
423    #[test]
424    fn test_try_fuse_group_single() {
425        assert_eq!(try_fuse_group(&[5], &[2]), Some((5, 2)));
426    }
427
428    #[test]
429    fn test_try_fuse_group_contiguous_row_major() {
430        assert_eq!(try_fuse_group(&[3, 4], &[4, 1]), Some((12, 1)));
431    }
432
433    #[test]
434    fn test_try_fuse_group_contiguous_col_major() {
435        assert_eq!(try_fuse_group(&[3, 4], &[1, 3]), Some((12, 1)));
436    }
437
438    #[test]
439    fn test_try_fuse_group_non_contiguous() {
440        assert_eq!(try_fuse_group(&[3, 4], &[8, 1]), None);
441    }
442
443    #[test]
444    fn parallel_transpose_splits_the_full_tile_grid_for_a_short_row_extent() {
445        // The source's unit-stride extent is the short side of the transpose, so
446        // the whole kernel would be a single row tile over `block`, and the budget is
447        // two. The parallel entry must split the
448        // column tiles too; a row-only split would leave the work on one worker.
449        // The result is checked element by element.
450        let rows = 20000usize;
451        let cols = 3usize;
452        let data: Vec<f64> = (0..rows * cols).map(|i| i as f64).collect();
453        let src =
454            StridedArray::<f64>::from_parts(data, &[rows, cols], &[cols as isize, 1], 0).unwrap();
455        let pool = threadteam::Pool::builder().threads(2).build().unwrap();
456        let mut dst = StridedArray::<f64>::col_major(&[rows, cols]);
457        let exec = Exec::new(&pool, 2).unwrap();
458        copy_into(&mut dst.view_mut(), &src.view(), exec).unwrap();
459        for r in 0..rows {
460            for c in 0..cols {
461                assert_eq!(dst.get(&[r, c]), (r * cols + c) as f64);
462            }
463        }
464    }
465
466    #[test]
467    fn test_copy_shape_mismatch() {
468        let src = StridedArray::<f64>::col_major(&[2, 3]);
469        let mut dst = StridedArray::<f64>::col_major(&[3, 2]);
470        let result = copy_into(&mut dst.view_mut(), &src.view(), Exec::serial());
471        assert!(result.is_err());
472    }
473}