strided_basic/erased/arg_reduce.rs
1//! Dtype-erased `argmax` / `argmin` along one axis.
2
3use super::line::{for_each_unit, LineLayout, UnitKernel, UnitPtr, PANEL};
4use super::{
5 check_reduce_layout_offset_arithmetic, checked_total_len, reduce_uninit_writer, reduce_writer,
6 ReduceWriter,
7};
8use crate::erased_common::{check_dtype, validate_uninit_no_overlap};
9use crate::*;
10use num_complex::{Complex32, Complex64};
11
12/// Runtime operation of an [`ErasedArgReducePlan`].
13///
14/// # Examples
15///
16/// ```
17/// use strided_basic::ArgReduceOp;
18/// assert_ne!(ArgReduceOp::Max, ArgReduceOp::MaxAbs);
19/// ```
20#[non_exhaustive]
21#[derive(Clone, Copy, Debug, Eq, PartialEq)]
22pub enum ArgReduceOp {
23 /// Index of the largest value. Real dtypes only.
24 Max,
25 /// Index of the smallest value. Real dtypes only.
26 Min,
27 /// Index of the largest magnitude (`|x|`, the modulus for complex input).
28 MaxAbs,
29 /// Index of the smallest magnitude (`|x|`, the modulus for complex input).
30 MinAbs,
31}
32
33impl ArgReduceOp {
34 #[inline]
35 fn label(self) -> &'static str {
36 match self {
37 Self::Max => "argmax",
38 Self::Min => "argmin",
39 Self::MaxAbs => "argmax_abs",
40 Self::MinAbs => "argmin_abs",
41 }
42 }
43}
44
45/// Dtype-erased `argmax` / `argmin` along one axis.
46///
47/// The destination holds one index per line: its dimensions are the source
48/// dimensions with `axis` removed (batch axes are preserved in order), and its
49/// dtype is the index dtype, `i32` or `i64`. Indices count from the start of
50/// the logical axis, whatever the sign of its stride.
51///
52/// Supported source dtypes are `f32`, `f64`, `i32` and `i64` for every
53/// [`ArgReduceOp`], and `c32` / `c64` for the magnitude variants
54/// ([`ArgReduceOp::MaxAbs`], [`ArgReduceOp::MinAbs`]) only.
55///
56/// # Ties, NaN and magnitudes
57///
58/// * Ties resolve to the **lowest index**. `-0.0` and `+0.0` compare equal,
59/// so they tie.
60/// * NaN propagates like [`ReduceOp::Max`]: if a line contains a NaN, the
61/// result is the index of its **first** NaN, for both the max and the min
62/// variants. A complex element is NaN when either component is NaN. This
63/// matches NumPy, PyTorch and JAX.
64/// * Infinities order normally; `+inf` beats every finite value for `Max`.
65/// * The complex magnitude is the overflow-safe modulus (`hypot` of the
66/// components), so values near the top of the exponent range still order
67/// correctly. A component of `±inf` makes the magnitude `+inf` unless the
68/// other component is NaN.
69/// * The integer magnitude is the unsigned absolute value, so `i32::MIN` is
70/// larger than `i32::MAX`.
71///
72/// The result does not depend on the layout, execution context or thread
73/// count.
74///
75/// # Examples
76///
77/// ```
78/// use strided_basic::{
79/// ArgReduceOp, ErasedArgReducePlan, ErasedRawStridedMut, ErasedRawStridedRef, ExecContext,
80/// KernelDType,
81/// };
82///
83/// // Column-major 3 x 2 matrix; argmax down each column.
84/// let src = [1.0_f64, 5.0, 5.0, -2.0, -7.0, 3.0];
85/// let mut out = [0_i64; 2];
86/// let plan = ErasedArgReducePlan::compile(
87/// KernelDType::F64,
88/// KernelDType::I64,
89/// ArgReduceOp::Max,
90/// &[3, 2],
91/// &[1, 3],
92/// &[2],
93/// &[1],
94/// 0,
95/// )
96/// .unwrap();
97/// let src_ref = ErasedRawStridedRef::from_slice(&src, &[3, 2], &[1, 3], 0).unwrap();
98/// let mut dest = ErasedRawStridedMut::from_slice_mut(&mut out, &[2], &[1], 0).unwrap();
99/// plan.execute(&ExecContext::serial(), &mut dest, &src_ref).unwrap();
100/// assert_eq!(out, [1, 2]); // lowest index wins the 5.0 tie
101/// ```
102#[derive(Clone, Debug)]
103pub struct ErasedArgReducePlan {
104 dtype: KernelDType,
105 index_dtype: KernelDType,
106 op: ArgReduceOp,
107 src_dims: crate::inline::InlineVec<usize>,
108 src_strides: crate::inline::InlineVec<isize>,
109 dest_dims: crate::inline::InlineVec<usize>,
110 dest_strides: crate::inline::InlineVec<isize>,
111 layout: LineLayout,
112}
113
114impl ErasedArgReducePlan {
115 /// Validate and store an arg-reduction plan for fixed layouts.
116 ///
117 /// `dest_dims` must equal `src_dims` with `axis` removed.
118 ///
119 /// # Examples
120 ///
121 /// ```
122 /// use strided_basic::{ArgReduceOp, ErasedArgReducePlan, KernelDType};
123 /// let plan = ErasedArgReducePlan::compile(
124 /// KernelDType::C64, KernelDType::I32, ArgReduceOp::MaxAbs, &[4, 3], &[3, 1], &[3], &[1], 0,
125 /// )
126 /// .unwrap();
127 /// assert_eq!(plan.index_dtype(), KernelDType::I32);
128 /// // Complex input has no order without the magnitude.
129 /// assert!(ErasedArgReducePlan::compile(
130 /// KernelDType::C64, KernelDType::I64, ArgReduceOp::Max, &[4], &[1], &[], &[], 0,
131 /// )
132 /// .is_err());
133 /// // An empty axis has no index to return.
134 /// assert!(ErasedArgReducePlan::compile(
135 /// KernelDType::F64, KernelDType::I64, ArgReduceOp::Max, &[0, 2], &[1, 1], &[2], &[1], 0,
136 /// )
137 /// .is_err());
138 /// ```
139 ///
140 /// # Errors
141 ///
142 /// * `UnsupportedDType` for a `bool` source or an index dtype other than
143 /// `i32` / `i64`;
144 /// * `UnsupportedOp` for `Max` / `Min` on complex input, and for a
145 /// zero-length `axis`, which has no index to return;
146 /// * `InvalidAxis`, `StrideLengthMismatch`, `ShapeMismatch` and
147 /// `NonInjectiveOutputLayout` for inconsistent layouts;
148 /// * `OffsetOverflow` when a layout's offsets are not representable, or
149 /// when the axis is too long for an `i32` index.
150 #[allow(clippy::too_many_arguments)]
151 pub fn compile(
152 dtype: KernelDType,
153 index_dtype: KernelDType,
154 op: ArgReduceOp,
155 src_dims: &[usize],
156 src_strides: &[isize],
157 dest_dims: &[usize],
158 dest_strides: &[isize],
159 axis: usize,
160 ) -> Result<Self> {
161 check_arg_dtype(dtype, op)?;
162 if !matches!(index_dtype, KernelDType::I32 | KernelDType::I64) {
163 return Err(StridedError::UnsupportedDType {
164 dtype: index_dtype.label(),
165 });
166 }
167 if src_dims.len() != src_strides.len() || dest_dims.len() != dest_strides.len() {
168 return Err(StridedError::StrideLengthMismatch);
169 }
170 let rank = src_dims.len();
171 if axis >= rank {
172 return Err(StridedError::InvalidAxis { axis, rank });
173 }
174 let expected: crate::inline::InlineVec<usize> = (0..rank)
175 .filter(|&a| a != axis)
176 .map(|a| src_dims[a])
177 .collect();
178 if dest_dims != expected.as_slice() {
179 return Err(StridedError::ShapeMismatch(
180 dest_dims.to_vec(),
181 expected.to_vec(),
182 ));
183 }
184 if src_dims[axis] == 0 {
185 return Err(StridedError::UnsupportedOp {
186 op: "argmax/argmin over a zero-length axis",
187 dtype: dtype.label(),
188 });
189 }
190 if index_dtype == KernelDType::I32 && i32::try_from(src_dims[axis] - 1).is_err() {
191 return Err(StridedError::OffsetOverflow);
192 }
193 checked_total_len(src_dims)?;
194 check_reduce_layout_offset_arithmetic(src_dims, src_strides)?;
195 check_reduce_layout_offset_arithmetic(dest_dims, dest_strides)?;
196 if !crate::layout_check::is_injective_layout(dest_dims, dest_strides) {
197 return Err(StridedError::NonInjectiveOutputLayout);
198 }
199 let layout = LineLayout::compile(src_dims, src_strides, dest_strides, 0, axis, false)?;
200 Ok(Self {
201 dtype,
202 index_dtype,
203 op,
204 src_dims: src_dims.into(),
205 src_strides: src_strides.into(),
206 dest_dims: dest_dims.into(),
207 dest_strides: dest_strides.into(),
208 layout,
209 })
210 }
211
212 /// Source element dtype.
213 ///
214 /// # Examples
215 ///
216 /// ```
217 /// use strided_basic::{ArgReduceOp, ErasedArgReducePlan, KernelDType};
218 /// let plan = ErasedArgReducePlan::compile(
219 /// KernelDType::F32, KernelDType::I64, ArgReduceOp::Min, &[2], &[1], &[], &[], 0,
220 /// )
221 /// .unwrap();
222 /// assert_eq!(plan.dtype(), KernelDType::F32);
223 /// ```
224 #[inline]
225 pub fn dtype(&self) -> KernelDType {
226 self.dtype
227 }
228
229 /// Destination index dtype (`i32` or `i64`).
230 ///
231 /// # Examples
232 ///
233 /// ```
234 /// use strided_basic::{ArgReduceOp, ErasedArgReducePlan, KernelDType};
235 /// let plan = ErasedArgReducePlan::compile(
236 /// KernelDType::F32, KernelDType::I64, ArgReduceOp::Min, &[2], &[1], &[], &[], 0,
237 /// )
238 /// .unwrap();
239 /// assert_eq!(plan.index_dtype(), KernelDType::I64);
240 /// ```
241 #[inline]
242 pub fn index_dtype(&self) -> KernelDType {
243 self.index_dtype
244 }
245
246 /// Arg-reduction operation.
247 ///
248 /// # Examples
249 ///
250 /// ```
251 /// use strided_basic::{ArgReduceOp, ErasedArgReducePlan, KernelDType};
252 /// let plan = ErasedArgReducePlan::compile(
253 /// KernelDType::F32, KernelDType::I64, ArgReduceOp::Min, &[2], &[1], &[], &[], 0,
254 /// )
255 /// .unwrap();
256 /// assert_eq!(plan.op(), ArgReduceOp::Min);
257 /// ```
258 #[inline]
259 pub fn op(&self) -> ArgReduceOp {
260 self.op
261 }
262
263 fn check_layouts(
264 &self,
265 dest_dims: &[usize],
266 dest_strides: &[isize],
267 src: &ErasedRawStridedRef<'_>,
268 ) -> Result<()> {
269 if src.dims() != self.src_dims.as_slice()
270 || src.strides() != self.src_strides.as_slice()
271 || dest_dims != self.dest_dims.as_slice()
272 || dest_strides != self.dest_strides.as_slice()
273 {
274 return Err(StridedError::PlanLayoutMismatch);
275 }
276 Ok(())
277 }
278
279 /// Execute into an initialized index destination.
280 ///
281 /// # Examples
282 ///
283 /// ```
284 /// use strided_basic::{
285 /// ArgReduceOp, ErasedArgReducePlan, ErasedRawStridedMut, ErasedRawStridedRef,
286 /// ExecContext, KernelDType,
287 /// };
288 /// let src = [3_i32, -9, 9, 1];
289 /// let mut out = [0_i32; 1];
290 /// let plan = ErasedArgReducePlan::compile(
291 /// KernelDType::I32, KernelDType::I32, ArgReduceOp::MaxAbs, &[4], &[1], &[], &[], 0,
292 /// )
293 /// .unwrap();
294 /// let src_ref = ErasedRawStridedRef::from_slice(&src, &[4], &[1], 0).unwrap();
295 /// let mut dest = ErasedRawStridedMut::from_slice_mut(&mut out, &[], &[], 0).unwrap();
296 /// plan.execute(&ExecContext::serial(), &mut dest, &src_ref).unwrap();
297 /// assert_eq!(out, [1]);
298 /// ```
299 ///
300 /// # Errors
301 ///
302 /// Returns `DTypeMismatch` or `PlanLayoutMismatch` when a descriptor does
303 /// not match the plan, before any destination write.
304 pub fn execute(
305 &self,
306 ctx: &ExecContext,
307 dest: &mut ErasedRawStridedMut<'_>,
308 src: &ErasedRawStridedRef<'_>,
309 ) -> Result<()> {
310 check_dtype(self.index_dtype, dest.dtype())?;
311 check_dtype(self.dtype, src.dtype())?;
312 self.check_layouts(dest.dims(), dest.strides(), src)?;
313 match self.index_dtype {
314 KernelDType::I32 => {
315 let mut writer = reduce_writer::<i32>(dest)?;
316 self.dispatch_dtype(ctx, &mut writer, src)
317 }
318 _ => {
319 let mut writer = reduce_writer::<i64>(dest)?;
320 self.dispatch_dtype(ctx, &mut writer, src)
321 }
322 }
323 }
324
325 /// Execute into an uninitialized index destination.
326 ///
327 /// On success every reachable destination element is written; validation
328 /// errors are returned before any write.
329 ///
330 /// # Examples
331 ///
332 /// ```
333 /// use core::mem::MaybeUninit;
334 /// use num_complex::Complex64;
335 /// use strided_basic::{
336 /// ArgReduceOp, ErasedArgReducePlan, ErasedRawStridedPtr, ErasedRawStridedRef,
337 /// ErasedRawStridedUninitMut, ExecContext, KernelDType,
338 /// };
339 /// let src = [Complex64::new(3.0, 4.0), Complex64::new(0.0, 6.0)];
340 /// let mut out = [MaybeUninit::<i64>::uninit()];
341 /// let plan = ErasedArgReducePlan::compile(
342 /// KernelDType::C64, KernelDType::I64, ArgReduceOp::MinAbs, &[2], &[1], &[], &[], 0,
343 /// )
344 /// .unwrap();
345 /// let src_ref = ErasedRawStridedRef::from_slice(&src, &[2], &[1], 0).unwrap();
346 /// let src_ptr = ErasedRawStridedPtr::from_ref(&src_ref);
347 /// let mut dest = ErasedRawStridedUninitMut::from_uninit_slice(&mut out, &[], &[], 0).unwrap();
348 /// plan.execute_uninit(&ExecContext::serial(), &mut dest, &src_ptr).unwrap();
349 /// assert_eq!(unsafe { out[0].assume_init() }, 0);
350 /// ```
351 ///
352 /// # Errors
353 ///
354 /// As [`Self::execute`], plus `OverlappingInputOutput` when the source
355 /// overlaps the destination allocation.
356 pub fn execute_uninit(
357 &self,
358 ctx: &ExecContext,
359 dest: &mut ErasedRawStridedUninitMut<'_>,
360 src: &ErasedRawStridedPtr<'_>,
361 ) -> Result<()> {
362 check_dtype(self.index_dtype, dest.dtype())?;
363 check_dtype(self.dtype, src.dtype())?;
364 validate_uninit_no_overlap(dest, src, 0)?;
365 // SAFETY: the owning erased entry rejected all input/output overlap before conversion.
366 let src = unsafe { src.try_as_ref_after_no_overlap() }?;
367 self.check_layouts(dest.dims(), dest.strides(), &src)?;
368 match self.index_dtype {
369 KernelDType::I32 => {
370 let mut writer = reduce_uninit_writer::<i32>(dest)?;
371 self.dispatch_dtype(ctx, &mut writer, &src)
372 }
373 _ => {
374 let mut writer = reduce_uninit_writer::<i64>(dest)?;
375 self.dispatch_dtype(ctx, &mut writer, &src)
376 }
377 }
378 }
379
380 fn dispatch_dtype<I, W>(
381 &self,
382 ctx: &ExecContext,
383 dest: &mut W,
384 src: &ErasedRawStridedRef<'_>,
385 ) -> Result<()>
386 where
387 I: ArgIndex,
388 W: ReduceWriter<I>,
389 {
390 // The dtype and op are matched once per execution; each loop is
391 // monomorphized for one (dtype, key, direction, index) combination.
392 macro_rules! real {
393 ($ty:ty) => {
394 match self.op {
395 ArgReduceOp::Max => self.run::<$ty, I, W, Plain, Greater>(ctx, dest, src),
396 ArgReduceOp::Min => self.run::<$ty, I, W, Plain, Less>(ctx, dest, src),
397 ArgReduceOp::MaxAbs => {
398 self.run::<$ty, I, W, Magnitude, Greater>(ctx, dest, src)
399 }
400 ArgReduceOp::MinAbs => self.run::<$ty, I, W, Magnitude, Less>(ctx, dest, src),
401 }
402 };
403 }
404 macro_rules! complex {
405 ($ty:ty) => {
406 match self.op {
407 ArgReduceOp::MaxAbs => {
408 self.run::<$ty, I, W, Magnitude, Greater>(ctx, dest, src)
409 }
410 ArgReduceOp::MinAbs => self.run::<$ty, I, W, Magnitude, Less>(ctx, dest, src),
411 // INVARIANT: check_arg_dtype rejects ordered complex ops at compile.
412 ArgReduceOp::Max | ArgReduceOp::Min => Err(StridedError::UnsupportedOp {
413 op: self.op.label(),
414 dtype: self.dtype.label(),
415 }),
416 }
417 };
418 }
419 match self.dtype {
420 KernelDType::F32 => real!(f32),
421 KernelDType::F64 => real!(f64),
422 KernelDType::I32 => real!(i32),
423 KernelDType::I64 => real!(i64),
424 KernelDType::C32 => complex!(Complex32),
425 KernelDType::C64 => complex!(Complex64),
426 _ => Err(StridedError::UnsupportedDType {
427 dtype: self.dtype.label(),
428 }),
429 }
430 }
431
432 fn run<T, I, W, F, D>(
433 &self,
434 ctx: &ExecContext,
435 dest: &mut W,
436 src: &ErasedRawStridedRef<'_>,
437 ) -> Result<()>
438 where
439 T: KernelStorageElement + MaybeSendSync,
440 I: ArgIndex,
441 W: ReduceWriter<I>,
442 F: KeyFn<T>,
443 D: Direction,
444 {
445 let layout = &self.layout;
446 let source = UnitPtr(src.data_as::<T>()?.as_ptr() as *mut T);
447 // SAFETY: the validated writer owns the destination allocation.
448 let target = UnitPtr(unsafe { dest.ptr() });
449 let n = layout.axis_len;
450 let ss = layout.src_axis_stride;
451 let lane = layout.dest_lane_stride;
452 let kernel = ArgUnit::<T, I, F, D> {
453 source,
454 target,
455 n,
456 ss,
457 lane,
458 _ops: core::marker::PhantomData,
459 };
460 // INVARIANT: (1) compile checked the signed source/destination spans
461 // and every cursor step/reset, and rejected an empty axis; (2) the raw
462 // descriptors validated every reachable offset; (3) execute checked
463 // exact plan-layout equality. Units own disjoint outputs.
464 // SAFETY: the three-link invariant above.
465 unsafe { for_each_unit(ctx, layout, src.offset(), dest.offset(), kernel) }
466 }
467}
468
469/// Per-unit arg-reduction kernel of one execution.
470struct ArgUnit<T, I, F, D> {
471 source: UnitPtr<T>,
472 target: UnitPtr<I>,
473 n: usize,
474 ss: isize,
475 lane: isize,
476 _ops: core::marker::PhantomData<fn() -> (F, D)>,
477}
478
479impl<T, I, F, D> Clone for ArgUnit<T, I, F, D> {
480 fn clone(&self) -> Self {
481 *self
482 }
483}
484impl<T, I, F, D> Copy for ArgUnit<T, I, F, D> {}
485
486impl<T, I, F, D> UnitKernel for ArgUnit<T, I, F, D>
487where
488 T: KernelStorageElement + MaybeSendSync,
489 I: ArgIndex,
490 F: KeyFn<T>,
491 D: Direction,
492{
493 #[inline(always)]
494 unsafe fn unit(self, so: isize, d_o: isize, width: usize) {
495 let Self {
496 source,
497 target,
498 n,
499 ss,
500 lane,
501 ..
502 } = self;
503 // SAFETY: the caller passes offsets of the validated layout.
504 unsafe {
505 if width == 1 {
506 let index = arg_line::<T, F, D>(source.get(), so, ss, n);
507 target.get().offset(d_o).write(I::from_index(index));
508 } else {
509 arg_panel::<T, I, F, D>(source.get(), so, ss, n, target.get(), d_o, lane, width);
510 }
511 }
512 }
513}
514
515fn check_arg_dtype(dtype: KernelDType, op: ArgReduceOp) -> Result<()> {
516 match dtype {
517 KernelDType::F32 | KernelDType::F64 | KernelDType::I32 | KernelDType::I64 => Ok(()),
518 KernelDType::C32 | KernelDType::C64 => match op {
519 ArgReduceOp::MaxAbs | ArgReduceOp::MinAbs => Ok(()),
520 ArgReduceOp::Max | ArgReduceOp::Min => Err(StridedError::UnsupportedOp {
521 op: op.label(),
522 dtype: dtype.label(),
523 }),
524 },
525 _ => Err(StridedError::UnsupportedDType {
526 dtype: dtype.label(),
527 }),
528 }
529}
530
531/// Index element written to the destination.
532pub(super) trait ArgIndex: KernelStorageElement + MaybeSendSync {
533 fn from_index(index: usize) -> Self;
534}
535
536impl ArgIndex for i32 {
537 #[inline(always)]
538 fn from_index(index: usize) -> Self {
539 // INVARIANT: compile rejected axes whose last index exceeds i32::MAX.
540 index as i32
541 }
542}
543
544impl ArgIndex for i64 {
545 #[inline(always)]
546 fn from_index(index: usize) -> Self {
547 // INVARIANT: an addressable axis length fits in i64.
548 index as i64
549 }
550}
551
552/// Comparable key of an element.
553pub(super) trait OrderKey: Copy + PartialEq {
554 /// Whether the key is NaN (never for integer keys).
555 fn key_is_nan(self) -> bool;
556 /// Whether `candidate` replaces `best` for a maximum: strictly greater,
557 /// or the first NaN.
558 fn beats_max(candidate: Self, best: Self) -> bool;
559 /// Whether `candidate` replaces `best` for a minimum: strictly less, or
560 /// the first NaN.
561 fn beats_min(candidate: Self, best: Self) -> bool;
562}
563
564macro_rules! impl_float_key {
565 ($($ty:ty),*) => {$(
566 impl OrderKey for $ty {
567 #[inline(always)]
568 fn key_is_nan(self) -> bool {
569 self.is_nan()
570 }
571 #[inline(always)]
572 fn beats_max(candidate: Self, best: Self) -> bool {
573 // Select form without a data dependent branch; a NaN `best`
574 // is never replaced, so the first NaN wins.
575 (candidate > best) | (candidate.is_nan() & !best.is_nan())
576 }
577 #[inline(always)]
578 fn beats_min(candidate: Self, best: Self) -> bool {
579 (candidate < best) | (candidate.is_nan() & !best.is_nan())
580 }
581 }
582 )*};
583}
584impl_float_key!(f32, f64);
585
586macro_rules! impl_int_key {
587 ($($ty:ty),*) => {$(
588 impl OrderKey for $ty {
589 #[inline(always)]
590 fn key_is_nan(self) -> bool {
591 false
592 }
593 #[inline(always)]
594 fn beats_max(candidate: Self, best: Self) -> bool {
595 candidate > best
596 }
597 #[inline(always)]
598 fn beats_min(candidate: Self, best: Self) -> bool {
599 candidate < best
600 }
601 }
602 )*};
603}
604impl_int_key!(i32, i64, u32, u64);
605
606/// Maps an element to its comparison key.
607pub(super) trait KeyFn<T> {
608 type Key: OrderKey;
609 fn key(value: T) -> Self::Key;
610}
611
612/// The value itself (real dtypes).
613pub(super) struct Plain;
614/// The magnitude `|x|`.
615pub(super) struct Magnitude;
616
617macro_rules! impl_plain_key {
618 ($($ty:ty),*) => {$(
619 impl KeyFn<$ty> for Plain {
620 type Key = $ty;
621 #[inline(always)]
622 fn key(value: $ty) -> $ty {
623 value
624 }
625 }
626 )*};
627}
628impl_plain_key!(f32, f64, i32, i64);
629
630impl KeyFn<f32> for Magnitude {
631 type Key = f32;
632 #[inline(always)]
633 fn key(value: f32) -> f32 {
634 value.abs()
635 }
636}
637impl KeyFn<f64> for Magnitude {
638 type Key = f64;
639 #[inline(always)]
640 fn key(value: f64) -> f64 {
641 value.abs()
642 }
643}
644impl KeyFn<i32> for Magnitude {
645 type Key = u32;
646 #[inline(always)]
647 fn key(value: i32) -> u32 {
648 value.unsigned_abs()
649 }
650}
651impl KeyFn<i64> for Magnitude {
652 type Key = u64;
653 #[inline(always)]
654 fn key(value: i64) -> u64 {
655 value.unsigned_abs()
656 }
657}
658impl KeyFn<Complex32> for Magnitude {
659 type Key = f32;
660 #[inline(always)]
661 fn key(value: Complex32) -> f32 {
662 // `hypot` returns +inf for an infinite component even when the other
663 // is NaN; the NaN test keeps any NaN component a NaN key.
664 if value.re.is_nan() || value.im.is_nan() {
665 f32::NAN
666 } else {
667 value.re.hypot(value.im)
668 }
669 }
670}
671impl KeyFn<Complex64> for Magnitude {
672 type Key = f64;
673 #[inline(always)]
674 fn key(value: Complex64) -> f64 {
675 if value.re.is_nan() || value.im.is_nan() {
676 f64::NAN
677 } else {
678 value.re.hypot(value.im)
679 }
680 }
681}
682
683/// Maximum or minimum, fixed at compile time.
684pub(super) trait Direction {
685 fn beats<K: OrderKey>(candidate: K, best: K) -> bool;
686}
687pub(super) struct Greater;
688pub(super) struct Less;
689impl Direction for Greater {
690 #[inline(always)]
691 fn beats<K: OrderKey>(candidate: K, best: K) -> bool {
692 K::beats_max(candidate, best)
693 }
694}
695impl Direction for Less {
696 #[inline(always)]
697 fn beats<K: OrderKey>(candidate: K, best: K) -> bool {
698 K::beats_min(candidate, best)
699 }
700}
701
702/// Independent lanes of the contiguous winner search.
703const ARG_LANES: usize = 8;
704
705/// Index of the winning element of one line.
706///
707/// A unit-stride line runs two passes: a lane-parallel search for the winning
708/// key (NaN if the line has one), then a scan for the first element whose key
709/// equals it (or is NaN). Equal keys tie, so the second pass returns the
710/// lowest index of the winner, exactly as the sequential scan used for
711/// strided lines.
712///
713/// # Safety
714///
715/// Every `src.offset(so + k * ss)` for `k < n` must be readable, and `n > 0`.
716#[inline(always)]
717unsafe fn arg_line<T, F, D>(src: *const T, so: isize, ss: isize, n: usize) -> usize
718where
719 T: Copy,
720 F: KeyFn<T>,
721 D: Direction,
722{
723 // SAFETY: the caller guarantees every visited offset and `n > 0`; a unit
724 // stride line is `n` contiguous elements.
725 unsafe {
726 if ss == 1 {
727 let values = core::slice::from_raw_parts(src.offset(so), n);
728 let mut lanes = [F::key(values[0]); ARG_LANES];
729 let mut chunks = values.chunks_exact(ARG_LANES);
730 for chunk in &mut chunks {
731 for (lane, &value) in lanes.iter_mut().zip(chunk) {
732 let key = F::key(value);
733 *lane = if D::beats(key, *lane) { key } else { *lane };
734 }
735 }
736 // Which NaN or which signed zero wins here does not matter: the
737 // second pass matches by NaN-ness or by equality.
738 let mut winner = lanes[0];
739 let tail = chunks.remainder().iter().map(|&value| F::key(value));
740 for key in lanes[1..].iter().copied().chain(tail) {
741 if D::beats(key, winner) {
742 winner = key;
743 }
744 }
745 let found = if winner.key_is_nan() {
746 values.iter().position(|&value| F::key(value).key_is_nan())
747 } else {
748 values.iter().position(|&value| F::key(value) == winner)
749 };
750 // INVARIANT: the winner is the key of some element of the line.
751 return found.unwrap_or(0);
752 }
753 let mut best = F::key(src.offset(so).read());
754 let mut best_index = 0;
755 let mut offset = so;
756 for index in 1..n {
757 offset += ss;
758 let key = F::key(src.offset(offset).read());
759 if D::beats(key, best) {
760 best = key;
761 best_index = index;
762 }
763 }
764 best_index
765 }
766}
767
768/// Arg-reduces `width` adjacent lines whose source elements are contiguous
769/// across the lines; output `j` is written at `d_o + j * lane`.
770///
771/// # Safety
772///
773/// As [`arg_line`] for each line `j < width` with source base `so + j`, every
774/// destination offset must be writable, and `width <= PANEL`.
775#[allow(clippy::too_many_arguments)]
776#[inline(always)]
777unsafe fn arg_panel<T, I, F, D>(
778 src: *const T,
779 so: isize,
780 ss: isize,
781 n: usize,
782 dst: *mut I,
783 d_o: isize,
784 lane: isize,
785 width: usize,
786) where
787 T: Copy,
788 I: ArgIndex,
789 F: KeyFn<T>,
790 D: Direction,
791{
792 debug_assert!(width <= PANEL);
793 // SAFETY: the caller guarantees `width` contiguous source elements at
794 // every axis position and the destination offsets.
795 unsafe {
796 let first = core::slice::from_raw_parts(src.offset(so), width);
797 let mut best = [F::key(first[0]); PANEL];
798 let mut best_index = [0usize; PANEL];
799 for (best, &value) in best.iter_mut().zip(first) {
800 *best = F::key(value);
801 }
802 let best = &mut best[..width];
803 let best_index = &mut best_index[..width];
804 let mut offset = so;
805 for index in 1..n {
806 offset += ss;
807 let row = core::slice::from_raw_parts(src.offset(offset), width);
808 for ((best, best_index), &value) in best.iter_mut().zip(best_index.iter_mut()).zip(row)
809 {
810 let key = F::key(value);
811 let take = D::beats(key, *best);
812 *best = if take { key } else { *best };
813 *best_index = if take { index } else { *best_index };
814 }
815 }
816 for (lane_index, &index) in best_index.iter().enumerate() {
817 dst.offset(d_o + lane_index as isize * lane)
818 .write(I::from_index(index));
819 }
820 }
821}