Skip to main content

strided_kernel/
gather_plan.rs

1//! Prepared indexed plans over raw strided value and index layouts.
2//!
3//! This module owns the generic gather, dynamic-slice/update, and scatter
4//! traversals used by the erased replay layer. It models the XLA/tenferro
5//! indexed shape vocabulary, but keeps tensor allocation, dtype promotion, and
6//! frontend error policy outside `strided-kernel`.
7
8use core::{mem::MaybeUninit, ops::Add};
9
10use crate::copy_plan::{CopyPlan, OverwriteWriter, ReadModifyWrite};
11use crate::{
12    MaybeSendSync, RawStridedMut, RawStridedRef, Result, StridedError, RAW_FUSED_RANK_LIMIT,
13};
14
15#[cfg(feature = "parallel")]
16type AxisVec<T> = smallvec::SmallVec<[T; RAW_FUSED_RANK_LIMIT]>;
17#[cfg(not(feature = "parallel"))]
18type AxisVec<T> = Vec<T>;
19
20/// Gather configuration shared by generic and erased replay.
21///
22/// The fields follow the usual gather vocabulary:
23///
24/// - `start_index_map[component]` names the operand axis controlled by a
25///   component in the index vector;
26/// - `collapsed_slice_dims` names operand axes whose slice size is one and
27///   which do not appear as output window axes;
28/// - `offset_dims` names output axes that represent window offsets;
29/// - output axes not in `offset_dims` are batch axes from `start_indices`;
30/// - `index_vector_dim == start_indices_rank` represents scalar index vectors.
31#[derive(Clone, Debug, Eq, PartialEq)]
32pub struct GatherSpec {
33    pub offset_dims: Vec<usize>,
34    pub collapsed_slice_dims: Vec<usize>,
35    pub start_index_map: Vec<usize>,
36    pub index_vector_dim: usize,
37    pub slice_sizes: Vec<usize>,
38}
39
40/// Index scalar types accepted by [`GatherPlan`].
41pub trait GatherIndex: Copy + MaybeSendSync {
42    fn to_i64(self) -> i64;
43}
44
45impl GatherIndex for i32 {
46    #[inline]
47    fn to_i64(self) -> i64 {
48        i64::from(self)
49    }
50}
51
52impl GatherIndex for i64 {
53    #[inline]
54    fn to_i64(self) -> i64 {
55        self
56    }
57}
58
59/// A compiled gather traversal for one value layout, index layout, and output layout.
60#[derive(Clone, Debug)]
61pub struct GatherPlan {
62    operand_dims: AxisVec<usize>,
63    operand_strides: AxisVec<isize>,
64    index_dims: AxisVec<usize>,
65    index_strides: AxisVec<isize>,
66    dest_dims: AxisVec<usize>,
67    dest_strides: AxisVec<isize>,
68    spec: GatherSpec,
69    batch_shape: AxisVec<usize>,
70    out_axis_to_operand_dim: AxisVec<Option<usize>>,
71    total: usize,
72}
73
74/// Scatter configuration shared by generic and erased replay.
75///
76/// `ScatterPlan` implements tenferro's current additive scatter semantics:
77/// every update value is added to the selected output slot, so overlapping
78/// windows accumulate in deterministic column-major replay order.
79#[derive(Clone, Debug, Eq, PartialEq)]
80pub struct ScatterSpec {
81    pub update_window_dims: Vec<usize>,
82    pub inserted_window_dims: Vec<usize>,
83    pub scatter_dims_to_operand_dims: Vec<usize>,
84    pub index_vector_dim: usize,
85}
86
87/// A compiled fixed-window dynamic-slice traversal.
88#[derive(Clone, Debug)]
89pub struct DynamicSlicePlan {
90    operand_dims: AxisVec<usize>,
91    operand_strides: AxisVec<isize>,
92    start_dims: AxisVec<usize>,
93    start_strides: AxisVec<isize>,
94    dest_dims: AxisVec<usize>,
95    dest_strides: AxisVec<isize>,
96    slice_sizes: AxisVec<usize>,
97    total: usize,
98}
99
100/// A compiled dynamic-update-slice traversal.
101///
102/// Execution first copies `operand` into `dest`, then overwrites the clamped
103/// update window. The plan performs no allocation for ranks at most
104/// [`RAW_FUSED_RANK_LIMIT`].
105#[derive(Clone, Debug)]
106pub struct DynamicUpdateSlicePlan {
107    operand_dims: AxisVec<usize>,
108    operand_strides: AxisVec<isize>,
109    start_dims: AxisVec<usize>,
110    start_strides: AxisVec<isize>,
111    update_dims: AxisVec<usize>,
112    update_strides: AxisVec<isize>,
113    dest_dims: AxisVec<usize>,
114    dest_strides: AxisVec<isize>,
115    total: usize,
116    copy_plan: CopyPlan,
117}
118
119/// A compiled additive scatter traversal.
120///
121/// Execution first copies `operand` into `dest`, then applies additive updates
122/// in deterministic column-major order. Boolean values are intentionally not
123/// supported because additive scatter has no bool semantics.
124#[derive(Clone, Debug)]
125pub struct ScatterPlan {
126    operand_dims: AxisVec<usize>,
127    operand_strides: AxisVec<isize>,
128    index_dims: AxisVec<usize>,
129    index_strides: AxisVec<isize>,
130    update_dims: AxisVec<usize>,
131    update_strides: AxisVec<isize>,
132    dest_dims: AxisVec<usize>,
133    dest_strides: AxisVec<isize>,
134    spec: ScatterSpec,
135    batch_shape: AxisVec<usize>,
136    window_dims: AxisVec<usize>,
137    window_shape: AxisVec<usize>,
138    window_shape_updates: AxisVec<usize>,
139    is_update_window_dim: AxisVec<bool>,
140    batch_elems: usize,
141    window_elems: usize,
142    copy_plan: CopyPlan,
143}
144
145impl GatherPlan {
146    /// Compile a gather plan for fixed operand, index, and destination layouts.
147    pub fn compile(
148        operand_dims: &[usize],
149        operand_strides: &[isize],
150        index_dims: &[usize],
151        index_strides: &[isize],
152        dest_dims: &[usize],
153        dest_strides: &[isize],
154        spec: GatherSpec,
155    ) -> Result<Self> {
156        if operand_dims.len() != operand_strides.len()
157            || index_dims.len() != index_strides.len()
158            || dest_dims.len() != dest_strides.len()
159        {
160            return Err(StridedError::StrideLengthMismatch);
161        }
162        checked_total_len(operand_dims)?;
163        checked_total_len(index_dims)?;
164        let total = checked_total_len(dest_dims)?;
165        if !crate::fused::is_injective_layout(dest_dims, dest_strides) {
166            return Err(StridedError::NonInjectiveOutputLayout);
167        }
168
169        let operand_rank = operand_dims.len();
170        if spec.slice_sizes.len() != operand_rank {
171            return Err(StridedError::RankMismatch(
172                spec.slice_sizes.len(),
173                operand_rank,
174            ));
175        }
176        validate_unique_axes(&spec.collapsed_slice_dims, operand_rank)?;
177        validate_unique_axes(&spec.start_index_map, operand_rank)?;
178        if spec.index_vector_dim > index_dims.len() {
179            return Err(StridedError::InvalidAxis {
180                axis: spec.index_vector_dim,
181                rank: index_dims.len() + 1,
182            });
183        }
184
185        for (axis, (&window, &dim)) in spec.slice_sizes.iter().zip(operand_dims.iter()).enumerate()
186        {
187            if window > dim {
188                return Err(StridedError::InvalidAxis {
189                    axis,
190                    rank: operand_rank,
191                });
192            }
193        }
194        for &axis in &spec.collapsed_slice_dims {
195            if spec.slice_sizes[axis] != 1 {
196                return Err(StridedError::InvalidAxis {
197                    axis,
198                    rank: operand_rank,
199                });
200            }
201        }
202
203        let index_vector_size = if spec.index_vector_dim == index_dims.len() {
204            1
205        } else {
206            index_dims[spec.index_vector_dim]
207        };
208        if index_vector_size != spec.start_index_map.len() {
209            return Err(StridedError::RankMismatch(
210                index_vector_size,
211                spec.start_index_map.len(),
212            ));
213        }
214
215        let window_dims = operand_window_dims(operand_rank, &spec.collapsed_slice_dims);
216        if spec.offset_dims.len() != window_dims.len() {
217            return Err(StridedError::RankMismatch(
218                spec.offset_dims.len(),
219                window_dims.len(),
220            ));
221        }
222
223        let batch_shape = index_batch_shape(index_dims, spec.index_vector_dim);
224        let out_rank = batch_shape.len() + spec.offset_dims.len();
225        validate_unique_axes(&spec.offset_dims, out_rank)?;
226
227        let mut out_axis_to_operand_dim: AxisVec<Option<usize>> =
228            (0..out_rank).map(|_| None).collect();
229        for (offset_axis, &out_axis) in spec.offset_dims.iter().enumerate() {
230            out_axis_to_operand_dim[out_axis] = Some(window_dims[offset_axis]);
231        }
232
233        let mut expected_dest_dims: AxisVec<usize> = AxisVec::with_capacity(out_rank);
234        let mut batch_axis = 0usize;
235        for &operand_dim in &out_axis_to_operand_dim {
236            match operand_dim {
237                Some(axis) => expected_dest_dims.push(spec.slice_sizes[axis]),
238                None => {
239                    expected_dest_dims.push(batch_shape[batch_axis]);
240                    batch_axis += 1;
241                }
242            }
243        }
244        if dest_dims != &expected_dest_dims[..] {
245            return Err(StridedError::ShapeMismatch(
246                dest_dims.to_vec(),
247                expected_dest_dims.to_vec(),
248            ));
249        }
250
251        Ok(Self {
252            operand_dims: operand_dims.into(),
253            operand_strides: operand_strides.into(),
254            index_dims: index_dims.into(),
255            index_strides: index_strides.into(),
256            dest_dims: dest_dims.into(),
257            dest_strides: dest_strides.into(),
258            spec,
259            batch_shape,
260            out_axis_to_operand_dim,
261            total,
262        })
263    }
264
265    #[inline]
266    pub fn spec(&self) -> &GatherSpec {
267        &self.spec
268    }
269
270    #[inline]
271    pub fn dest_dims(&self) -> &[usize] {
272        &self.dest_dims
273    }
274
275    /// Execute the prepared gather traversal.
276    pub fn execute<T, I>(
277        &self,
278        dest: &mut RawStridedMut<'_, T>,
279        operand: &RawStridedRef<'_, T>,
280        start_indices: &RawStridedRef<'_, I>,
281    ) -> Result<()>
282    where
283        T: Copy + MaybeSendSync,
284        I: GatherIndex,
285    {
286        self.execute_with_writer(dest, operand, start_indices)
287    }
288
289    /// Execute the prepared gather into a destination whose reachable slots
290    /// may be uninitialized. Every logical destination slot is written.
291    pub(crate) fn execute_uninit<T, I>(
292        &self,
293        dest: &mut RawStridedMut<'_, MaybeUninit<T>>,
294        operand: &RawStridedRef<'_, T>,
295        start_indices: &RawStridedRef<'_, I>,
296    ) -> Result<()>
297    where
298        T: Copy + MaybeSendSync,
299        I: GatherIndex,
300    {
301        self.execute_with_writer(dest, operand, start_indices)
302    }
303
304    fn execute_with_writer<T, I, W>(
305        &self,
306        dest: &mut W,
307        operand: &RawStridedRef<'_, T>,
308        start_indices: &RawStridedRef<'_, I>,
309    ) -> Result<()>
310    where
311        T: Copy + MaybeSendSync,
312        I: GatherIndex,
313        W: OverwriteWriter<T>,
314    {
315        self.check_call(dest, operand, start_indices)?;
316        if self.total == 0 {
317            return Ok(());
318        }
319        #[cfg(feature = "parallel")]
320        {
321            let nthreads = crate::threading::parallel_threads_for_len(self.total);
322            if nthreads > 1 {
323                return self.execute_parallel(dest, operand, start_indices, nthreads);
324            }
325        }
326
327        let mut out_idx_storage = CoordScratch::new(self.dest_dims.len());
328        let mut batch_idx_storage = CoordScratch::new(self.batch_shape.len());
329        let mut operand_idx_storage = CoordScratch::new(self.operand_dims.len());
330        let mut window_offsets_storage = CoordScratch::new(self.operand_dims.len());
331        let out_idx = out_idx_storage.as_mut_slice();
332        let batch_idx = batch_idx_storage.as_mut_slice();
333        let operand_idx = operand_idx_storage.as_mut_slice();
334        let window_offsets = window_offsets_storage.as_mut_slice();
335
336        let dest_offset_base = dest.offset();
337        let operand_offset_base = operand.offset();
338        let operand_strides = operand.strides();
339        let index_offset_base = start_indices.offset();
340        let index_strides = start_indices.strides();
341        let operand_data = operand.data();
342        let index_data = start_indices.data();
343
344        for _ in 0..self.total {
345            window_offsets.fill(0);
346            let mut batch_axis = 0usize;
347            for (out_axis, &operand_dim) in self.out_axis_to_operand_dim.iter().enumerate() {
348                match operand_dim {
349                    Some(axis) => window_offsets[axis] = out_idx[out_axis],
350                    None => {
351                        batch_idx[batch_axis] = out_idx[out_axis];
352                        batch_axis += 1;
353                    }
354                }
355            }
356
357            operand_idx.fill(0);
358            for (component, &operand_dim) in self.spec.start_index_map.iter().enumerate() {
359                let start = self.index_component(
360                    start_indices.dims(),
361                    index_strides,
362                    index_offset_base,
363                    index_data,
364                    &batch_idx,
365                    component,
366                )?;
367                operand_idx[operand_dim] = self.clamp_window_start(start, operand_dim);
368            }
369            for axis in 0..operand_idx.len() {
370                operand_idx[axis] += window_offsets[axis];
371            }
372
373            let dest_offset = checked_strided_offset(dest_offset_base, dest.strides(), &out_idx)?;
374            let operand_offset =
375                checked_strided_offset(operand_offset_base, operand_strides, &operand_idx)?;
376            // SAFETY: the validated operand layout proves this source offset.
377            let value = unsafe { *operand_data.as_ptr().offset(operand_offset) };
378            // SAFETY: the validated plan proves this logical offset is in-bounds.
379            unsafe { dest.write_at(dest_offset, value) };
380            advance_col_major_index(out_idx, &self.dest_dims);
381        }
382        Ok(())
383    }
384
385    #[cfg(feature = "parallel")]
386    fn execute_parallel<T, I, W>(
387        &self,
388        dest: &mut W,
389        operand: &RawStridedRef<'_, T>,
390        start_indices: &RawStridedRef<'_, I>,
391        nthreads: usize,
392    ) -> Result<()>
393    where
394        T: Copy + MaybeSendSync,
395        I: GatherIndex,
396        W: OverwriteWriter<T>,
397    {
398        let dest_offset_base = dest.offset();
399        let operand_offset_base = operand.offset();
400        let index_offset_base = start_indices.offset();
401        // SAFETY: the validated writer owns the destination allocation.
402        let dest_ptr = crate::threading::SendPtr(unsafe { dest.data_ptr() });
403        let operand_ptr = crate::threading::SendPtr(operand.data().as_ptr() as *mut T);
404        let index_ptr = crate::threading::SendPtr(start_indices.data().as_ptr() as *mut I);
405
406        crate::threading::parallel_map_reduce(
407            0..self.total,
408            nthreads,
409            &|range| {
410                let mut out_idx_storage = CoordScratch::new(self.dest_dims.len());
411                let mut batch_idx_storage = CoordScratch::new(self.batch_shape.len());
412                let mut operand_idx_storage = CoordScratch::new(self.operand_dims.len());
413                let mut window_offsets_storage = CoordScratch::new(self.operand_dims.len());
414                let out_idx = out_idx_storage.as_mut_slice();
415                let batch_idx = batch_idx_storage.as_mut_slice();
416                let operand_idx = operand_idx_storage.as_mut_slice();
417                let window_offsets = window_offsets_storage.as_mut_slice();
418                fill_col_major_index(range.start, &self.dest_dims, out_idx);
419                let dest_ptr = dest_ptr.as_ptr();
420                let operand_ptr = operand_ptr.as_const();
421                let index_ptr = index_ptr.as_const();
422
423                for _ in range {
424                    window_offsets.fill(0);
425                    let mut batch_axis = 0usize;
426                    for (out_axis, &operand_dim) in self.out_axis_to_operand_dim.iter().enumerate()
427                    {
428                        match operand_dim {
429                            Some(axis) => window_offsets[axis] = out_idx[out_axis],
430                            None => {
431                                batch_idx[batch_axis] = out_idx[out_axis];
432                                batch_axis += 1;
433                            }
434                        }
435                    }
436
437                    operand_idx.fill(0);
438                    for (component, &operand_dim) in self.spec.start_index_map.iter().enumerate() {
439                        let start = self.index_component_ptr(
440                            start_indices.dims(),
441                            index_offset_base,
442                            index_ptr,
443                            batch_idx,
444                            component,
445                        )?;
446                        operand_idx[operand_dim] = self.clamp_window_start(start, operand_dim);
447                    }
448                    for axis in 0..operand_idx.len() {
449                        operand_idx[axis] += window_offsets[axis];
450                    }
451
452                    let dest_offset =
453                        checked_strided_offset(dest_offset_base, &self.dest_strides, out_idx)?;
454                    let operand_offset = checked_strided_offset(
455                        operand_offset_base,
456                        &self.operand_strides,
457                        operand_idx,
458                    )?;
459                    unsafe {
460                        // SAFETY: gather writes one value per logical output,
461                        // and compile rejected non-injective destination
462                        // layouts.
463                        dest_ptr
464                            .offset(dest_offset)
465                            .write(operand_ptr.offset(operand_offset).read());
466                    }
467                    advance_col_major_index(out_idx, &self.dest_dims);
468                }
469                Ok(())
470            },
471            &|left, right| left.and(right),
472        )
473    }
474
475    fn check_call<T, I, W>(
476        &self,
477        dest: &W,
478        operand: &RawStridedRef<'_, T>,
479        start_indices: &RawStridedRef<'_, I>,
480    ) -> Result<()>
481    where
482        W: OverwriteWriter<T>,
483    {
484        if dest.dims() != &self.dest_dims[..]
485            || dest.strides() != &self.dest_strides[..]
486            || operand.dims() != &self.operand_dims[..]
487            || operand.strides() != &self.operand_strides[..]
488            || start_indices.dims() != &self.index_dims[..]
489            || start_indices.strides() != &self.index_strides[..]
490        {
491            return Err(StridedError::PlanLayoutMismatch);
492        }
493        Ok(())
494    }
495
496    fn index_component<I>(
497        &self,
498        index_dims: &[usize],
499        index_strides: &[isize],
500        index_offset_base: isize,
501        index_data: &[I],
502        batch_idx: &[usize],
503        component: usize,
504    ) -> Result<i64>
505    where
506        I: GatherIndex,
507    {
508        let mut offset = index_offset_base;
509        let mut batch_axis = 0usize;
510        for axis in 0..index_dims.len() {
511            let coord = if axis == self.spec.index_vector_dim {
512                component
513            } else {
514                let coord = batch_idx[batch_axis];
515                batch_axis += 1;
516                coord
517            };
518            offset = checked_offset_add(offset, index_strides[axis], coord)?;
519        }
520        Ok(unsafe { *index_data.as_ptr().offset(offset) }.to_i64())
521    }
522
523    #[cfg(feature = "parallel")]
524    fn index_component_ptr<I>(
525        &self,
526        index_dims: &[usize],
527        index_offset_base: isize,
528        index_ptr: *const I,
529        batch_idx: &[usize],
530        component: usize,
531    ) -> Result<i64>
532    where
533        I: GatherIndex,
534    {
535        let mut offset = index_offset_base;
536        let mut batch_axis = 0usize;
537        for axis in 0..index_dims.len() {
538            let coord = if axis == self.spec.index_vector_dim {
539                component
540            } else {
541                let coord = batch_idx[batch_axis];
542                batch_axis += 1;
543                coord
544            };
545            offset = checked_offset_add(offset, self.index_strides[axis], coord)?;
546        }
547        Ok(unsafe { *index_ptr.offset(offset) }.to_i64())
548    }
549
550    #[inline]
551    fn clamp_window_start(&self, start: i64, operand_dim: usize) -> usize {
552        let dim_size = self.operand_dims[operand_dim];
553        let window_size = self.spec.slice_sizes[operand_dim];
554        let max_start = dim_size.saturating_sub(window_size) as i64;
555        start.clamp(0, max_start) as usize
556    }
557}
558
559impl DynamicSlicePlan {
560    /// Compile a fixed-window dynamic-slice traversal.
561    ///
562    /// `start_*` describes a rank-1 index vector whose length equals the
563    /// operand rank. `dest_dims` must equal `slice_sizes`.
564    pub fn compile(
565        operand_dims: &[usize],
566        operand_strides: &[isize],
567        start_dims: &[usize],
568        start_strides: &[isize],
569        dest_dims: &[usize],
570        dest_strides: &[isize],
571        slice_sizes: &[usize],
572    ) -> Result<Self> {
573        if operand_dims.len() != operand_strides.len()
574            || start_dims.len() != start_strides.len()
575            || dest_dims.len() != dest_strides.len()
576        {
577            return Err(StridedError::StrideLengthMismatch);
578        }
579        if slice_sizes.len() != operand_dims.len() {
580            return Err(StridedError::RankMismatch(
581                slice_sizes.len(),
582                operand_dims.len(),
583            ));
584        }
585        validate_start_vector(start_dims, operand_dims.len())?;
586        checked_total_len(operand_dims)?;
587        checked_total_len(start_dims)?;
588        let total = checked_total_len(dest_dims)?;
589        if dest_dims != slice_sizes {
590            return Err(StridedError::ShapeMismatch(
591                dest_dims.to_vec(),
592                slice_sizes.to_vec(),
593            ));
594        }
595        if !crate::fused::is_injective_layout(dest_dims, dest_strides) {
596            return Err(StridedError::NonInjectiveOutputLayout);
597        }
598        validate_window_sizes(operand_dims, slice_sizes)?;
599
600        Ok(Self {
601            operand_dims: operand_dims.into(),
602            operand_strides: operand_strides.into(),
603            start_dims: start_dims.into(),
604            start_strides: start_strides.into(),
605            dest_dims: dest_dims.into(),
606            dest_strides: dest_strides.into(),
607            slice_sizes: slice_sizes.into(),
608            total,
609        })
610    }
611
612    /// Execute the prepared dynamic-slice traversal.
613    pub fn execute<T, I>(
614        &self,
615        dest: &mut RawStridedMut<'_, T>,
616        operand: &RawStridedRef<'_, T>,
617        starts: &RawStridedRef<'_, I>,
618    ) -> Result<()>
619    where
620        T: Copy + MaybeSendSync,
621        I: GatherIndex,
622    {
623        self.execute_with_writer(dest, operand, starts)
624    }
625
626    pub(crate) fn execute_uninit<T, I>(
627        &self,
628        dest: &mut RawStridedMut<'_, MaybeUninit<T>>,
629        operand: &RawStridedRef<'_, T>,
630        starts: &RawStridedRef<'_, I>,
631    ) -> Result<()>
632    where
633        T: Copy + MaybeSendSync,
634        I: GatherIndex,
635    {
636        self.execute_with_writer(dest, operand, starts)
637    }
638
639    fn execute_with_writer<T, I, W>(
640        &self,
641        dest: &mut W,
642        operand: &RawStridedRef<'_, T>,
643        starts: &RawStridedRef<'_, I>,
644    ) -> Result<()>
645    where
646        T: Copy + MaybeSendSync,
647        I: GatherIndex,
648        W: OverwriteWriter<T>,
649    {
650        self.check_call(dest, operand, starts)?;
651        if self.total == 0 {
652            return Ok(());
653        }
654        if self.uses_rank_one_contiguous_path() {
655            return self.execute_rank_one_contiguous(dest, operand, starts);
656        }
657        #[cfg(feature = "parallel")]
658        {
659            let nthreads = crate::threading::parallel_threads_for_len(self.total);
660            if nthreads > 1 {
661                return self.execute_parallel(dest, operand, starts, nthreads);
662            }
663        }
664
665        let mut starts_storage = CoordScratch::new(self.operand_dims.len());
666        let mut dest_idx_storage = CoordScratch::new(self.dest_dims.len());
667        let mut operand_idx_storage = CoordScratch::new(self.operand_dims.len());
668        let clamped_starts = starts_storage.as_mut_slice();
669        let dest_idx = dest_idx_storage.as_mut_slice();
670        let operand_idx = operand_idx_storage.as_mut_slice();
671        read_clamped_starts(
672            starts,
673            &self.operand_dims,
674            &self.slice_sizes,
675            clamped_starts,
676        )?;
677
678        let operand_offset_base = operand.offset();
679        let operand_strides = operand.strides();
680        let dest_offset_base = dest.offset();
681        let operand_data = operand.data();
682
683        for _ in 0..self.total {
684            for axis in 0..operand_idx.len() {
685                operand_idx[axis] = clamped_starts[axis] + dest_idx[axis];
686            }
687            let operand_offset =
688                checked_strided_offset(operand_offset_base, operand_strides, operand_idx)?;
689            let dest_offset = checked_strided_offset(dest_offset_base, dest.strides(), dest_idx)?;
690            // SAFETY: the validated plan proves both offsets.
691            let value = unsafe { *operand_data.as_ptr().offset(operand_offset) };
692            // SAFETY: the validated plan proves this logical offset is in-bounds.
693            unsafe { dest.write_at(dest_offset, value) };
694            advance_col_major_index(dest_idx, &self.dest_dims);
695        }
696        Ok(())
697    }
698
699    #[inline]
700    fn uses_rank_one_contiguous_path(&self) -> bool {
701        self.operand_dims.len() == 1 && self.operand_strides[0] == 1 && self.dest_strides[0] == 1
702    }
703
704    fn execute_rank_one_contiguous<T, I, W>(
705        &self,
706        dest: &mut W,
707        operand: &RawStridedRef<'_, T>,
708        starts: &RawStridedRef<'_, I>,
709    ) -> Result<()>
710    where
711        T: Copy,
712        I: GatherIndex,
713        W: OverwriteWriter<T>,
714    {
715        let mut clamped_starts = [0usize; 1];
716        read_clamped_starts(
717            starts,
718            &self.operand_dims,
719            &self.slice_sizes,
720            &mut clamped_starts,
721        )?;
722        let source_start = checked_offset_add(operand.offset(), 1, clamped_starts[0])?;
723        let source_start =
724            usize::try_from(source_start).map_err(|_| StridedError::OffsetOverflow)?;
725        let dest_start =
726            usize::try_from(dest.offset()).map_err(|_| StridedError::OffsetOverflow)?;
727        let source_end = source_start
728            .checked_add(self.total)
729            .ok_or(StridedError::OffsetOverflow)?;
730        let source = operand
731            .data()
732            .get(source_start..source_end)
733            .ok_or(StridedError::OffsetOverflow)?;
734        // SAFETY: the validated writer owns the destination allocation.
735        let dest_ptr = unsafe { dest.data_ptr() };
736        // SAFETY: bounds were checked above and the writer owns the logical
737        // destination storage.
738        unsafe {
739            core::ptr::copy_nonoverlapping(source.as_ptr(), dest_ptr.add(dest_start), self.total);
740        }
741        Ok(())
742    }
743
744    #[cfg(feature = "parallel")]
745    fn execute_parallel<T, I, W>(
746        &self,
747        dest: &mut W,
748        operand: &RawStridedRef<'_, T>,
749        starts: &RawStridedRef<'_, I>,
750        nthreads: usize,
751    ) -> Result<()>
752    where
753        T: Copy + MaybeSendSync,
754        I: GatherIndex,
755        W: OverwriteWriter<T>,
756    {
757        let mut clamped_starts: AxisVec<usize> = (0..self.operand_dims.len()).map(|_| 0).collect();
758        read_clamped_starts(
759            starts,
760            &self.operand_dims,
761            &self.slice_sizes,
762            &mut clamped_starts,
763        )?;
764
765        let operand_offset_base = operand.offset();
766        let dest_offset_base = dest.offset();
767        let operand_ptr = crate::threading::SendPtr(operand.data().as_ptr() as *mut T);
768        // SAFETY: the validated writer owns the destination allocation.
769        let dest_ptr = crate::threading::SendPtr(unsafe { dest.data_ptr() });
770
771        crate::threading::parallel_map_reduce(
772            0..self.total,
773            nthreads,
774            &|range| {
775                let mut dest_idx_storage = CoordScratch::new(self.dest_dims.len());
776                let mut operand_idx_storage = CoordScratch::new(self.operand_dims.len());
777                let dest_idx = dest_idx_storage.as_mut_slice();
778                let operand_idx = operand_idx_storage.as_mut_slice();
779                fill_col_major_index(range.start, &self.dest_dims, dest_idx);
780                let operand_ptr = operand_ptr.as_const();
781                let dest_ptr = dest_ptr.as_ptr();
782
783                for _ in range {
784                    for axis in 0..operand_idx.len() {
785                        operand_idx[axis] = clamped_starts[axis] + dest_idx[axis];
786                    }
787                    let operand_offset = checked_strided_offset(
788                        operand_offset_base,
789                        &self.operand_strides,
790                        operand_idx,
791                    )?;
792                    let dest_offset =
793                        checked_strided_offset(dest_offset_base, &self.dest_strides, dest_idx)?;
794                    unsafe {
795                        // SAFETY: dynamic slice writes one value per logical
796                        // output, and compile rejected non-injective
797                        // destination layouts.
798                        dest_ptr
799                            .offset(dest_offset)
800                            .write(operand_ptr.offset(operand_offset).read());
801                    }
802                    advance_col_major_index(dest_idx, &self.dest_dims);
803                }
804                Ok(())
805            },
806            &|left, right| left.and(right),
807        )
808    }
809
810    fn check_call<T, I, W>(
811        &self,
812        dest: &W,
813        operand: &RawStridedRef<'_, T>,
814        starts: &RawStridedRef<'_, I>,
815    ) -> Result<()>
816    where
817        W: OverwriteWriter<T>,
818    {
819        if dest.dims() != &self.dest_dims[..]
820            || dest.strides() != &self.dest_strides[..]
821            || operand.dims() != &self.operand_dims[..]
822            || operand.strides() != &self.operand_strides[..]
823            || starts.dims() != &self.start_dims[..]
824            || starts.strides() != &self.start_strides[..]
825        {
826            return Err(StridedError::PlanLayoutMismatch);
827        }
828        Ok(())
829    }
830}
831
832impl DynamicUpdateSlicePlan {
833    /// Compile a dynamic-update-slice traversal.
834    ///
835    /// `dest_dims` must match `operand_dims`; execution materializes
836    /// `dest = operand` and then overwrites the clamped update window.
837    #[allow(clippy::too_many_arguments)]
838    pub fn compile(
839        operand_dims: &[usize],
840        operand_strides: &[isize],
841        start_dims: &[usize],
842        start_strides: &[isize],
843        update_dims: &[usize],
844        update_strides: &[isize],
845        dest_dims: &[usize],
846        dest_strides: &[isize],
847    ) -> Result<Self> {
848        if operand_dims.len() != operand_strides.len()
849            || start_dims.len() != start_strides.len()
850            || update_dims.len() != update_strides.len()
851            || dest_dims.len() != dest_strides.len()
852        {
853            return Err(StridedError::StrideLengthMismatch);
854        }
855        if update_dims.len() != operand_dims.len() {
856            return Err(StridedError::RankMismatch(
857                update_dims.len(),
858                operand_dims.len(),
859            ));
860        }
861        validate_start_vector(start_dims, operand_dims.len())?;
862        checked_total_len(operand_dims)?;
863        checked_total_len(start_dims)?;
864        let total = checked_total_len(update_dims)?;
865        if dest_dims != operand_dims {
866            return Err(StridedError::ShapeMismatch(
867                dest_dims.to_vec(),
868                operand_dims.to_vec(),
869            ));
870        }
871        if !crate::fused::is_injective_layout(dest_dims, dest_strides) {
872            return Err(StridedError::NonInjectiveOutputLayout);
873        }
874        validate_window_sizes(operand_dims, update_dims)?;
875        let copy_plan = CopyPlan::compile(operand_dims, dest_strides, operand_strides)?;
876
877        Ok(Self {
878            operand_dims: operand_dims.into(),
879            operand_strides: operand_strides.into(),
880            start_dims: start_dims.into(),
881            start_strides: start_strides.into(),
882            update_dims: update_dims.into(),
883            update_strides: update_strides.into(),
884            dest_dims: dest_dims.into(),
885            dest_strides: dest_strides.into(),
886            total,
887            copy_plan,
888        })
889    }
890
891    /// Execute the prepared dynamic-update-slice traversal.
892    pub fn execute<T, I>(
893        &self,
894        dest: &mut RawStridedMut<'_, T>,
895        operand: &RawStridedRef<'_, T>,
896        update: &RawStridedRef<'_, T>,
897        starts: &RawStridedRef<'_, I>,
898    ) -> Result<()>
899    where
900        T: Copy + MaybeSendSync,
901        I: GatherIndex,
902    {
903        self.check_call(dest, operand, update, starts)?;
904        self.copy_plan.execute(dest, operand)?;
905        self.execute_update_with_writer(dest, update, starts)
906    }
907
908    /// Execute dynamic update into a destination whose reachable slots may be
909    /// uninitialized. The copy completes before any read-modify-write access.
910    pub(crate) fn execute_uninit<'a, T, I>(
911        &self,
912        dest: &'a mut RawStridedMut<'a, MaybeUninit<T>>,
913        operand: &RawStridedRef<'_, T>,
914        update: &RawStridedRef<'_, T>,
915        starts: &RawStridedRef<'_, I>,
916    ) -> Result<()>
917    where
918        T: Copy + MaybeSendSync,
919        I: GatherIndex,
920    {
921        self.check_call(dest, operand, update, starts)?;
922        self.copy_plan
923            .execute_uninit_then(dest, operand, |mut receipt| {
924                self.execute_update_with_writer(&mut receipt, update, starts)
925            })?
926    }
927
928    fn execute_update_with_writer<T, I, W>(
929        &self,
930        dest: &mut W,
931        update: &RawStridedRef<'_, T>,
932        starts: &RawStridedRef<'_, I>,
933    ) -> Result<()>
934    where
935        T: Copy + MaybeSendSync,
936        I: GatherIndex,
937        W: OverwriteWriter<T>,
938    {
939        if self.total == 0 {
940            return Ok(());
941        }
942        if self.uses_rank_one_contiguous_path() {
943            return self.execute_rank_one_contiguous(dest, update, starts);
944        }
945        #[cfg(feature = "parallel")]
946        {
947            let nthreads = crate::threading::parallel_threads_for_len(self.total);
948            if nthreads > 1 {
949                return self.execute_update_parallel(dest, update, starts, nthreads);
950            }
951        }
952
953        let mut starts_storage = CoordScratch::new(self.operand_dims.len());
954        let mut update_idx_storage = CoordScratch::new(self.update_dims.len());
955        let mut dest_idx_storage = CoordScratch::new(self.operand_dims.len());
956        let clamped_starts = starts_storage.as_mut_slice();
957        let update_idx = update_idx_storage.as_mut_slice();
958        let dest_idx = dest_idx_storage.as_mut_slice();
959        read_clamped_starts(
960            starts,
961            &self.operand_dims,
962            &self.update_dims,
963            clamped_starts,
964        )?;
965
966        let update_offset_base = update.offset();
967        let update_strides = update.strides();
968        let dest_offset_base = dest.offset();
969        let update_data = update.data();
970
971        for _ in 0..self.total {
972            for axis in 0..dest_idx.len() {
973                dest_idx[axis] = clamped_starts[axis] + update_idx[axis];
974            }
975            let update_offset =
976                checked_strided_offset(update_offset_base, update_strides, update_idx)?;
977            let dest_offset = checked_strided_offset(dest_offset_base, dest.strides(), dest_idx)?;
978            let value = unsafe { *update_data.as_ptr().offset(update_offset) };
979            // SAFETY: the validated plan proves this logical offset is in-bounds.
980            unsafe { dest.write_at(dest_offset, value) };
981            advance_col_major_index(update_idx, &self.update_dims);
982        }
983        Ok(())
984    }
985
986    #[inline]
987    fn uses_rank_one_contiguous_path(&self) -> bool {
988        self.operand_dims.len() == 1
989            && self.operand_strides[0] == 1
990            && self.update_strides[0] == 1
991            && self.dest_strides[0] == 1
992    }
993
994    fn execute_rank_one_contiguous<T, I, W>(
995        &self,
996        dest: &mut W,
997        update: &RawStridedRef<'_, T>,
998        starts: &RawStridedRef<'_, I>,
999    ) -> Result<()>
1000    where
1001        T: Copy,
1002        I: GatherIndex,
1003        W: OverwriteWriter<T>,
1004    {
1005        let mut clamped_starts = [0usize; 1];
1006        read_clamped_starts(
1007            starts,
1008            &self.operand_dims,
1009            &self.update_dims,
1010            &mut clamped_starts,
1011        )?;
1012        let update_start =
1013            usize::try_from(update.offset()).map_err(|_| StridedError::OffsetOverflow)?;
1014        let dest_start = checked_offset_add(dest.offset(), 1, clamped_starts[0])?;
1015        let dest_start = usize::try_from(dest_start).map_err(|_| StridedError::OffsetOverflow)?;
1016        let update_end = update_start
1017            .checked_add(self.total)
1018            .ok_or(StridedError::OffsetOverflow)?;
1019        let update = update
1020            .data()
1021            .get(update_start..update_end)
1022            .ok_or(StridedError::OffsetOverflow)?;
1023        // SAFETY: the validated writer owns the destination allocation.
1024        let dest_ptr = unsafe { dest.data_ptr() };
1025        // SAFETY: the checked ranges are inside the destination allocation.
1026        unsafe {
1027            core::ptr::copy_nonoverlapping(update.as_ptr(), dest_ptr.add(dest_start), self.total);
1028        }
1029        Ok(())
1030    }
1031
1032    #[cfg(feature = "parallel")]
1033    fn execute_update_parallel<T, I, W>(
1034        &self,
1035        dest: &mut W,
1036        update: &RawStridedRef<'_, T>,
1037        starts: &RawStridedRef<'_, I>,
1038        nthreads: usize,
1039    ) -> Result<()>
1040    where
1041        T: Copy + MaybeSendSync,
1042        I: GatherIndex,
1043        W: OverwriteWriter<T>,
1044    {
1045        let mut clamped_starts: AxisVec<usize> = (0..self.operand_dims.len()).map(|_| 0).collect();
1046        read_clamped_starts(
1047            starts,
1048            &self.operand_dims,
1049            &self.update_dims,
1050            &mut clamped_starts,
1051        )?;
1052
1053        let update_offset_base = update.offset();
1054        let dest_offset_base = dest.offset();
1055        let update_ptr = crate::threading::SendPtr(update.data().as_ptr() as *mut T);
1056        // SAFETY: the validated writer owns the destination allocation.
1057        let dest_ptr = crate::threading::SendPtr(unsafe { dest.data_ptr() });
1058
1059        crate::threading::parallel_map_reduce(
1060            0..self.total,
1061            nthreads,
1062            &|range| {
1063                let mut update_idx_storage = CoordScratch::new(self.update_dims.len());
1064                let mut dest_idx_storage = CoordScratch::new(self.operand_dims.len());
1065                let update_idx = update_idx_storage.as_mut_slice();
1066                let dest_idx = dest_idx_storage.as_mut_slice();
1067                fill_col_major_index(range.start, &self.update_dims, update_idx);
1068                let update_ptr = update_ptr.as_const();
1069                let dest_ptr = dest_ptr.as_ptr();
1070
1071                for _ in range {
1072                    for axis in 0..dest_idx.len() {
1073                        dest_idx[axis] = clamped_starts[axis] + update_idx[axis];
1074                    }
1075                    let update_offset = checked_strided_offset(
1076                        update_offset_base,
1077                        &self.update_strides,
1078                        update_idx,
1079                    )?;
1080                    let dest_offset =
1081                        checked_strided_offset(dest_offset_base, &self.dest_strides, dest_idx)?;
1082                    unsafe {
1083                        // SAFETY: each update-domain logical index maps to a
1084                        // distinct destination position for a fixed window, and
1085                        // compile rejected non-injective destination layouts.
1086                        dest_ptr
1087                            .offset(dest_offset)
1088                            .write(update_ptr.offset(update_offset).read());
1089                    }
1090                    advance_col_major_index(update_idx, &self.update_dims);
1091                }
1092                Ok(())
1093            },
1094            &|left, right| left.and(right),
1095        )
1096    }
1097
1098    fn check_call<T, I, W>(
1099        &self,
1100        dest: &W,
1101        operand: &RawStridedRef<'_, T>,
1102        update: &RawStridedRef<'_, T>,
1103        starts: &RawStridedRef<'_, I>,
1104    ) -> Result<()>
1105    where
1106        W: OverwriteWriter<T>,
1107    {
1108        if dest.dims() != &self.dest_dims[..]
1109            || dest.strides() != &self.dest_strides[..]
1110            || operand.dims() != &self.operand_dims[..]
1111            || operand.strides() != &self.operand_strides[..]
1112            || update.dims() != &self.update_dims[..]
1113            || update.strides() != &self.update_strides[..]
1114            || starts.dims() != &self.start_dims[..]
1115            || starts.strides() != &self.start_strides[..]
1116        {
1117            return Err(StridedError::PlanLayoutMismatch);
1118        }
1119        Ok(())
1120    }
1121}
1122
1123impl ScatterPlan {
1124    /// Compile an additive scatter traversal.
1125    #[allow(clippy::too_many_arguments)]
1126    pub fn compile(
1127        operand_dims: &[usize],
1128        operand_strides: &[isize],
1129        index_dims: &[usize],
1130        index_strides: &[isize],
1131        update_dims: &[usize],
1132        update_strides: &[isize],
1133        dest_dims: &[usize],
1134        dest_strides: &[isize],
1135        spec: ScatterSpec,
1136    ) -> Result<Self> {
1137        if operand_dims.len() != operand_strides.len()
1138            || index_dims.len() != index_strides.len()
1139            || update_dims.len() != update_strides.len()
1140            || dest_dims.len() != dest_strides.len()
1141        {
1142            return Err(StridedError::StrideLengthMismatch);
1143        }
1144        checked_total_len(operand_dims)?;
1145        checked_total_len(index_dims)?;
1146        checked_total_len(update_dims)?;
1147        if dest_dims != operand_dims {
1148            return Err(StridedError::ShapeMismatch(
1149                dest_dims.to_vec(),
1150                operand_dims.to_vec(),
1151            ));
1152        }
1153        if !crate::fused::is_injective_layout(dest_dims, dest_strides) {
1154            return Err(StridedError::NonInjectiveOutputLayout);
1155        }
1156
1157        let operand_rank = operand_dims.len();
1158        validate_unique_axes(&spec.inserted_window_dims, operand_rank)?;
1159        validate_unique_axes(&spec.scatter_dims_to_operand_dims, operand_rank)?;
1160        if spec.index_vector_dim > index_dims.len() {
1161            return Err(StridedError::InvalidAxis {
1162                axis: spec.index_vector_dim,
1163                rank: index_dims.len() + 1,
1164            });
1165        }
1166        let index_vector_size = if spec.index_vector_dim == index_dims.len() {
1167            1
1168        } else {
1169            index_dims[spec.index_vector_dim]
1170        };
1171        if index_vector_size != spec.scatter_dims_to_operand_dims.len() {
1172            return Err(StridedError::RankMismatch(
1173                index_vector_size,
1174                spec.scatter_dims_to_operand_dims.len(),
1175            ));
1176        }
1177
1178        let batch_shape = index_batch_shape(index_dims, spec.index_vector_dim);
1179        let window_dims = operand_window_dims(operand_rank, &spec.inserted_window_dims);
1180        if spec.update_window_dims.len() != window_dims.len() {
1181            return Err(StridedError::RankMismatch(
1182                spec.update_window_dims.len(),
1183                window_dims.len(),
1184            ));
1185        }
1186
1187        let update_rank = update_dims.len();
1188        let expected_batch_rank = update_rank
1189            .checked_sub(spec.update_window_dims.len())
1190            .ok_or(StridedError::RankMismatch(
1191                spec.update_window_dims.len(),
1192                update_rank,
1193            ))?;
1194        if expected_batch_rank != batch_shape.len() {
1195            return Err(StridedError::RankMismatch(
1196                expected_batch_rank,
1197                batch_shape.len(),
1198            ));
1199        }
1200        validate_unique_axes(&spec.update_window_dims, update_rank)?;
1201
1202        let mut is_update_window_dim: AxisVec<bool> = (0..update_rank).map(|_| false).collect();
1203        for &axis in &spec.update_window_dims {
1204            is_update_window_dim[axis] = true;
1205        }
1206
1207        let mut batch_axis = 0usize;
1208        for axis in 0..update_rank {
1209            if !is_update_window_dim[axis] {
1210                if update_dims[axis] != batch_shape[batch_axis] {
1211                    return Err(StridedError::ShapeMismatch(
1212                        update_dims.to_vec(),
1213                        expected_scatter_update_shape(&batch_shape, &spec, update_dims).to_vec(),
1214                    ));
1215                }
1216                batch_axis += 1;
1217            }
1218        }
1219
1220        let mut window_shape: AxisVec<usize> = (0..operand_rank).map(|_| 1).collect();
1221        let mut window_shape_updates: AxisVec<usize> =
1222            AxisVec::with_capacity(spec.update_window_dims.len());
1223        for (pos, &update_axis) in spec.update_window_dims.iter().enumerate() {
1224            let dim = update_dims[update_axis];
1225            window_shape_updates.push(dim);
1226            window_shape[window_dims[pos]] = dim;
1227        }
1228        validate_window_sizes(operand_dims, &window_shape)?;
1229
1230        let batch_elems = checked_total_len(&batch_shape)?;
1231        let window_elems = checked_total_len(&window_shape_updates)?;
1232        let copy_plan = CopyPlan::compile(operand_dims, dest_strides, operand_strides)?;
1233
1234        Ok(Self {
1235            operand_dims: operand_dims.into(),
1236            operand_strides: operand_strides.into(),
1237            index_dims: index_dims.into(),
1238            index_strides: index_strides.into(),
1239            update_dims: update_dims.into(),
1240            update_strides: update_strides.into(),
1241            dest_dims: dest_dims.into(),
1242            dest_strides: dest_strides.into(),
1243            spec,
1244            batch_shape,
1245            window_dims,
1246            window_shape,
1247            window_shape_updates,
1248            is_update_window_dim,
1249            batch_elems,
1250            window_elems,
1251            copy_plan,
1252        })
1253    }
1254
1255    /// Execute the prepared additive scatter traversal.
1256    pub fn execute<T, I>(
1257        &self,
1258        dest: &mut RawStridedMut<'_, T>,
1259        operand: &RawStridedRef<'_, T>,
1260        scatter_indices: &RawStridedRef<'_, I>,
1261        updates: &RawStridedRef<'_, T>,
1262    ) -> Result<()>
1263    where
1264        T: Copy + Add<Output = T> + MaybeSendSync,
1265        I: GatherIndex,
1266    {
1267        self.check_call(dest, operand, scatter_indices, updates)?;
1268        self.copy_plan.execute(dest, operand)?;
1269        self.execute_updates(dest, scatter_indices, updates, |a, b| a + b)
1270    }
1271
1272    /// Execute additive scatter into a destination whose reachable slots may
1273    /// be uninitialized. The operand copy completes before any RMW access.
1274    pub(crate) fn execute_uninit<'a, T, I>(
1275        &self,
1276        dest: &'a mut RawStridedMut<'a, MaybeUninit<T>>,
1277        operand: &RawStridedRef<'_, T>,
1278        scatter_indices: &RawStridedRef<'_, I>,
1279        updates: &RawStridedRef<'_, T>,
1280        combine: fn(T, T) -> T,
1281    ) -> Result<()>
1282    where
1283        T: Copy + Add<Output = T> + MaybeSendSync,
1284        I: GatherIndex,
1285    {
1286        self.check_call(dest, operand, scatter_indices, updates)?;
1287        self.copy_plan
1288            .execute_uninit_then(dest, operand, |mut receipt| {
1289                self.execute_updates(&mut receipt, scatter_indices, updates, combine)
1290            })?
1291    }
1292
1293    fn execute_updates<T, I, W>(
1294        &self,
1295        dest: &mut W,
1296        scatter_indices: &RawStridedRef<'_, I>,
1297        updates: &RawStridedRef<'_, T>,
1298        combine: fn(T, T) -> T,
1299    ) -> Result<()>
1300    where
1301        T: Copy + MaybeSendSync,
1302        I: GatherIndex,
1303        W: ReadModifyWrite<T>,
1304    {
1305        if self.batch_elems == 0 || self.window_elems == 0 {
1306            return Ok(());
1307        }
1308
1309        // Overlapping additive updates are order-sensitive, so this remains a
1310        // deterministic serial replay until a combine-aware parallel plan exists.
1311        let mut batch_idx_storage = CoordScratch::new(self.batch_shape.len());
1312        let mut window_idx_storage = CoordScratch::new(self.window_shape_updates.len());
1313        let mut update_idx_storage = CoordScratch::new(self.update_dims.len());
1314        let mut operand_base_storage = CoordScratch::new(self.operand_dims.len());
1315        let mut operand_idx_storage = CoordScratch::new(self.operand_dims.len());
1316        let batch_idx = batch_idx_storage.as_mut_slice();
1317        let window_idx = window_idx_storage.as_mut_slice();
1318        let update_idx = update_idx_storage.as_mut_slice();
1319        let operand_base = operand_base_storage.as_mut_slice();
1320        let operand_idx = operand_idx_storage.as_mut_slice();
1321
1322        let index_offset_base = scatter_indices.offset();
1323        let index_strides = scatter_indices.strides();
1324        let index_data = scatter_indices.data();
1325        let update_offset_base = updates.offset();
1326        let update_strides = updates.strides();
1327        let update_data = updates.data();
1328        let dest_offset_base = dest.offset();
1329
1330        for _ in 0..self.batch_elems {
1331            operand_base.fill(0);
1332            for (component, &operand_dim) in
1333                self.spec.scatter_dims_to_operand_dims.iter().enumerate()
1334            {
1335                let start = index_component(
1336                    scatter_indices.dims(),
1337                    index_strides,
1338                    index_offset_base,
1339                    index_data,
1340                    self.spec.index_vector_dim,
1341                    batch_idx,
1342                    component,
1343                )?;
1344                operand_base[operand_dim] = clamp_window_start(
1345                    start,
1346                    self.operand_dims[operand_dim],
1347                    self.window_shape[operand_dim],
1348                );
1349            }
1350
1351            window_idx.fill(0);
1352            for _ in 0..self.window_elems {
1353                let mut batch_axis = 0usize;
1354                let mut window_axis = 0usize;
1355                for axis in 0..self.update_dims.len() {
1356                    if self.is_update_window_dim[axis] {
1357                        update_idx[axis] = window_idx[window_axis];
1358                        window_axis += 1;
1359                    } else {
1360                        update_idx[axis] = batch_idx[batch_axis];
1361                        batch_axis += 1;
1362                    }
1363                }
1364
1365                operand_idx.copy_from_slice(operand_base);
1366                for (window_axis, &operand_axis) in self.window_dims.iter().enumerate() {
1367                    operand_idx[operand_axis] += window_idx[window_axis];
1368                }
1369
1370                let update_offset =
1371                    checked_strided_offset(update_offset_base, update_strides, update_idx)?;
1372                let dest_offset =
1373                    checked_strided_offset(dest_offset_base, dest.strides(), operand_idx)?;
1374                let value = unsafe { *update_data.as_ptr().offset(update_offset) };
1375                // SAFETY: copy completion and serial scatter traversal prove
1376                // this initialized logical slot is in-bounds.
1377                unsafe { dest.add_at(dest_offset, value, combine) };
1378                advance_col_major_index(window_idx, &self.window_shape_updates);
1379            }
1380            advance_col_major_index(batch_idx, &self.batch_shape);
1381        }
1382        Ok(())
1383    }
1384
1385    fn check_call<T, I, W>(
1386        &self,
1387        dest: &W,
1388        operand: &RawStridedRef<'_, T>,
1389        scatter_indices: &RawStridedRef<'_, I>,
1390        updates: &RawStridedRef<'_, T>,
1391    ) -> Result<()>
1392    where
1393        W: OverwriteWriter<T>,
1394    {
1395        if dest.dims() != &self.dest_dims[..]
1396            || dest.strides() != &self.dest_strides[..]
1397            || operand.dims() != &self.operand_dims[..]
1398            || operand.strides() != &self.operand_strides[..]
1399            || scatter_indices.dims() != &self.index_dims[..]
1400            || scatter_indices.strides() != &self.index_strides[..]
1401            || updates.dims() != &self.update_dims[..]
1402            || updates.strides() != &self.update_strides[..]
1403        {
1404            return Err(StridedError::PlanLayoutMismatch);
1405        }
1406        Ok(())
1407    }
1408}
1409
1410fn validate_unique_axes(axes: &[usize], rank: usize) -> Result<()> {
1411    let mut seen = vec![false; rank];
1412    for &axis in axes {
1413        if axis >= rank {
1414            return Err(StridedError::InvalidAxis { axis, rank });
1415        }
1416        if seen[axis] {
1417            return Err(StridedError::InvalidAxis { axis, rank });
1418        }
1419        seen[axis] = true;
1420    }
1421    Ok(())
1422}
1423
1424fn validate_start_vector(start_dims: &[usize], operand_rank: usize) -> Result<()> {
1425    if start_dims.len() != 1 {
1426        return Err(StridedError::RankMismatch(start_dims.len(), 1));
1427    }
1428    if start_dims[0] != operand_rank {
1429        return Err(StridedError::RankMismatch(start_dims[0], operand_rank));
1430    }
1431    Ok(())
1432}
1433
1434fn validate_window_sizes(operand_dims: &[usize], window_sizes: &[usize]) -> Result<()> {
1435    if operand_dims.len() != window_sizes.len() {
1436        return Err(StridedError::RankMismatch(
1437            window_sizes.len(),
1438            operand_dims.len(),
1439        ));
1440    }
1441    for (axis, (&window, &dim)) in window_sizes.iter().zip(operand_dims.iter()).enumerate() {
1442        if window > dim {
1443            return Err(StridedError::InvalidAxis {
1444                axis,
1445                rank: operand_dims.len(),
1446            });
1447        }
1448    }
1449    Ok(())
1450}
1451
1452fn read_clamped_starts<I>(
1453    starts: &RawStridedRef<'_, I>,
1454    operand_dims: &[usize],
1455    window_sizes: &[usize],
1456    out: &mut [usize],
1457) -> Result<()>
1458where
1459    I: GatherIndex,
1460{
1461    debug_assert_eq!(operand_dims.len(), window_sizes.len());
1462    debug_assert_eq!(operand_dims.len(), out.len());
1463    for axis in 0..operand_dims.len() {
1464        let offset = checked_offset_add(starts.offset(), starts.strides()[0], axis)?;
1465        let start = unsafe { *starts.data().as_ptr().offset(offset) }.to_i64();
1466        out[axis] = clamp_window_start(start, operand_dims[axis], window_sizes[axis]);
1467    }
1468    Ok(())
1469}
1470
1471fn index_component<I>(
1472    index_dims: &[usize],
1473    index_strides: &[isize],
1474    index_offset_base: isize,
1475    index_data: &[I],
1476    index_vector_dim: usize,
1477    batch_idx: &[usize],
1478    component: usize,
1479) -> Result<i64>
1480where
1481    I: GatherIndex,
1482{
1483    let mut offset = index_offset_base;
1484    let mut batch_axis = 0usize;
1485    for axis in 0..index_dims.len() {
1486        let coord = if axis == index_vector_dim {
1487            component
1488        } else {
1489            let coord = batch_idx[batch_axis];
1490            batch_axis += 1;
1491            coord
1492        };
1493        offset = checked_offset_add(offset, index_strides[axis], coord)?;
1494    }
1495    Ok(unsafe { *index_data.as_ptr().offset(offset) }.to_i64())
1496}
1497
1498#[inline]
1499fn clamp_window_start(start: i64, dim_size: usize, window_size: usize) -> usize {
1500    let max_start = dim_size.saturating_sub(window_size) as i64;
1501    start.clamp(0, max_start) as usize
1502}
1503
1504fn expected_scatter_update_shape(
1505    batch_shape: &[usize],
1506    spec: &ScatterSpec,
1507    update_dims: &[usize],
1508) -> AxisVec<usize> {
1509    let mut expected: AxisVec<usize> = AxisVec::with_capacity(update_dims.len());
1510    let mut batch_axis = 0usize;
1511    for axis in 0..update_dims.len() {
1512        if spec.update_window_dims.contains(&axis) {
1513            expected.push(update_dims[axis]);
1514        } else {
1515            expected.push(batch_shape[batch_axis]);
1516            batch_axis += 1;
1517        }
1518    }
1519    expected
1520}
1521
1522fn operand_window_dims(rank: usize, collapsed_slice_dims: &[usize]) -> AxisVec<usize> {
1523    (0..rank)
1524        .filter(|axis| !collapsed_slice_dims.contains(axis))
1525        .collect()
1526}
1527
1528fn index_batch_shape(index_dims: &[usize], index_vector_dim: usize) -> AxisVec<usize> {
1529    if index_vector_dim == index_dims.len() {
1530        return index_dims.into();
1531    }
1532    index_dims
1533        .iter()
1534        .enumerate()
1535        .filter_map(|(axis, &dim)| (axis != index_vector_dim).then_some(dim))
1536        .collect()
1537}
1538
1539fn checked_total_len(dims: &[usize]) -> Result<usize> {
1540    if dims.is_empty() {
1541        return Ok(1);
1542    }
1543    dims.iter()
1544        .try_fold(1usize, |acc, &dim| acc.checked_mul(dim))
1545        .ok_or(StridedError::OffsetOverflow)
1546}
1547
1548fn checked_strided_offset(base: isize, strides: &[isize], index: &[usize]) -> Result<isize> {
1549    let mut offset = base;
1550    for (&stride, &coord) in strides.iter().zip(index.iter()) {
1551        offset = checked_offset_add(offset, stride, coord)?;
1552    }
1553    Ok(offset)
1554}
1555
1556fn checked_offset_add(base: isize, stride: isize, coord: usize) -> Result<isize> {
1557    let coord = isize::try_from(coord).map_err(|_| StridedError::OffsetOverflow)?;
1558    let scaled = stride
1559        .checked_mul(coord)
1560        .ok_or(StridedError::OffsetOverflow)?;
1561    base.checked_add(scaled).ok_or(StridedError::OffsetOverflow)
1562}
1563
1564fn advance_col_major_index(index: &mut [usize], shape: &[usize]) {
1565    for axis in 0..index.len() {
1566        index[axis] += 1;
1567        if index[axis] < shape[axis] {
1568            return;
1569        }
1570        index[axis] = 0;
1571    }
1572}
1573
1574#[cfg(feature = "parallel")]
1575fn fill_col_major_index(mut linear: usize, shape: &[usize], out: &mut [usize]) {
1576    for (axis, coord) in out.iter_mut().enumerate() {
1577        let dim = shape[axis];
1578        *coord = linear % dim;
1579        linear /= dim;
1580    }
1581}
1582
1583struct CoordScratch {
1584    inline: [usize; RAW_FUSED_RANK_LIMIT],
1585    heap: Option<Vec<usize>>,
1586    len: usize,
1587}
1588
1589impl CoordScratch {
1590    fn new(len: usize) -> Self {
1591        if len <= RAW_FUSED_RANK_LIMIT {
1592            Self {
1593                inline: [0; RAW_FUSED_RANK_LIMIT],
1594                heap: None,
1595                len,
1596            }
1597        } else {
1598            Self {
1599                inline: [0; RAW_FUSED_RANK_LIMIT],
1600                heap: Some(vec![0; len]),
1601                len,
1602            }
1603        }
1604    }
1605
1606    fn as_mut_slice(&mut self) -> &mut [usize] {
1607        match &mut self.heap {
1608            Some(heap) => heap,
1609            None => &mut self.inline[..self.len],
1610        }
1611    }
1612}
1613
1614#[cfg(test)]
1615mod tests {
1616    use super::{DynamicSlicePlan, DynamicUpdateSlicePlan};
1617
1618    #[test]
1619    fn dynamic_slice_fast_path_is_limited_to_rank_one_contiguous_layouts() {
1620        let contiguous =
1621            DynamicSlicePlan::compile(&[16], &[1], &[1], &[1], &[8], &[1], &[8]).unwrap();
1622        assert!(contiguous.uses_rank_one_contiguous_path());
1623
1624        let higher_rank =
1625            DynamicSlicePlan::compile(&[4, 4], &[1, 4], &[2], &[1], &[2, 2], &[1, 2], &[2, 2])
1626                .unwrap();
1627        assert!(!higher_rank.uses_rank_one_contiguous_path());
1628
1629        let strided = DynamicSlicePlan::compile(&[16], &[2], &[1], &[1], &[8], &[2], &[8]).unwrap();
1630        assert!(!strided.uses_rank_one_contiguous_path());
1631    }
1632
1633    #[test]
1634    fn dynamic_update_fast_path_is_limited_to_rank_one_contiguous_layouts() {
1635        let contiguous =
1636            DynamicUpdateSlicePlan::compile(&[16], &[1], &[1], &[1], &[8], &[1], &[16], &[1])
1637                .unwrap();
1638        assert!(contiguous.uses_rank_one_contiguous_path());
1639
1640        let higher_rank = DynamicUpdateSlicePlan::compile(
1641            &[4, 4],
1642            &[1, 4],
1643            &[2],
1644            &[1],
1645            &[2, 2],
1646            &[1, 2],
1647            &[4, 4],
1648            &[1, 4],
1649        )
1650        .unwrap();
1651        assert!(!higher_rank.uses_rank_one_contiguous_path());
1652
1653        let strided =
1654            DynamicUpdateSlicePlan::compile(&[16], &[2], &[1], &[1], &[8], &[2], &[16], &[2])
1655                .unwrap();
1656        assert!(!strided.uses_rank_one_contiguous_path());
1657    }
1658}