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