Skip to main content

strided_basic/
raw_ops.rs

1//! Allocation-free copy/axpy over borrowed raw strided layouts.
2//!
3//! The map/zip kernels build a traversal plan per call; for small replay
4//! copies that fixed cost dominates. These entry points take
5//! [`RawStridedRef`]/[`RawStridedMut`], fuse the stride pair into a loop nest
6//! (inline storage for fused ranks at most [`RAW_FUSED_RANK_LIMIT`], one
7//! rank-bounded spill above that), and run plain loops - no view plan, no
8//! cache, and no per-element allocation on any call path. Higher *original*
9//! ranks are normalized and fused before that choice is made, so they no
10//! longer leave the fused replay.
11
12use crate::{ElementOpApply, RawStridedMut, RawStridedRef, Result};
13use core::ops::{Add, Mul};
14
15use crate::maybe_sync::MaybeSendSync;
16
17/// Rank up to which a fused loop nest is stored inline.
18///
19/// The stateless [`copy_scale_raw`] family and [`crate::CopyPlan`] fuse any rank:
20/// layouts whose *fused* rank is at most this limit replay from that inline
21/// storage, and higher fused ranks spill to storage bounded by their rank.
22///
23/// Replay is allocation-free for an original rank at most this limit. A higher
24/// *original* rank needs one transient scratch buffer even when the fused layout
25/// still fits inline, because fusion happens before the representation is
26/// chosen; the replay itself is still allocation-free in that case.
27pub const RAW_FUSED_RANK_LIMIT: usize = 8;
28
29/// One fused axis, used while sorting and merging axes and when a layout has to
30/// spill beyond [`RAW_FUSED_RANK_LIMIT`].
31#[derive(Clone, Copy, Debug)]
32pub(crate) struct FusedAxis {
33    pub(crate) dim: usize,
34    pub(crate) dst: isize,
35    pub(crate) src: isize,
36}
37
38const EMPTY_AXIS: FusedAxis = FusedAxis {
39    dim: 1,
40    dst: 0,
41    src: 0,
42};
43
44/// Stack-allocated fused stride pair (axes ordered by destination stride,
45/// adjacent contiguous axes merged). Built once and replayed by both the
46/// per-call raw kernels and the prepared [`crate::CopyPlan`].
47///
48/// This is the representation every call site replays through, so it keeps the
49/// exact shape, size and `Copy`-ness it had before high-rank fusion existed.
50#[derive(Clone, Copy, Debug)]
51pub(crate) struct FusedPairLayout {
52    pub(crate) rank: usize,
53    pub(crate) dims: [usize; RAW_FUSED_RANK_LIMIT],
54    dst_strides: [isize; RAW_FUSED_RANK_LIMIT],
55    src_strides: [isize; RAW_FUSED_RANK_LIMIT],
56}
57
58impl FusedPairLayout {
59    /// An empty rank-zero layout (one element at the base offset).
60    ///
61    /// Used as the neutral value when an inline `TinyVec<[CopyPlan; N]>` is
62    /// default-initialized; it is never executed as a real plan.
63    fn empty() -> Self {
64        Self {
65            rank: 0,
66            dims: [1; RAW_FUSED_RANK_LIMIT],
67            dst_strides: [0; RAW_FUSED_RANK_LIMIT],
68            src_strides: [0; RAW_FUSED_RANK_LIMIT],
69        }
70    }
71
72    fn from_axes(axes: &[FusedAxis]) -> Self {
73        debug_assert!(axes.len() <= RAW_FUSED_RANK_LIMIT);
74        let mut layout = Self {
75            rank: axes.len(),
76            dims: [1; RAW_FUSED_RANK_LIMIT],
77            dst_strides: [0; RAW_FUSED_RANK_LIMIT],
78            src_strides: [0; RAW_FUSED_RANK_LIMIT],
79        };
80        for (slot, axis) in layout.dims[..axes.len()].iter_mut().zip(axes) {
81            *slot = axis.dim;
82        }
83        for (slot, axis) in layout.dst_strides[..axes.len()].iter_mut().zip(axes) {
84            *slot = axis.dst;
85        }
86        for (slot, axis) in layout.src_strides[..axes.len()].iter_mut().zip(axes) {
87            *slot = axis.src;
88        }
89        layout
90    }
91}
92
93/// A fused pair traversal of any rank.
94///
95/// Fused ranks at most [`RAW_FUSED_RANK_LIMIT`] use the inline layout; longer
96/// layouts keep their axes in one `Vec`. That storage is sized by the original
97/// rank (the fusion scratch is reused, and `truncate` keeps its capacity), never
98/// by the tensor's element count.
99#[derive(Clone, Debug)]
100pub(crate) enum PairTraversal {
101    Inline(FusedPairLayout),
102    Spilled(Vec<FusedAxis>),
103}
104
105impl Default for FusedPairLayout {
106    fn default() -> Self {
107        Self::empty()
108    }
109}
110
111impl Default for PairTraversal {
112    fn default() -> Self {
113        Self::empty()
114    }
115}
116
117impl PairTraversal {
118    /// The empty inline layout, used as a neutral default.
119    #[inline]
120    fn empty() -> Self {
121        Self::Inline(FusedPairLayout::empty())
122    }
123
124    /// The inline layout, or `None` when the traversal spilled.
125    #[cfg(any(test, feature = "parallel"))]
126    #[inline]
127    pub(crate) fn inline(&self) -> Option<&FusedPairLayout> {
128        match self {
129            Self::Inline(layout) => Some(layout),
130            Self::Spilled(_) => None,
131        }
132    }
133
134    /// Number of fused axes.
135    #[cfg(test)]
136    pub(crate) fn rank(&self) -> usize {
137        match self {
138            Self::Inline(layout) => layout.rank,
139            Self::Spilled(axes) => axes.len(),
140        }
141    }
142}
143
144/// Sort `dims`/`dst_strides`/`src_strides` by destination stride, drop size-1
145/// axes, and merge axes that are contiguous in both layouts.
146///
147/// Returns the fused rank written to `scratch[..rank]`. A zero extent collapses
148/// to a single zero-length axis, which every replay treats as an empty range.
149fn fuse_axes(
150    scratch: &mut [FusedAxis],
151    dims: &[usize],
152    dst_strides: &[isize],
153    src_strides: &[isize],
154) -> usize {
155    debug_assert!(scratch.len() >= dims.len());
156    let mut rank = 0usize;
157    for axis in 0..dims.len() {
158        if dims[axis] == 1 {
159            continue;
160        }
161        if dims[axis] == 0 {
162            scratch[0] = FusedAxis {
163                dim: 0,
164                dst: 0,
165                src: 0,
166            };
167            return 1;
168        }
169        let entry = FusedAxis {
170            dim: dims[axis],
171            dst: dst_strides[axis],
172            src: src_strides[axis],
173        };
174        let mut position = rank;
175        while position > 0 && scratch[position - 1].dst > entry.dst {
176            scratch[position] = scratch[position - 1];
177            position -= 1;
178        }
179        scratch[position] = entry;
180        rank += 1;
181    }
182    if rank == 0 {
183        scratch[0] = EMPTY_AXIS;
184        return 1;
185    }
186    let mut fused = 0usize;
187    for axis in 1..rank {
188        // A merged extent that overflows (possible for a stride-0 broadcast
189        // whose element count exceeds usize) leaves the axes unfused.
190        let merged = isize::try_from(scratch[fused].dim)
191            .ok()
192            .filter(|&extent| {
193                scratch[fused].dst.checked_mul(extent) == Some(scratch[axis].dst)
194                    && scratch[fused].src.checked_mul(extent) == Some(scratch[axis].src)
195            })
196            .and_then(|_| scratch[fused].dim.checked_mul(scratch[axis].dim));
197        if let Some(merged) = merged {
198            scratch[fused].dim = merged;
199        } else {
200            fused += 1;
201            scratch[fused] = scratch[axis];
202        }
203    }
204    fused + 1
205}
206
207/// Fuse a raw stride pair into a traversal of any rank.
208///
209/// Inline when the fused rank is at most [`RAW_FUSED_RANK_LIMIT`], otherwise one
210/// rank-bounded spill. An original rank above the limit uses a transient scratch
211/// buffer even when the fused layout still fits inline, so that fusion happens
212/// before the representation is chosen.
213pub(crate) fn fuse_pair_traversal(
214    dims: &[usize],
215    dst_strides: &[isize],
216    src_strides: &[isize],
217) -> PairTraversal {
218    if dims.len() <= RAW_FUSED_RANK_LIMIT {
219        let mut scratch = [EMPTY_AXIS; RAW_FUSED_RANK_LIMIT];
220        let rank = fuse_axes(&mut scratch, dims, dst_strides, src_strides);
221        debug_assert!(rank <= RAW_FUSED_RANK_LIMIT);
222        return PairTraversal::Inline(FusedPairLayout::from_axes(&scratch[..rank]));
223    }
224    let mut scratch = vec![EMPTY_AXIS; dims.len()];
225    let rank = fuse_axes(&mut scratch, dims, dst_strides, src_strides);
226    scratch.truncate(rank);
227    if rank <= RAW_FUSED_RANK_LIMIT {
228        PairTraversal::Inline(FusedPairLayout::from_axes(&scratch))
229    } else {
230        PairTraversal::Spilled(scratch)
231    }
232}
233
234/// Fuse a raw stride pair into the inline representation.
235///
236/// Returns `None` when the fused rank exceeds [`RAW_FUSED_RANK_LIMIT`]. Used by
237/// the window plans, whose non-fused replay is compiled alongside the plan.
238pub(crate) fn fuse_pair_layout(
239    dims: &[usize],
240    dst_strides: &[isize],
241    src_strides: &[isize],
242) -> Option<FusedPairLayout> {
243    match fuse_pair_traversal(dims, dst_strides, src_strides) {
244        PairTraversal::Inline(layout) => Some(layout),
245        PairTraversal::Spilled(_) => None,
246    }
247}
248
249/// Number of logical elements covered by a fused layout.
250///
251/// Every constructor of a [`FusedPairLayout`] starts from dims whose product
252/// was checked by [`crate::kernel::total_len`], and fusion only merges axes
253/// with a checked product, so the product cannot overflow.
254#[inline]
255pub(crate) fn fused_total(layout: &FusedPairLayout) -> usize {
256    layout.dims[..layout.rank].iter().product()
257}
258
259/// Number of logical elements covered by a spilled axis list.
260#[inline]
261fn spilled_total(axes: &[FusedAxis]) -> usize {
262    axes.iter().map(|axis| axis.dim).product()
263}
264
265/// Serial replay of a fused pair layout over borrowed raw views.
266///
267/// The destination layout need not be injective: runs are replayed in order,
268/// so accumulating callers such as [`axpy_raw`] keep their sequential
269/// semantics. Parallel replay is only offered by [`crate::CopyPlan`], whose
270/// compile step proves destination injectivity.
271pub(crate) fn apply_fused_pair<D, S, Apply, Op>(
272    dst: &mut RawStridedMut<'_, D>,
273    src: &RawStridedRef<'_, S>,
274    layout: &FusedPairLayout,
275    apply: Apply,
276    op: Op,
277) where
278    D: Copy,
279    S: Copy,
280    Apply: Fn(&mut D, S),
281    Op: Fn(S) -> S,
282{
283    let total = fused_total(layout);
284    if total == 0 {
285        return;
286    }
287    let src_ptr = src.data().as_ptr();
288    let src_base = src.offset();
289    let dst_base = dst.offset();
290    let dst_ptr = dst.data_mut().as_mut_ptr();
291    // SAFETY: `RawStridedRef`/`RawStridedMut` guarantee (by `new`, or by the
292    // caller of `new_unchecked`) that every offset reachable from their offset
293    // through their dims/strides lies inside their data. Callers pass a layout
294    // fused from exactly those dims/strides, so every logical index in
295    // `0..total` maps to in-bounds source and destination slots. The shared
296    // and exclusive borrows cannot overlap, and this is the only writer.
297    unsafe {
298        apply_fused_range(
299            dst_ptr, dst_base, src_ptr, src_base, layout, 0, total, &apply, &op,
300        );
301    }
302}
303
304/// Serial replay of a spilled fused axis list over borrowed raw views.
305///
306/// Kept out of line and separate from [`apply_fused_pair`] so that the inline
307/// entry points keep the code shape they had before high-rank fusion existed:
308/// fused ranks above [`RAW_FUSED_RANK_LIMIT`] are the rare case, and inlining
309/// this second carry loop into them measurably slowed the inline path (extra
310/// register pressure in a loop that runs once per short run).
311#[inline(never)]
312pub(crate) fn apply_spilled_pair<D, S, Apply, Op>(
313    dst: &mut RawStridedMut<'_, D>,
314    src: &RawStridedRef<'_, S>,
315    axes: &[FusedAxis],
316    apply: Apply,
317    op: Op,
318) where
319    D: Copy,
320    S: Copy,
321    Apply: Fn(&mut D, S),
322    Op: Fn(S) -> S,
323{
324    let total = spilled_total(axes);
325    if total == 0 {
326        return;
327    }
328    let src_ptr = src.data().as_ptr();
329    let src_base = src.offset();
330    let dst_base = dst.offset();
331    let dst_ptr = dst.data_mut().as_mut_ptr();
332    // SAFETY: same contract as `apply_fused_pair`, for the axis list fused from
333    // the same dims/strides.
334    unsafe {
335        apply_spilled_range(
336            dst_ptr, dst_base, src_ptr, src_base, axes, 0, total, &apply, &op,
337        );
338    }
339}
340
341/// Replay logical indices `start..start + len` of a fused pair layout, in
342/// column-major order of the fused axes (axis 0 fastest).
343///
344/// The start coordinate is decoded once; runs along axis 0 then advance the
345/// outer coordinates incrementally, so no per-element range checks or
346/// coordinate rebuilds remain in the hot loop.
347///
348/// # Safety
349///
350/// - `start + len` must not exceed [`fused_total`] of `layout`.
351/// - For every logical index `i` in the range, `dst_base` plus the fused
352///   destination offset of `i` must be an in-bounds, writable slot of the
353///   allocation behind `dst_ptr`, and likewise `src_base` plus the source
354///   offset must be an in-bounds, readable slot behind `src_ptr`.
355/// - No other thread may access the destination slots of this range while it
356///   runs, and the source slots must not be written concurrently. Destination
357///   and source slots must not overlap.
358#[allow(clippy::too_many_arguments)]
359pub(crate) unsafe fn apply_fused_range<D, S, Apply, Op>(
360    dst_ptr: *mut D,
361    dst_base: isize,
362    src_ptr: *const S,
363    src_base: isize,
364    layout: &FusedPairLayout,
365    start: usize,
366    len: usize,
367    apply: &Apply,
368    op: &Op,
369) where
370    D: Copy,
371    S: Copy,
372    Apply: Fn(&mut D, S),
373    Op: Fn(S) -> S,
374{
375    if len == 0 {
376        return;
377    }
378    let rank = layout.rank;
379    let inner_len = layout.dims[0];
380    let inner_dst = layout.dst_strides[0];
381    let inner_src = layout.src_strides[0];
382
383    // Decode the start coordinate once per range. `dst_offset`/`src_offset`
384    // track the offset of the current run start (outer coordinates plus the
385    // inner coordinate `inner_start`); every value is a reachable offset.
386    let mut index = [0usize; RAW_FUSED_RANK_LIMIT];
387    let mut rest = start;
388    let mut dst_outer = dst_base;
389    let mut src_outer = src_base;
390    for axis in 0..rank {
391        let dim = layout.dims[axis];
392        index[axis] = rest % dim;
393        rest /= dim;
394        if axis > 0 {
395            dst_outer += index[axis] as isize * layout.dst_strides[axis];
396            src_outer += index[axis] as isize * layout.src_strides[axis];
397        }
398    }
399    let mut inner_start = index[0];
400    let mut remaining = len;
401    loop {
402        let run = (inner_len - inner_start).min(remaining);
403        let dst_run = dst_outer + inner_start as isize * inner_dst;
404        let src_run = src_outer + inner_start as isize * inner_src;
405        // SAFETY: the run covers logical indices inside the caller's range, so
406        // by this function's contract every slot touched below is in-bounds,
407        // exclusively owned by this call (destination) and unaliased.
408        unsafe {
409            apply_run(
410                dst_ptr, dst_run, inner_dst, src_ptr, src_run, inner_src, run, apply, op,
411            )
412        };
413        remaining -= run;
414        if remaining == 0 {
415            return;
416        }
417        inner_start = 0;
418        // Advance an axis only when another position along it follows, and
419        // rewind it from its last position, so every intermediate base is a
420        // reachable offset of a validated layout (issue #243 follow-up): a
421        // layout ending within one stride of `isize::MAX` must not overflow.
422        // `remaining > 0` guarantees a following run exists, so the loop
423        // always finds an axis to advance before running out of rank.
424        let mut axis = 1;
425        while axis < rank {
426            if index[axis] + 1 < layout.dims[axis] {
427                index[axis] += 1;
428                dst_outer += layout.dst_strides[axis];
429                src_outer += layout.src_strides[axis];
430                break;
431            }
432            let last = (layout.dims[axis] - 1) as isize;
433            dst_outer -= last * layout.dst_strides[axis];
434            src_outer -= last * layout.src_strides[axis];
435            index[axis] = 0;
436            axis += 1;
437        }
438    }
439}
440
441/// Replay logical indices `start..start + len` of a spilled fused axis list, in
442/// column-major order of the axes (axis 0 fastest).
443///
444/// The carry loop is the one [`apply_fused_range`] uses, over interleaved axes
445/// with its carry state in one rank-bounded `Vec`: no allocation scales with
446/// the tensor size and no per-element coordinate decode happens.
447///
448/// # Safety
449///
450/// The [`apply_fused_range`] contract holds for `axes`.
451///
452/// Kept out of line: inlining this second carry loop into the inline replay
453/// measurably slows that one down.
454#[allow(clippy::too_many_arguments)]
455#[inline(never)]
456pub(crate) unsafe fn apply_spilled_range<D, S, Apply, Op>(
457    dst_ptr: *mut D,
458    dst_base: isize,
459    src_ptr: *const S,
460    src_base: isize,
461    axes: &[FusedAxis],
462    start: usize,
463    len: usize,
464    apply: &Apply,
465    op: &Op,
466) where
467    D: Copy,
468    S: Copy,
469    Apply: Fn(&mut D, S),
470    Op: Fn(S) -> S,
471{
472    if len == 0 {
473        return;
474    }
475    let rank = axes.len();
476    let inner_len = axes[0].dim;
477    let inner_dst = axes[0].dst;
478    let inner_src = axes[0].src;
479
480    let mut index = vec![0usize; rank];
481    let mut rest = start;
482    let mut dst_outer = dst_base;
483    let mut src_outer = src_base;
484    for axis in 0..rank {
485        let dim = axes[axis].dim;
486        index[axis] = rest % dim;
487        rest /= dim;
488        if axis > 0 {
489            dst_outer += index[axis] as isize * axes[axis].dst;
490            src_outer += index[axis] as isize * axes[axis].src;
491        }
492    }
493    let mut inner_start = index[0];
494    let mut remaining = len;
495    loop {
496        let run = (inner_len - inner_start).min(remaining);
497        let dst_run = dst_outer + inner_start as isize * inner_dst;
498        let src_run = src_outer + inner_start as isize * inner_src;
499        // SAFETY: the run covers logical indices inside the caller's range, so
500        // by the caller's contract every slot touched below is in-bounds,
501        // exclusively owned by this call (destination) and unaliased.
502        unsafe {
503            apply_run(
504                dst_ptr, dst_run, inner_dst, src_ptr, src_run, inner_src, run, apply, op,
505            )
506        };
507        remaining -= run;
508        if remaining == 0 {
509            return;
510        }
511        inner_start = 0;
512        // Same incremental carry as the inline replay: offsets only ever hold
513        // reachable positions, and `remaining > 0` guarantees an axis advances.
514        let mut axis = 1;
515        while axis < rank {
516            if index[axis] + 1 < axes[axis].dim {
517                index[axis] += 1;
518                dst_outer += axes[axis].dst;
519                src_outer += axes[axis].src;
520                break;
521            }
522            let last = (axes[axis].dim - 1) as isize;
523            dst_outer -= last * axes[axis].dst;
524            src_outer -= last * axes[axis].src;
525            index[axis] = 0;
526            axis += 1;
527        }
528    }
529}
530
531/// One run along the fused inner axis.
532///
533/// # Safety
534///
535/// Every `dst_offset + k * dst_stride` and `src_offset + k * src_stride` for
536/// `k < len` must be an in-bounds slot as described in [`apply_fused_range`],
537/// with exclusive access to the destination slots.
538#[allow(clippy::too_many_arguments)]
539#[inline(always)]
540unsafe fn apply_run<D, S, Apply, Op>(
541    dst_ptr: *mut D,
542    dst_offset: isize,
543    dst_stride: isize,
544    src_ptr: *const S,
545    src_offset: isize,
546    src_stride: isize,
547    len: usize,
548    apply: &Apply,
549    op: &Op,
550) where
551    D: Copy,
552    S: Copy,
553    Apply: Fn(&mut D, S),
554    Op: Fn(S) -> S,
555{
556    if dst_stride == 1 {
557        // SAFETY: a unit destination stride makes the run `len` consecutive
558        // in-bounds slots starting at `dst_offset`, exclusively owned here.
559        let dst_run = unsafe { core::slice::from_raw_parts_mut(dst_ptr.offset(dst_offset), len) };
560        match src_stride {
561            1 => {
562                // SAFETY: `len` consecutive in-bounds source slots.
563                let src_run =
564                    unsafe { core::slice::from_raw_parts(src_ptr.offset(src_offset), len) };
565                for (dst, &value) in dst_run.iter_mut().zip(src_run) {
566                    apply(dst, op(value));
567                }
568            }
569            -1 => {
570                // SAFETY: the run reads `src_offset - (len - 1) ..= src_offset`,
571                // all in-bounds; the lowest one is the run's last source slot.
572                let src_run = unsafe {
573                    core::slice::from_raw_parts(
574                        src_ptr.offset(src_offset - (len as isize - 1)),
575                        len,
576                    )
577                };
578                for (dst, &value) in dst_run.iter_mut().zip(src_run.iter().rev()) {
579                    apply(dst, op(value));
580                }
581            }
582            _ => {
583                let src_start = unsafe { src_ptr.offset(src_offset) };
584                for (position, dst) in dst_run.iter_mut().enumerate() {
585                    // SAFETY: `position < len`, so this is a run source slot.
586                    let value = unsafe { *src_start.offset(position as isize * src_stride) };
587                    apply(dst, op(value));
588                }
589            }
590        }
591        return;
592    }
593    // SAFETY: the run start is an in-bounds slot of each allocation.
594    let dst_start = unsafe { dst_ptr.offset(dst_offset) };
595    let src_start = unsafe { src_ptr.offset(src_offset) };
596    for position in 0..len as isize {
597        // SAFETY: `position < len`, so both are slots of this run.
598        unsafe {
599            let value = *src_start.offset(position * src_stride);
600            apply(&mut *dst_start.offset(position * dst_stride), op(value));
601        }
602    }
603}
604
605fn ensure_same_dims(dst: &[usize], src: &[usize]) -> Result<()> {
606    if dst != src {
607        return Err(crate::StridedError::ShapeMismatch(
608            dst.to_vec(),
609            src.to_vec(),
610        ));
611    }
612    // Reject an element count beyond usize (a huge stride-0 broadcast)
613    // instead of replaying it.
614    crate::kernel::total_len(dst)?;
615    Ok(())
616}
617
618/// `dest = scale * src` over borrowed raw strided layouts.
619pub fn copy_scale_raw<T>(
620    dest: &mut RawStridedMut<'_, T>,
621    src: &RawStridedRef<'_, T>,
622    scale: T,
623) -> Result<()>
624where
625    T: Copy + Mul<T, Output = T> + ElementOpApply + MaybeSendSync,
626{
627    ensure_same_dims(dest.dims(), src.dims())?;
628    match fuse_pair_traversal(dest.dims(), dest.strides(), src.strides()) {
629        PairTraversal::Inline(layout) => apply_fused_pair(
630            dest,
631            src,
632            &layout,
633            |dst, value| *dst = value,
634            |value: T| scale * value,
635        ),
636        PairTraversal::Spilled(axes) => apply_spilled_pair(
637            dest,
638            src,
639            &axes,
640            |dst, value| *dst = value,
641            |value: T| scale * value,
642        ),
643    }
644    Ok(())
645}
646
647/// `dest = scale * conj(src)` over borrowed raw strided layouts.
648pub fn copy_scale_conj_raw<T>(
649    dest: &mut RawStridedMut<'_, T>,
650    src: &RawStridedRef<'_, T>,
651    scale: T,
652) -> Result<()>
653where
654    T: Copy + Mul<T, Output = T> + ElementOpApply + MaybeSendSync,
655{
656    ensure_same_dims(dest.dims(), src.dims())?;
657    match fuse_pair_traversal(dest.dims(), dest.strides(), src.strides()) {
658        PairTraversal::Inline(layout) => apply_fused_pair(
659            dest,
660            src,
661            &layout,
662            |dst, value| *dst = value,
663            |value: T| scale * value.conj(),
664        ),
665        PairTraversal::Spilled(axes) => apply_spilled_pair(
666            dest,
667            src,
668            &axes,
669            |dst, value| *dst = value,
670            |value: T| scale * value.conj(),
671        ),
672    }
673    Ok(())
674}
675
676/// `dest = alpha * src + dest` over borrowed raw strided layouts.
677pub fn axpy_raw<T>(
678    dest: &mut RawStridedMut<'_, T>,
679    src: &RawStridedRef<'_, T>,
680    alpha: T,
681) -> Result<()>
682where
683    T: Copy + Add<T, Output = T> + Mul<T, Output = T> + ElementOpApply + MaybeSendSync,
684{
685    ensure_same_dims(dest.dims(), src.dims())?;
686    match fuse_pair_traversal(dest.dims(), dest.strides(), src.strides()) {
687        PairTraversal::Inline(layout) => apply_fused_pair(
688            dest,
689            src,
690            &layout,
691            |dst, value| *dst = *dst + value,
692            |value: T| alpha * value,
693        ),
694        PairTraversal::Spilled(axes) => apply_spilled_pair(
695            dest,
696            src,
697            &axes,
698            |dst, value| *dst = *dst + value,
699            |value: T| alpha * value,
700        ),
701    }
702    Ok(())
703}
704
705/// `dest = alpha * conj(src) + dest` over borrowed raw strided layouts.
706pub fn axpy_conj_raw<T>(
707    dest: &mut RawStridedMut<'_, T>,
708    src: &RawStridedRef<'_, T>,
709    alpha: T,
710) -> Result<()>
711where
712    T: Copy + Add<T, Output = T> + Mul<T, Output = T> + ElementOpApply + MaybeSendSync,
713{
714    ensure_same_dims(dest.dims(), src.dims())?;
715    match fuse_pair_traversal(dest.dims(), dest.strides(), src.strides()) {
716        PairTraversal::Inline(layout) => apply_fused_pair(
717            dest,
718            src,
719            &layout,
720            |dst, value| *dst = *dst + value,
721            |value: T| alpha * value.conj(),
722        ),
723        PairTraversal::Spilled(axes) => apply_spilled_pair(
724            dest,
725            src,
726            &axes,
727            |dst, value| *dst = *dst + value,
728            |value: T| alpha * value.conj(),
729        ),
730    }
731    Ok(())
732}
733
734#[cfg(test)]
735#[path = "raw_ops/tests/tests.rs"]
736mod tests;