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;