Skip to main content

strided_kernel/
fused.rs

1//! Runtime-DAG fused elementwise kernels.
2
3use core::mem::MaybeUninit;
4
5use crate::kernel::{
6    build_plan_fused, build_plan_fused_small, ensure_same_shape, for_each_inner_block_preordered,
7    total_len, SMALL_TENSOR_THRESHOLD,
8};
9use crate::map_view::{
10    map_into_validated, validate_destination_layout_without_alloc, zip_map2_into_validated,
11    zip_map3_into_validated, zip_map4_into_validated, ValidatedDestinationLayout,
12};
13use crate::{MaybeSendSync, Result, StridedError, StridedView, StridedViewMut};
14
15#[cfg(feature = "parallel")]
16use crate::fuse::compute_costs;
17#[cfg(feature = "parallel")]
18use crate::threading::{
19    for_each_inner_block_with_offsets, mapreduce_threaded, SendPtr, MINTHREADLENGTH,
20};
21
22/// Runtime scalar operation for a fused elementwise plan.
23#[derive(Clone, Copy, Debug, Eq, PartialEq)]
24pub enum FusedOp {
25    Add,
26    Multiply,
27    Negate,
28    Conj,
29    Divide,
30    Abs,
31    Maximum,
32    Minimum,
33    Clamp,
34    Exp,
35    Log,
36    Sin,
37    Cos,
38    Tanh,
39    Sqrt,
40    Rsqrt,
41    Pow,
42    Expm1,
43    Log1p,
44}
45
46impl FusedOp {
47    #[inline]
48    pub const fn label(self) -> &'static str {
49        match self {
50            Self::Add => "add",
51            Self::Multiply => "multiply",
52            Self::Negate => "negate",
53            Self::Conj => "conj",
54            Self::Divide => "divide",
55            Self::Abs => "abs",
56            Self::Maximum => "maximum",
57            Self::Minimum => "minimum",
58            Self::Clamp => "clamp",
59            Self::Exp => "exp",
60            Self::Log => "log",
61            Self::Sin => "sin",
62            Self::Cos => "cos",
63            Self::Tanh => "tanh",
64            Self::Sqrt => "sqrt",
65            Self::Rsqrt => "rsqrt",
66            Self::Pow => "pow",
67            Self::Expm1 => "expm1",
68            Self::Log1p => "log1p",
69        }
70    }
71}
72
73/// One SSA instruction in a [`FusedPlan`].
74#[derive(Clone, Debug, Eq, PartialEq)]
75pub struct FusedInst {
76    pub op: FusedOp,
77    pub inputs: Vec<usize>,
78}
79
80/// Topologically ordered fused elementwise SSA DAG.
81///
82/// Values are numbered in evaluation order. Input values occupy
83/// `0..input_count`; each instruction appends one value after the previous
84/// inputs/instructions. For example, with `input_count == 2`, the first
85/// instruction writes value `2`, the second writes value `3`, and so on.
86/// `outputs` contains the value ids to write to `dests` in order.
87///
88/// All inputs and destinations passed to [`fused_elementwise_into`] must have
89/// the same shape and scalar type. Broadcast inputs should be represented with
90/// `StridedView::broadcast` before building the plan; the fused API does not
91/// perform implicit broadcasting.
92#[derive(Clone, Debug, Eq, PartialEq)]
93pub struct FusedPlan {
94    pub input_count: usize,
95    pub outputs: Vec<usize>,
96    pub ops: Vec<FusedInst>,
97}
98
99/// Scalar types supported by [`fused_elementwise_into`].
100pub trait FusedScalar: Copy + MaybeSendSync + 'static {
101    fn fused_dtype_label() -> &'static str {
102        core::any::type_name::<Self>()
103    }
104
105    fn supports_fused_op(_op: FusedOp) -> bool {
106        true
107    }
108
109    fn fused_add(self, rhs: Self) -> Self;
110    fn fused_multiply(self, rhs: Self) -> Self;
111    fn fused_negate(self) -> Self;
112    fn fused_conj(self) -> Self;
113    fn fused_divide(self, rhs: Self) -> Self;
114    fn fused_abs(self) -> Self;
115    fn fused_maximum(self, rhs: Self) -> Self;
116    fn fused_minimum(self, rhs: Self) -> Self;
117    fn fused_clamp(self, min: Self, max: Self) -> Self;
118    fn fused_exp(self) -> Self;
119    fn fused_log(self) -> Self;
120    fn fused_sin(self) -> Self;
121    fn fused_cos(self) -> Self;
122    fn fused_tanh(self) -> Self;
123    fn fused_sqrt(self) -> Self;
124    fn fused_rsqrt(self) -> Self;
125    fn fused_pow(self, rhs: Self) -> Self;
126    fn fused_expm1(self) -> Self;
127    fn fused_log1p(self) -> Self;
128}
129
130macro_rules! unsupported_fused_op {
131    ($op:literal, $ty:literal) => {
132        unreachable!("unsupported fused op {} for dtype {}", $op, $ty)
133    };
134}
135
136macro_rules! impl_real_fused_scalar {
137    ($ty:ty) => {
138        impl FusedScalar for $ty {
139            #[inline(always)]
140            fn fused_add(self, rhs: Self) -> Self {
141                self + rhs
142            }
143
144            #[inline(always)]
145            fn fused_multiply(self, rhs: Self) -> Self {
146                self * rhs
147            }
148
149            #[inline(always)]
150            fn fused_negate(self) -> Self {
151                -self
152            }
153
154            #[inline(always)]
155            fn fused_conj(self) -> Self {
156                self
157            }
158
159            #[inline(always)]
160            fn fused_divide(self, rhs: Self) -> Self {
161                self / rhs
162            }
163
164            #[inline(always)]
165            fn fused_abs(self) -> Self {
166                self.abs()
167            }
168
169            #[inline(always)]
170            fn fused_maximum(self, rhs: Self) -> Self {
171                self.max(rhs)
172            }
173
174            #[inline(always)]
175            fn fused_minimum(self, rhs: Self) -> Self {
176                self.min(rhs)
177            }
178
179            #[inline(always)]
180            fn fused_clamp(self, min: Self, max: Self) -> Self {
181                self.fused_maximum(min).fused_minimum(max)
182            }
183
184            #[inline(always)]
185            fn fused_exp(self) -> Self {
186                self.exp()
187            }
188
189            #[inline(always)]
190            fn fused_log(self) -> Self {
191                self.ln()
192            }
193
194            #[inline(always)]
195            fn fused_sin(self) -> Self {
196                self.sin()
197            }
198
199            #[inline(always)]
200            fn fused_cos(self) -> Self {
201                self.cos()
202            }
203
204            #[inline(always)]
205            fn fused_tanh(self) -> Self {
206                self.tanh()
207            }
208
209            #[inline(always)]
210            fn fused_sqrt(self) -> Self {
211                self.sqrt()
212            }
213
214            #[inline(always)]
215            fn fused_rsqrt(self) -> Self {
216                1.0 / self.sqrt()
217            }
218
219            #[inline(always)]
220            fn fused_pow(self, rhs: Self) -> Self {
221                self.powf(rhs)
222            }
223
224            #[inline(always)]
225            fn fused_expm1(self) -> Self {
226                self.exp_m1()
227            }
228
229            #[inline(always)]
230            fn fused_log1p(self) -> Self {
231                self.ln_1p()
232            }
233        }
234    };
235}
236
237macro_rules! impl_complex_fused_scalar {
238    ($ty:ty) => {
239        impl FusedScalar for $ty {
240            #[inline(always)]
241            fn fused_add(self, rhs: Self) -> Self {
242                self + rhs
243            }
244
245            #[inline(always)]
246            fn fused_multiply(self, rhs: Self) -> Self {
247                self * rhs
248            }
249
250            #[inline(always)]
251            fn fused_negate(self) -> Self {
252                -self
253            }
254
255            #[inline(always)]
256            fn fused_conj(self) -> Self {
257                num_complex::Complex::conj(&self)
258            }
259
260            #[inline(always)]
261            fn fused_divide(self, rhs: Self) -> Self {
262                self / rhs
263            }
264
265            #[inline(always)]
266            fn fused_abs(self) -> Self {
267                Self::new(self.norm(), 0.0)
268            }
269
270            #[inline(always)]
271            fn fused_maximum(self, rhs: Self) -> Self {
272                if self.norm_sqr() >= rhs.norm_sqr() {
273                    self
274                } else {
275                    rhs
276                }
277            }
278
279            #[inline(always)]
280            fn fused_minimum(self, rhs: Self) -> Self {
281                if self.norm_sqr() <= rhs.norm_sqr() {
282                    self
283                } else {
284                    rhs
285                }
286            }
287
288            #[inline(always)]
289            fn fused_clamp(self, min: Self, max: Self) -> Self {
290                self.fused_maximum(min).fused_minimum(max)
291            }
292
293            #[inline(always)]
294            fn fused_exp(self) -> Self {
295                self.exp()
296            }
297
298            #[inline(always)]
299            fn fused_log(self) -> Self {
300                self.ln()
301            }
302
303            #[inline(always)]
304            fn fused_sin(self) -> Self {
305                self.sin()
306            }
307
308            #[inline(always)]
309            fn fused_cos(self) -> Self {
310                self.cos()
311            }
312
313            #[inline(always)]
314            fn fused_tanh(self) -> Self {
315                self.tanh()
316            }
317
318            #[inline(always)]
319            fn fused_sqrt(self) -> Self {
320                self.sqrt()
321            }
322
323            #[inline(always)]
324            fn fused_rsqrt(self) -> Self {
325                Self::new(1.0, 0.0) / self.sqrt()
326            }
327
328            #[inline(always)]
329            fn fused_pow(self, rhs: Self) -> Self {
330                self.powc(rhs)
331            }
332
333            #[inline(always)]
334            fn fused_expm1(self) -> Self {
335                self.exp() - Self::new(1.0, 0.0)
336            }
337
338            #[inline(always)]
339            fn fused_log1p(self) -> Self {
340                (self + Self::new(1.0, 0.0)).ln()
341            }
342        }
343    };
344}
345
346impl_real_fused_scalar!(f32);
347impl_real_fused_scalar!(f64);
348impl_complex_fused_scalar!(num_complex::Complex32);
349impl_complex_fused_scalar!(num_complex::Complex64);
350
351macro_rules! impl_signed_integer_fused_scalar {
352    ($ty:ty, $label:literal) => {
353        impl FusedScalar for $ty {
354            #[inline]
355            fn fused_dtype_label() -> &'static str {
356                $label
357            }
358
359            #[inline]
360            fn supports_fused_op(op: FusedOp) -> bool {
361                matches!(
362                    op,
363                    FusedOp::Add
364                        | FusedOp::Multiply
365                        | FusedOp::Negate
366                        | FusedOp::Conj
367                        | FusedOp::Abs
368                        | FusedOp::Maximum
369                        | FusedOp::Minimum
370                        | FusedOp::Clamp
371                )
372            }
373
374            #[inline(always)]
375            fn fused_add(self, rhs: Self) -> Self {
376                self.wrapping_add(rhs)
377            }
378
379            #[inline(always)]
380            fn fused_multiply(self, rhs: Self) -> Self {
381                self.wrapping_mul(rhs)
382            }
383
384            #[inline(always)]
385            fn fused_negate(self) -> Self {
386                self.wrapping_neg()
387            }
388
389            #[inline(always)]
390            fn fused_conj(self) -> Self {
391                self
392            }
393
394            #[inline(always)]
395            fn fused_divide(self, _rhs: Self) -> Self {
396                unsupported_fused_op!("divide", $label)
397            }
398
399            #[inline(always)]
400            fn fused_abs(self) -> Self {
401                self.wrapping_abs()
402            }
403
404            #[inline(always)]
405            fn fused_maximum(self, rhs: Self) -> Self {
406                self.max(rhs)
407            }
408
409            #[inline(always)]
410            fn fused_minimum(self, rhs: Self) -> Self {
411                self.min(rhs)
412            }
413
414            #[inline(always)]
415            fn fused_clamp(self, min: Self, max: Self) -> Self {
416                self.fused_maximum(min).fused_minimum(max)
417            }
418
419            #[inline(always)]
420            fn fused_exp(self) -> Self {
421                unsupported_fused_op!("exp", $label)
422            }
423
424            #[inline(always)]
425            fn fused_log(self) -> Self {
426                unsupported_fused_op!("log", $label)
427            }
428
429            #[inline(always)]
430            fn fused_sin(self) -> Self {
431                unsupported_fused_op!("sin", $label)
432            }
433
434            #[inline(always)]
435            fn fused_cos(self) -> Self {
436                unsupported_fused_op!("cos", $label)
437            }
438
439            #[inline(always)]
440            fn fused_tanh(self) -> Self {
441                unsupported_fused_op!("tanh", $label)
442            }
443
444            #[inline(always)]
445            fn fused_sqrt(self) -> Self {
446                unsupported_fused_op!("sqrt", $label)
447            }
448
449            #[inline(always)]
450            fn fused_rsqrt(self) -> Self {
451                unsupported_fused_op!("rsqrt", $label)
452            }
453
454            #[inline(always)]
455            fn fused_pow(self, _rhs: Self) -> Self {
456                unsupported_fused_op!("pow", $label)
457            }
458
459            #[inline(always)]
460            fn fused_expm1(self) -> Self {
461                unsupported_fused_op!("expm1", $label)
462            }
463
464            #[inline(always)]
465            fn fused_log1p(self) -> Self {
466                unsupported_fused_op!("log1p", $label)
467            }
468        }
469    };
470}
471
472impl_signed_integer_fused_scalar!(i32, "i32");
473impl_signed_integer_fused_scalar!(i64, "i64");
474
475impl FusedScalar for bool {
476    #[inline]
477    fn fused_dtype_label() -> &'static str {
478        "bool"
479    }
480
481    #[inline]
482    fn supports_fused_op(op: FusedOp) -> bool {
483        matches!(op, FusedOp::Conj)
484    }
485
486    #[inline(always)]
487    fn fused_add(self, _rhs: Self) -> Self {
488        unsupported_fused_op!("add", "bool")
489    }
490
491    #[inline(always)]
492    fn fused_multiply(self, _rhs: Self) -> Self {
493        unsupported_fused_op!("multiply", "bool")
494    }
495
496    #[inline(always)]
497    fn fused_negate(self) -> Self {
498        unsupported_fused_op!("negate", "bool")
499    }
500
501    #[inline(always)]
502    fn fused_conj(self) -> Self {
503        self
504    }
505
506    #[inline(always)]
507    fn fused_divide(self, _rhs: Self) -> Self {
508        unsupported_fused_op!("divide", "bool")
509    }
510
511    #[inline(always)]
512    fn fused_abs(self) -> Self {
513        unsupported_fused_op!("abs", "bool")
514    }
515
516    #[inline(always)]
517    fn fused_maximum(self, _rhs: Self) -> Self {
518        unsupported_fused_op!("maximum", "bool")
519    }
520
521    #[inline(always)]
522    fn fused_minimum(self, _rhs: Self) -> Self {
523        unsupported_fused_op!("minimum", "bool")
524    }
525
526    #[inline(always)]
527    fn fused_clamp(self, _min: Self, _max: Self) -> Self {
528        unsupported_fused_op!("clamp", "bool")
529    }
530
531    #[inline(always)]
532    fn fused_exp(self) -> Self {
533        unsupported_fused_op!("exp", "bool")
534    }
535
536    #[inline(always)]
537    fn fused_log(self) -> Self {
538        unsupported_fused_op!("log", "bool")
539    }
540
541    #[inline(always)]
542    fn fused_sin(self) -> Self {
543        unsupported_fused_op!("sin", "bool")
544    }
545
546    #[inline(always)]
547    fn fused_cos(self) -> Self {
548        unsupported_fused_op!("cos", "bool")
549    }
550
551    #[inline(always)]
552    fn fused_tanh(self) -> Self {
553        unsupported_fused_op!("tanh", "bool")
554    }
555
556    #[inline(always)]
557    fn fused_sqrt(self) -> Self {
558        unsupported_fused_op!("sqrt", "bool")
559    }
560
561    #[inline(always)]
562    fn fused_rsqrt(self) -> Self {
563        unsupported_fused_op!("rsqrt", "bool")
564    }
565
566    #[inline(always)]
567    fn fused_pow(self, _rhs: Self) -> Self {
568        unsupported_fused_op!("pow", "bool")
569    }
570
571    #[inline(always)]
572    fn fused_expm1(self) -> Self {
573        unsupported_fused_op!("expm1", "bool")
574    }
575
576    #[inline(always)]
577    fn fused_log1p(self) -> Self {
578        unsupported_fused_op!("log1p", "bool")
579    }
580}
581
582#[inline]
583fn op_arity(op: FusedOp) -> usize {
584    match op {
585        FusedOp::Negate
586        | FusedOp::Conj
587        | FusedOp::Abs
588        | FusedOp::Exp
589        | FusedOp::Log
590        | FusedOp::Sin
591        | FusedOp::Cos
592        | FusedOp::Tanh
593        | FusedOp::Sqrt
594        | FusedOp::Rsqrt
595        | FusedOp::Expm1
596        | FusedOp::Log1p => 1,
597        FusedOp::Add
598        | FusedOp::Multiply
599        | FusedOp::Divide
600        | FusedOp::Maximum
601        | FusedOp::Minimum
602        | FusedOp::Pow => 2,
603        FusedOp::Clamp => 3,
604    }
605}
606
607pub(crate) fn validate_plan(
608    plan: &FusedPlan,
609    input_count: usize,
610    output_count: usize,
611) -> Result<()> {
612    if input_count != plan.input_count {
613        return Err(StridedError::RankMismatch(input_count, plan.input_count));
614    }
615    if output_count != plan.outputs.len() {
616        return Err(StridedError::RankMismatch(output_count, plan.outputs.len()));
617    }
618    if output_count == 0 {
619        return Err(StridedError::RankMismatch(0, 1));
620    }
621
622    let mut value_count = plan.input_count;
623    for inst in &plan.ops {
624        let expected_arity = op_arity(inst.op);
625        if inst.inputs.len() != expected_arity {
626            return Err(StridedError::RankMismatch(
627                inst.inputs.len(),
628                expected_arity,
629            ));
630        }
631        for &input in &inst.inputs {
632            if input >= value_count {
633                return Err(StridedError::InvalidAxis {
634                    axis: input,
635                    rank: value_count,
636                });
637            }
638        }
639        value_count += 1;
640    }
641
642    for &output in &plan.outputs {
643        if output >= value_count {
644            return Err(StridedError::InvalidAxis {
645                axis: output,
646                rank: value_count,
647            });
648        }
649    }
650
651    Ok(())
652}
653
654pub(crate) fn validate_plan_for_scalar<T: FusedScalar>(
655    plan: &FusedPlan,
656    input_count: usize,
657    output_count: usize,
658) -> Result<()> {
659    validate_plan(plan, input_count, output_count)?;
660    for inst in &plan.ops {
661        if !T::supports_fused_op(inst.op) {
662            return Err(StridedError::UnsupportedOp {
663                op: inst.op.label(),
664                dtype: T::fused_dtype_label(),
665            });
666        }
667    }
668    Ok(())
669}
670
671fn validate_shapes<T: FusedScalar>(
672    dests: &[StridedViewMut<'_, T>],
673    inputs: &[StridedView<'_, T>],
674) -> Result<()> {
675    let dims = dests[0].dims();
676    for dest in dests {
677        validate_destination_layout(dest)?;
678    }
679    for dest in &dests[1..] {
680        ensure_same_shape(dims, dest.dims())?;
681    }
682    for input in inputs {
683        ensure_same_shape(dims, input.dims())?;
684    }
685    Ok(())
686}
687
688fn validate_destination_layout<T>(dest: &StridedViewMut<'_, T>) -> Result<()> {
689    if is_injective_layout(dest.dims(), dest.strides()) {
690        Ok(())
691    } else {
692        Err(StridedError::NonInjectiveOutputLayout)
693    }
694}
695
696pub(crate) fn is_injective_layout(dims: &[usize], strides: &[isize]) -> bool {
697    let Some(total) = validate_injective_layout_inputs(dims, strides) else {
698        return false;
699    };
700    if total <= 1 || has_disjoint_stride_spans(dims, strides) {
701        return true;
702    }
703
704    const EXACT_CHECK_LIMIT: usize = 4096;
705    if total <= EXACT_CHECK_LIMIT {
706        return has_unique_offsets_exact(dims, strides, total);
707    }
708
709    false
710}
711
712pub(crate) fn is_injective_layout_without_alloc(dims: &[usize], strides: &[isize]) -> bool {
713    let Some(total) = validate_injective_layout_inputs(dims, strides) else {
714        return false;
715    };
716    if total <= 1 || has_disjoint_stride_spans(dims, strides) {
717        return true;
718    }
719
720    const EXACT_CHECK_LIMIT: usize = 4096;
721    total <= EXACT_CHECK_LIMIT && has_unique_offsets_pairwise(dims, strides, total)
722}
723
724fn offset_for_linear_index(dims: &[usize], strides: &[isize], mut linear: usize) -> Option<isize> {
725    let mut offset = 0isize;
726    for (&dim, &stride) in dims.iter().zip(strides.iter()) {
727        let index = linear % dim;
728        linear /= dim;
729        offset = offset.checked_add(stride.checked_mul(index as isize)?)?;
730    }
731    Some(offset)
732}
733
734fn has_unique_offsets_pairwise(dims: &[usize], strides: &[isize], total: usize) -> bool {
735    for lhs in 0..total {
736        let Some(lhs_offset) = offset_for_linear_index(dims, strides, lhs) else {
737            return false;
738        };
739        for rhs in (lhs + 1)..total {
740            if offset_for_linear_index(dims, strides, rhs) == Some(lhs_offset) {
741                return false;
742            }
743        }
744    }
745    true
746}
747
748fn validate_injective_layout_inputs(dims: &[usize], strides: &[isize]) -> Option<usize> {
749    if dims.len() != strides.len() {
750        return None;
751    }
752
753    let total = dims
754        .iter()
755        .try_fold(1usize, |acc, &dim| acc.checked_mul(dim))?;
756    if total <= 1 {
757        return Some(total);
758    }
759    if dims
760        .iter()
761        .zip(strides.iter())
762        .any(|(&dim, &stride)| dim > 1 && stride == 0)
763    {
764        return None;
765    }
766
767    let mut min_offset = 0isize;
768    let mut max_offset = 0isize;
769    for (&dim, &stride) in dims.iter().zip(strides.iter()) {
770        if dim <= 1 {
771            continue;
772        }
773        let extent = isize::try_from(dim - 1).ok()?;
774        let span = stride.checked_mul(extent)?;
775        if span >= 0 {
776            max_offset = max_offset.checked_add(span)?;
777        } else {
778            min_offset = min_offset.checked_add(span)?;
779        }
780    }
781    Some(total)
782}
783
784fn has_unique_offsets_exact(dims: &[usize], strides: &[isize], total: usize) -> bool {
785    let mut seen = std::collections::HashSet::with_capacity(total);
786    let mut indices = vec![0usize; dims.len()];
787    let mut offset = 0isize;
788
789    for _ in 0..total {
790        if !seen.insert(offset) {
791            return false;
792        }
793
794        for axis in 0..dims.len() {
795            indices[axis] += 1;
796            offset = match offset.checked_add(strides[axis]) {
797                Some(offset) => offset,
798                None => return false,
799            };
800            if indices[axis] < dims[axis] {
801                break;
802            }
803
804            let rewind = match strides[axis].checked_mul(indices[axis] as isize) {
805                Some(rewind) => rewind,
806                None => return false,
807            };
808            offset = match offset.checked_sub(rewind) {
809                Some(offset) => offset,
810                None => return false,
811            };
812            indices[axis] = 0;
813        }
814    }
815
816    true
817}
818
819fn has_disjoint_stride_spans(dims: &[usize], strides: &[isize]) -> bool {
820    let mut covered_span = 0u128;
821    let mut previous_axis = None;
822    let active_axes = dims.iter().filter(|&&dim| dim > 1).count();
823    for _ in 0..active_axes {
824        let mut next = None;
825        for (axis, (&dim, &stride)) in dims.iter().zip(strides.iter()).enumerate() {
826            if dim <= 1 {
827                continue;
828            }
829            let stride = match stride.checked_abs() {
830                Some(stride) => stride as u128,
831                None => return false,
832            };
833            let key = (stride, axis);
834            if previous_axis.is_some_and(|previous| key <= previous) {
835                continue;
836            }
837            if next.is_none_or(|(best, _)| key < best) {
838                next = Some((key, dim as u128 - 1));
839            }
840        }
841        let Some(((stride, axis), extent)) = next else {
842            return false;
843        };
844        if stride <= covered_span {
845            return false;
846        }
847        covered_span = match stride
848            .checked_mul(extent)
849            .and_then(|span| covered_span.checked_add(span))
850        {
851            Some(covered_span) => covered_span,
852            None => return false,
853        };
854        previous_axis = Some((stride, axis));
855    }
856
857    true
858}
859
860#[inline(always)]
861fn eval_op<T: FusedScalar>(op: FusedOp, regs: &[T], inputs: &[usize]) -> T {
862    match op {
863        FusedOp::Negate
864        | FusedOp::Conj
865        | FusedOp::Abs
866        | FusedOp::Exp
867        | FusedOp::Log
868        | FusedOp::Sin
869        | FusedOp::Cos
870        | FusedOp::Tanh
871        | FusedOp::Sqrt
872        | FusedOp::Rsqrt
873        | FusedOp::Expm1
874        | FusedOp::Log1p => eval_unary(op, regs[inputs[0]]),
875        FusedOp::Add
876        | FusedOp::Multiply
877        | FusedOp::Divide
878        | FusedOp::Maximum
879        | FusedOp::Minimum
880        | FusedOp::Pow => eval_binary(op, regs[inputs[0]], regs[inputs[1]]),
881        FusedOp::Clamp => eval_ternary(op, regs[inputs[0]], regs[inputs[1]], regs[inputs[2]]),
882    }
883}
884
885#[inline(always)]
886fn eval_unary<T: FusedScalar>(op: FusedOp, x: T) -> T {
887    match op {
888        FusedOp::Negate => x.fused_negate(),
889        FusedOp::Conj => x.fused_conj(),
890        FusedOp::Abs => x.fused_abs(),
891        FusedOp::Exp => x.fused_exp(),
892        FusedOp::Log => x.fused_log(),
893        FusedOp::Sin => x.fused_sin(),
894        FusedOp::Cos => x.fused_cos(),
895        FusedOp::Tanh => x.fused_tanh(),
896        FusedOp::Sqrt => x.fused_sqrt(),
897        FusedOp::Rsqrt => x.fused_rsqrt(),
898        FusedOp::Expm1 => x.fused_expm1(),
899        FusedOp::Log1p => x.fused_log1p(),
900        _ => unreachable!("not a unary fused op: {op:?}"),
901    }
902}
903
904#[inline(always)]
905fn eval_binary<T: FusedScalar>(op: FusedOp, a: T, b: T) -> T {
906    match op {
907        FusedOp::Add => a.fused_add(b),
908        FusedOp::Multiply => a.fused_multiply(b),
909        FusedOp::Divide => a.fused_divide(b),
910        FusedOp::Maximum => a.fused_maximum(b),
911        FusedOp::Minimum => a.fused_minimum(b),
912        FusedOp::Pow => a.fused_pow(b),
913        _ => unreachable!("not a binary fused op: {op:?}"),
914    }
915}
916
917#[inline(always)]
918fn eval_ternary<T: FusedScalar>(op: FusedOp, a: T, b: T, c: T) -> T {
919    match op {
920        FusedOp::Clamp => a.fused_clamp(b, c),
921        _ => unreachable!("not a ternary fused op: {op:?}"),
922    }
923}
924
925#[derive(Clone, Copy)]
926enum StaticFusedKind {
927    Unary(FusedOp, usize),
928    Binary(FusedOp, usize, usize),
929    Ternary(FusedOp, usize, usize, usize),
930    AddMulLeft,
931    AddMulRight,
932    MulAddExp,
933    DivClampSqrtRsqrt,
934}
935
936#[cfg(test)]
937std::thread_local! {
938    static UNINITIALIZED_STATIC_FAMILY_HITS: core::cell::Cell<[usize; 7]> =
939        const { core::cell::Cell::new([0; 7]) };
940}
941
942#[cfg(test)]
943impl StaticFusedKind {
944    fn test_index(self) -> usize {
945        match self {
946            Self::Unary(..) => 0,
947            Self::Binary(..) => 1,
948            Self::Ternary(..) => 2,
949            Self::AddMulLeft => 3,
950            Self::AddMulRight => 4,
951            Self::MulAddExp => 5,
952            Self::DivClampSqrtRsqrt => 6,
953        }
954    }
955}
956
957#[cfg(all(test, feature = "parallel"))]
958fn reset_uninitialized_static_family_hits() {
959    UNINITIALIZED_STATIC_FAMILY_HITS.set([0; 7]);
960}
961
962#[cfg(all(test, feature = "parallel"))]
963fn uninitialized_static_family_hits() -> [usize; 7] {
964    UNINITIALIZED_STATIC_FAMILY_HITS.get()
965}
966
967#[cfg(test)]
968fn record_uninitialized_static_family_hit(kind: StaticFusedKind) {
969    UNINITIALIZED_STATIC_FAMILY_HITS.set({
970        let mut hits = UNINITIALIZED_STATIC_FAMILY_HITS.get();
971        hits[kind.test_index()] += 1;
972        hits
973    });
974}
975
976fn classify_static_specialization(plan: &FusedPlan) -> Option<StaticFusedKind> {
977    if plan.outputs.len() != 1 {
978        return None;
979    }
980    if let [inst] = plan.ops.as_slice() {
981        if plan.outputs[0] != plan.input_count {
982            return None;
983        }
984        return match (op_arity(inst.op), inst.inputs.as_slice()) {
985            (1, [a]) => Some(StaticFusedKind::Unary(inst.op, *a)),
986            (2, [a, b]) => Some(StaticFusedKind::Binary(inst.op, *a, *b)),
987            (3, [a, b, c]) => Some(StaticFusedKind::Ternary(inst.op, *a, *b, *c)),
988            _ => None,
989        };
990    }
991    if plan.input_count == 2
992        && plan.outputs.as_slice() == [3]
993        && plan.ops.len() == 2
994        && plan.ops[0].op == FusedOp::Add
995        && plan.ops[0].inputs.as_slice() == [0, 1]
996        && plan.ops[1].op == FusedOp::Multiply
997    {
998        return match plan.ops[1].inputs.as_slice() {
999            [2, 0] => Some(StaticFusedKind::AddMulLeft),
1000            [0, 2] => Some(StaticFusedKind::AddMulRight),
1001            _ => None,
1002        };
1003    }
1004    if plan.input_count == 3
1005        && plan.outputs.as_slice() == [5]
1006        && plan.ops.len() == 3
1007        && plan.ops[0].op == FusedOp::Multiply
1008        && plan.ops[0].inputs.as_slice() == [0, 1]
1009        && plan.ops[1].op == FusedOp::Add
1010        && plan.ops[1].inputs.as_slice() == [3, 2]
1011        && plan.ops[2].op == FusedOp::Exp
1012        && plan.ops[2].inputs.as_slice() == [4]
1013    {
1014        return Some(StaticFusedKind::MulAddExp);
1015    }
1016    if plan.input_count == 4
1017        && plan.outputs.as_slice() == [8]
1018        && plan.ops.len() == 5
1019        && plan.ops[0].op == FusedOp::Divide
1020        && plan.ops[0].inputs.as_slice() == [0, 1]
1021        && plan.ops[1].op == FusedOp::Maximum
1022        && plan.ops[1].inputs.as_slice() == [4, 2]
1023        && plan.ops[2].op == FusedOp::Minimum
1024        && plan.ops[2].inputs.as_slice() == [5, 3]
1025        && plan.ops[3].op == FusedOp::Sqrt
1026        && plan.ops[3].inputs.as_slice() == [6]
1027        && plan.ops[4].op == FusedOp::Rsqrt
1028        && plan.ops[4].inputs.as_slice() == [7]
1029    {
1030        return Some(StaticFusedKind::DivClampSqrtRsqrt);
1031    }
1032    None
1033}
1034
1035trait StaticOutput<T: FusedScalar> {
1036    type Value: Copy + MaybeSendSync;
1037
1038    #[cfg(test)]
1039    const IS_UNINITIALIZED: bool;
1040
1041    fn write(value: T) -> Self::Value;
1042}
1043
1044struct InitializedStaticOutput;
1045
1046impl<T: FusedScalar> StaticOutput<T> for InitializedStaticOutput {
1047    type Value = T;
1048
1049    #[cfg(test)]
1050    const IS_UNINITIALIZED: bool = false;
1051
1052    #[inline(always)]
1053    fn write(value: T) -> T {
1054        value
1055    }
1056}
1057
1058struct UninitializedStaticOutput;
1059
1060impl<T: FusedScalar> StaticOutput<T> for UninitializedStaticOutput {
1061    type Value = MaybeUninit<T>;
1062
1063    #[cfg(test)]
1064    const IS_UNINITIALIZED: bool = true;
1065
1066    #[inline(always)]
1067    fn write(value: T) -> MaybeUninit<T> {
1068        MaybeUninit::new(value)
1069    }
1070}
1071
1072fn try_static_specialization_validated<T, O>(
1073    dest: &mut StridedViewMut<'_, O::Value>,
1074    inputs: &[StridedView<'_, T>],
1075    plan: &FusedPlan,
1076    validated: ValidatedDestinationLayout,
1077) -> Result<bool>
1078where
1079    T: FusedScalar,
1080    O: StaticOutput<T>,
1081{
1082    let Some(kind) = classify_static_specialization(plan) else {
1083        return Ok(false);
1084    };
1085    #[cfg(test)]
1086    if O::IS_UNINITIALIZED {
1087        record_uninitialized_static_family_hit(kind);
1088    }
1089
1090    match kind {
1091        StaticFusedKind::Unary(op, a) => {
1092            map_into_validated(dest, &inputs[a], |x| O::write(eval_unary(op, x)), validated)?
1093        }
1094        StaticFusedKind::Binary(op, a, b) => zip_map2_into_validated(
1095            dest,
1096            &inputs[a],
1097            &inputs[b],
1098            |x, y| O::write(eval_binary(op, x, y)),
1099            validated,
1100        )?,
1101        StaticFusedKind::Ternary(op, a, b, c) => zip_map3_into_validated(
1102            dest,
1103            &inputs[a],
1104            &inputs[b],
1105            &inputs[c],
1106            |x, y, z| O::write(eval_ternary(op, x, y, z)),
1107            validated,
1108        )?,
1109        StaticFusedKind::AddMulLeft => zip_map2_into_validated(
1110            dest,
1111            &inputs[0],
1112            &inputs[1],
1113            |a, b| O::write(a.fused_add(b).fused_multiply(a)),
1114            validated,
1115        )?,
1116        StaticFusedKind::AddMulRight => zip_map2_into_validated(
1117            dest,
1118            &inputs[0],
1119            &inputs[1],
1120            |a, b| O::write(a.fused_multiply(a.fused_add(b))),
1121            validated,
1122        )?,
1123        StaticFusedKind::MulAddExp => zip_map3_into_validated(
1124            dest,
1125            &inputs[0],
1126            &inputs[1],
1127            &inputs[2],
1128            |a, b, c| O::write(a.fused_multiply(b).fused_add(c).fused_exp()),
1129            validated,
1130        )?,
1131        StaticFusedKind::DivClampSqrtRsqrt => zip_map4_into_validated(
1132            dest,
1133            &inputs[0],
1134            &inputs[1],
1135            &inputs[2],
1136            &inputs[3],
1137            |a, b, lo, hi| {
1138                O::write(
1139                    a.fused_divide(b)
1140                        .fused_maximum(lo)
1141                        .fused_minimum(hi)
1142                        .fused_sqrt()
1143                        .fused_rsqrt(),
1144                )
1145            },
1146            validated,
1147        )?,
1148    }
1149    Ok(true)
1150}
1151
1152fn try_static_specialization<T: FusedScalar>(
1153    dests: &mut [StridedViewMut<'_, T>],
1154    inputs: &[StridedView<'_, T>],
1155    plan: &FusedPlan,
1156) -> Result<bool> {
1157    if dests.len() != 1 {
1158        return Ok(false);
1159    }
1160    let validated = validate_destination_layout_without_alloc(dests[0].dims(), dests[0].strides())?;
1161    try_static_specialization_validated::<T, InitializedStaticOutput>(
1162        &mut dests[0],
1163        inputs,
1164        plan,
1165        validated,
1166    )
1167}
1168
1169unsafe fn interpret_inner_loop<T: FusedScalar>(
1170    dst_ptrs: &[*mut T],
1171    input_ptrs: &[*const T],
1172    plan: &FusedPlan,
1173    offsets: &[isize],
1174    len: usize,
1175    strides: &[isize],
1176) {
1177    let output_count = dst_ptrs.len();
1178    let mut regs = Vec::with_capacity(plan.input_count + plan.ops.len());
1179
1180    for i in 0..len {
1181        let i = i as isize;
1182        regs.clear();
1183
1184        for (input_index, &input_ptr) in input_ptrs.iter().enumerate() {
1185            let stride_index = output_count + input_index;
1186            regs.push(*input_ptr.offset(offsets[stride_index] + i * strides[stride_index]));
1187        }
1188
1189        for inst in &plan.ops {
1190            regs.push(eval_op(inst.op, &regs, &inst.inputs));
1191        }
1192
1193        for (output_index, &dst_ptr) in dst_ptrs.iter().enumerate() {
1194            *dst_ptr.offset(offsets[output_index] + i * strides[output_index]) =
1195                regs[plan.outputs[output_index]];
1196        }
1197    }
1198}
1199
1200fn interpret_fused_elementwise_into<T: FusedScalar>(
1201    dests: &mut [StridedViewMut<'_, T>],
1202    inputs: &[StridedView<'_, T>],
1203    plan: &FusedPlan,
1204) -> Result<()> {
1205    #[cfg(feature = "parallel")]
1206    {
1207        let dims = dests[0].dims().to_vec();
1208        if total_len(&dims) == 0 {
1209            return Ok(());
1210        }
1211
1212        let dst_ptrs: Vec<*mut T> = dests.iter_mut().map(|dest| dest.as_mut_ptr()).collect();
1213        let input_ptrs: Vec<*const T> = inputs.iter().map(StridedView::ptr).collect();
1214
1215        let mut strides_list: Vec<&[isize]> = Vec::with_capacity(dests.len() + inputs.len());
1216        for dest in dests.iter() {
1217            strides_list.push(dest.strides());
1218        }
1219        for input in inputs {
1220            strides_list.push(input.strides());
1221        }
1222
1223        let elem_size = std::mem::size_of::<T>();
1224        let total = total_len(&dims);
1225        let (fused_dims, ordered_strides, kernel_plan) = if total <= SMALL_TENSOR_THRESHOLD {
1226            build_plan_fused_small(&dims, &strides_list)
1227        } else {
1228            build_plan_fused(&dims, &strides_list, Some(0), elem_size)
1229        };
1230
1231        let total: usize = fused_dims.iter().product();
1232        let nthreads = crate::execution_policy::rayon_threads();
1233        if total > MINTHREADLENGTH && nthreads > 1 {
1234            let dst_send: Vec<SendPtr<T>> = dst_ptrs.iter().map(|&ptr| SendPtr(ptr)).collect();
1235            let input_send: Vec<SendPtr<T>> = input_ptrs
1236                .iter()
1237                .map(|&ptr| SendPtr(ptr as *mut T))
1238                .collect();
1239
1240            let costs = compute_costs(&ordered_strides);
1241            let initial_offsets = vec![0isize; ordered_strides.len()];
1242            return mapreduce_threaded(
1243                &fused_dims,
1244                &kernel_plan.block,
1245                &ordered_strides,
1246                &initial_offsets,
1247                &costs,
1248                nthreads,
1249                0,
1250                1,
1251                &|dims, blocks, strides_list, offsets| {
1252                    let dst_ptrs: Vec<*mut T> = dst_send.iter().map(|ptr| ptr.as_ptr()).collect();
1253                    let input_ptrs: Vec<*const T> =
1254                        input_send.iter().map(|ptr| ptr.as_const()).collect();
1255                    for_each_inner_block_with_offsets(
1256                        dims,
1257                        blocks,
1258                        strides_list,
1259                        offsets,
1260                        |offsets, len, strides| {
1261                            unsafe {
1262                                interpret_inner_loop(
1263                                    &dst_ptrs,
1264                                    &input_ptrs,
1265                                    plan,
1266                                    offsets,
1267                                    len,
1268                                    strides,
1269                                );
1270                            }
1271                            Ok(())
1272                        },
1273                    )
1274                },
1275            );
1276        }
1277    }
1278
1279    interpret_fused_elementwise_into_serial(dests, inputs, plan)
1280}
1281
1282fn interpret_fused_elementwise_into_serial<T: FusedScalar>(
1283    dests: &mut [StridedViewMut<'_, T>],
1284    inputs: &[StridedView<'_, T>],
1285    plan: &FusedPlan,
1286) -> Result<()> {
1287    let dims = dests[0].dims().to_vec();
1288    if total_len(&dims) == 0 {
1289        return Ok(());
1290    }
1291
1292    let dst_ptrs: Vec<*mut T> = dests.iter_mut().map(|dest| dest.as_mut_ptr()).collect();
1293    let input_ptrs: Vec<*const T> = inputs.iter().map(StridedView::ptr).collect();
1294
1295    let mut strides_list: Vec<&[isize]> = Vec::with_capacity(dests.len() + inputs.len());
1296    for dest in dests.iter() {
1297        strides_list.push(dest.strides());
1298    }
1299    for input in inputs {
1300        strides_list.push(input.strides());
1301    }
1302
1303    let elem_size = std::mem::size_of::<T>();
1304    let total = total_len(&dims);
1305    let (fused_dims, ordered_strides, kernel_plan) = if total <= SMALL_TENSOR_THRESHOLD {
1306        build_plan_fused_small(&dims, &strides_list)
1307    } else {
1308        build_plan_fused(&dims, &strides_list, Some(0), elem_size)
1309    };
1310
1311    let initial_offsets = vec![0isize; ordered_strides.len()];
1312    for_each_inner_block_preordered(
1313        &fused_dims,
1314        &kernel_plan.block,
1315        &ordered_strides,
1316        &initial_offsets,
1317        |offsets, len, strides| {
1318            unsafe {
1319                interpret_inner_loop(&dst_ptrs, &input_ptrs, plan, offsets, len, strides);
1320            }
1321            Ok(())
1322        },
1323    )
1324}
1325
1326pub(crate) fn fused_elementwise_into_serial<T: FusedScalar>(
1327    dests: &mut [StridedViewMut<'_, T>],
1328    inputs: &[StridedView<'_, T>],
1329    plan: &FusedPlan,
1330) -> Result<()> {
1331    validate_plan_for_scalar::<T>(plan, inputs.len(), dests.len())?;
1332    validate_shapes(dests, inputs)?;
1333    interpret_fused_elementwise_into_serial(dests, inputs, plan)
1334}
1335
1336unsafe fn interpret_inner_loop_uninit<T: FusedScalar>(
1337    dst_ptr: *mut MaybeUninit<T>,
1338    input_ptrs: &[*const T],
1339    plan: &FusedPlan,
1340    offsets: &[isize],
1341    len: usize,
1342    strides: &[isize],
1343) {
1344    let mut regs = Vec::with_capacity(plan.input_count + plan.ops.len());
1345    for i in 0..len {
1346        let i = i as isize;
1347        regs.clear();
1348        for (input_index, &input_ptr) in input_ptrs.iter().enumerate() {
1349            let stride_index = 1 + input_index;
1350            regs.push(*input_ptr.offset(offsets[stride_index] + i * strides[stride_index]));
1351        }
1352        for inst in &plan.ops {
1353            regs.push(eval_op(inst.op, &regs, &inst.inputs));
1354        }
1355        *dst_ptr.offset(offsets[0] + i * strides[0]) = MaybeUninit::new(regs[plan.outputs[0]]);
1356    }
1357}
1358
1359pub(crate) fn fused_elementwise_into_uninit<T: FusedScalar>(
1360    dest: &mut StridedViewMut<'_, MaybeUninit<T>>,
1361    inputs: &[StridedView<'_, T>],
1362    plan: &FusedPlan,
1363    serial: bool,
1364    validated: ValidatedDestinationLayout,
1365) -> Result<()> {
1366    #[cfg(not(feature = "parallel"))]
1367    let _ = serial;
1368    validate_plan_for_scalar::<T>(plan, inputs.len(), 1)?;
1369    for input in inputs {
1370        ensure_same_shape(dest.dims(), input.dims())?;
1371    }
1372
1373    if !serial
1374        && try_static_specialization_validated::<T, UninitializedStaticOutput>(
1375            dest, inputs, plan, validated,
1376        )?
1377    {
1378        return Ok(());
1379    }
1380
1381    let dims = dest.dims();
1382    if total_len(dims) == 0 {
1383        return Ok(());
1384    }
1385    let dst_ptr = dest.as_mut_ptr();
1386    let input_ptrs: Vec<*const T> = inputs.iter().map(StridedView::ptr).collect();
1387    let mut strides_list: Vec<&[isize]> = Vec::with_capacity(1 + inputs.len());
1388    strides_list.push(dest.strides());
1389    for input in inputs {
1390        strides_list.push(input.strides());
1391    }
1392    let total = total_len(dims);
1393    let (fused_dims, ordered_strides, kernel_plan) = if total <= SMALL_TENSOR_THRESHOLD {
1394        build_plan_fused_small(dims, &strides_list)
1395    } else {
1396        build_plan_fused(dims, &strides_list, Some(0), core::mem::size_of::<T>())
1397    };
1398
1399    #[cfg(feature = "parallel")]
1400    {
1401        let total: usize = fused_dims.iter().product();
1402        let nthreads = crate::execution_policy::rayon_threads();
1403        if !serial && total > MINTHREADLENGTH && nthreads > 1 {
1404            let dst_send = SendPtr(dst_ptr);
1405            let input_send: Vec<SendPtr<T>> = input_ptrs
1406                .iter()
1407                .map(|&ptr| SendPtr(ptr as *mut T))
1408                .collect();
1409            let costs = compute_costs(&ordered_strides);
1410            let initial_offsets = vec![0isize; ordered_strides.len()];
1411            return mapreduce_threaded(
1412                &fused_dims,
1413                &kernel_plan.block,
1414                &ordered_strides,
1415                &initial_offsets,
1416                &costs,
1417                nthreads,
1418                0,
1419                1,
1420                &|dims, blocks, strides_list, offsets| {
1421                    let input_ptrs: Vec<*const T> =
1422                        input_send.iter().map(|ptr| ptr.as_const()).collect();
1423                    for_each_inner_block_with_offsets(
1424                        dims,
1425                        blocks,
1426                        strides_list,
1427                        offsets,
1428                        |offsets, len, strides| {
1429                            unsafe {
1430                                interpret_inner_loop_uninit(
1431                                    dst_send.as_ptr(),
1432                                    &input_ptrs,
1433                                    plan,
1434                                    offsets,
1435                                    len,
1436                                    strides,
1437                                );
1438                            }
1439                            Ok(())
1440                        },
1441                    )
1442                },
1443            );
1444        }
1445    }
1446
1447    let initial_offsets = vec![0isize; ordered_strides.len()];
1448    for_each_inner_block_preordered(
1449        &fused_dims,
1450        &kernel_plan.block,
1451        &ordered_strides,
1452        &initial_offsets,
1453        |offsets, len, strides| {
1454            unsafe {
1455                interpret_inner_loop_uninit(dst_ptr, &input_ptrs, plan, offsets, len, strides);
1456            }
1457            Ok(())
1458        },
1459    )
1460}
1461
1462/// Evaluate a runtime-DAG elementwise plan into one or more destinations.
1463///
1464/// The plan is validated before any destination is written:
1465///
1466/// - `inputs.len()` must equal `plan.input_count`;
1467/// - `dests.len()` must equal `plan.outputs.len()`;
1468/// - instruction operands must reference earlier SSA values with the right
1469///   arity for their [`FusedOp`];
1470/// - every input and destination must have exactly the destination shape;
1471/// - each mutable destination layout must be injective, so two logical output
1472///   elements never map to the same memory address.
1473///
1474/// The implementation dispatches known single-output plans to existing static
1475/// `map_into`/`zip_map*_into` kernels and uses a generic interpreter fallback
1476/// for arbitrary validated DAGs. Overlapping source/destination memory is not
1477/// supported by the strided kernels generally.
1478///
1479/// Real `Maximum`, `Minimum`, and `Clamp` use Rust `f32`/`f64` `max`/`min`
1480/// semantics. Complex `Abs` returns the norm in the real component; complex
1481/// `Maximum`, `Minimum`, and `Clamp` compare by squared norm. Signed integer
1482/// `Add`, `Multiply`, `Negate`, and `Abs` use wrapping arithmetic. `bool`
1483/// supports only copy-like identity plans and `Conj`; ambiguous arithmetic and
1484/// transcendental op/dtype pairs are rejected before any destination is written.
1485pub fn fused_elementwise_into<T: FusedScalar>(
1486    dests: &mut [StridedViewMut<'_, T>],
1487    inputs: &[StridedView<'_, T>],
1488    plan: &FusedPlan,
1489) -> Result<()> {
1490    validate_plan_for_scalar::<T>(plan, inputs.len(), dests.len())?;
1491    validate_shapes(dests, inputs)?;
1492    if try_static_specialization(dests, inputs, plan)? {
1493        return Ok(());
1494    }
1495    interpret_fused_elementwise_into(dests, inputs, plan)
1496}
1497
1498#[cfg(test)]
1499mod tests {
1500    use super::*;
1501    use crate::StridedArray;
1502    #[cfg(feature = "parallel")]
1503    use std::sync::Mutex;
1504
1505    #[cfg(feature = "parallel")]
1506    static UNINIT_WORKER_IDS: Mutex<Vec<std::thread::ThreadId>> = Mutex::new(Vec::new());
1507
1508    #[cfg(feature = "parallel")]
1509    #[derive(Clone, Copy)]
1510    struct ThreadTracked(u64);
1511
1512    #[cfg(feature = "parallel")]
1513    impl ThreadTracked {
1514        fn observed(value: u64) -> Self {
1515            let id = std::thread::current().id();
1516            let mut ids = UNINIT_WORKER_IDS.lock().unwrap();
1517            if !ids.contains(&id) {
1518                ids.push(id);
1519            }
1520            drop(ids);
1521            for _ in 0..32 {
1522                std::hint::spin_loop();
1523            }
1524            Self(value)
1525        }
1526    }
1527
1528    #[cfg(feature = "parallel")]
1529    impl FusedScalar for ThreadTracked {
1530        fn fused_add(self, rhs: Self) -> Self {
1531            Self::observed(self.0 + rhs.0)
1532        }
1533
1534        fn fused_multiply(self, rhs: Self) -> Self {
1535            Self(self.0 * rhs.0)
1536        }
1537
1538        fn fused_negate(self) -> Self {
1539            self
1540        }
1541
1542        fn fused_conj(self) -> Self {
1543            self
1544        }
1545
1546        fn fused_divide(self, _rhs: Self) -> Self {
1547            self
1548        }
1549
1550        fn fused_abs(self) -> Self {
1551            self
1552        }
1553
1554        fn fused_maximum(self, _rhs: Self) -> Self {
1555            self
1556        }
1557
1558        fn fused_minimum(self, _rhs: Self) -> Self {
1559            self
1560        }
1561
1562        fn fused_clamp(self, _min: Self, _max: Self) -> Self {
1563            self
1564        }
1565
1566        fn fused_exp(self) -> Self {
1567            self
1568        }
1569
1570        fn fused_log(self) -> Self {
1571            self
1572        }
1573
1574        fn fused_sin(self) -> Self {
1575            self
1576        }
1577
1578        fn fused_cos(self) -> Self {
1579            self
1580        }
1581
1582        fn fused_tanh(self) -> Self {
1583            self
1584        }
1585
1586        fn fused_sqrt(self) -> Self {
1587            self
1588        }
1589
1590        fn fused_rsqrt(self) -> Self {
1591            self
1592        }
1593
1594        fn fused_pow(self, _rhs: Self) -> Self {
1595            self
1596        }
1597
1598        fn fused_expm1(self) -> Self {
1599            self
1600        }
1601
1602        fn fused_log1p(self) -> Self {
1603            self
1604        }
1605    }
1606
1607    fn input(values: &[f64]) -> StridedArray<f64> {
1608        StridedArray::from_parts(values.to_vec(), &[values.len()], &[1], 0).unwrap()
1609    }
1610
1611    fn run_static(plan: &FusedPlan, arrays: &[StridedArray<f64>]) -> (bool, Vec<f64>) {
1612        let inputs: Vec<_> = arrays.iter().map(|array| array.view()).collect();
1613        let mut out = StridedArray::<f64>::col_major(arrays[0].dims());
1614        let used_static = {
1615            let mut dests = [out.view_mut()];
1616            try_static_specialization(&mut dests, &inputs, plan).unwrap()
1617        };
1618        (used_static, out.iter().copied().collect())
1619    }
1620
1621    fn run_interpreter(plan: &FusedPlan, arrays: &[StridedArray<f64>]) -> Vec<f64> {
1622        let inputs: Vec<_> = arrays.iter().map(|array| array.view()).collect();
1623        let mut out = StridedArray::<f64>::col_major(arrays[0].dims());
1624        {
1625            let mut dests = [out.view_mut()];
1626            interpret_fused_elementwise_into(&mut dests, &inputs, plan).unwrap();
1627        }
1628        out.iter().copied().collect()
1629    }
1630
1631    fn assert_static_matches_interpreter(plan: FusedPlan, arrays: &[StridedArray<f64>]) {
1632        let (used_static, static_values) = run_static(&plan, arrays);
1633        let interpreter_values = run_interpreter(&plan, arrays);
1634
1635        assert!(used_static, "plan should use static specialization");
1636        assert_eq!(static_values.len(), interpreter_values.len());
1637        for (actual, expected) in static_values.iter().zip(interpreter_values.iter()) {
1638            assert!((actual - expected).abs() < 1e-12);
1639        }
1640    }
1641
1642    #[cfg(feature = "parallel")]
1643    #[test]
1644    fn uninitialized_nonserial_replay_selects_every_static_family() {
1645        use crate::ExecContext;
1646
1647        fn run(plan: FusedPlan, arrays: &[StridedArray<f64>]) {
1648            let inputs: Vec<_> = arrays.iter().map(|array| array.view()).collect();
1649            let mut output = vec![MaybeUninit::uninit(); arrays[0].len()];
1650            let mut dest = StridedViewMut::new(&mut output, arrays[0].dims(), &[1], 0).unwrap();
1651            let validated =
1652                validate_destination_layout_without_alloc(dest.dims(), dest.strides()).unwrap();
1653            ExecContext::max_threads(2).unwrap().run(|| {
1654                fused_elementwise_into_uninit(&mut dest, &inputs, &plan, false, validated).unwrap();
1655            });
1656        }
1657
1658        reset_uninitialized_static_family_hits();
1659        let a = input(&[4.0, 9.0, 16.0]);
1660        let b = input(&[2.0, 3.0, 4.0]);
1661        let c = input(&[1.0, 1.5, 2.0]);
1662        let d = input(&[4.0, 4.0, 4.0]);
1663
1664        run(
1665            single_op(1, FusedOp::Sqrt, vec![0]),
1666            std::slice::from_ref(&a),
1667        );
1668        run(
1669            single_op(2, FusedOp::Add, vec![0, 1]),
1670            &[a.clone(), b.clone()],
1671        );
1672        run(
1673            single_op(3, FusedOp::Clamp, vec![0, 1, 2]),
1674            &[a.clone(), c.clone(), d.clone()],
1675        );
1676        run(
1677            FusedPlan {
1678                input_count: 2,
1679                outputs: vec![3],
1680                ops: vec![
1681                    FusedInst {
1682                        op: FusedOp::Add,
1683                        inputs: vec![0, 1],
1684                    },
1685                    FusedInst {
1686                        op: FusedOp::Multiply,
1687                        inputs: vec![2, 0],
1688                    },
1689                ],
1690            },
1691            &[a.clone(), b.clone()],
1692        );
1693        run(
1694            FusedPlan {
1695                input_count: 2,
1696                outputs: vec![3],
1697                ops: vec![
1698                    FusedInst {
1699                        op: FusedOp::Add,
1700                        inputs: vec![0, 1],
1701                    },
1702                    FusedInst {
1703                        op: FusedOp::Multiply,
1704                        inputs: vec![0, 2],
1705                    },
1706                ],
1707            },
1708            &[a.clone(), b.clone()],
1709        );
1710        run(
1711            FusedPlan {
1712                input_count: 3,
1713                outputs: vec![5],
1714                ops: vec![
1715                    FusedInst {
1716                        op: FusedOp::Multiply,
1717                        inputs: vec![0, 1],
1718                    },
1719                    FusedInst {
1720                        op: FusedOp::Add,
1721                        inputs: vec![3, 2],
1722                    },
1723                    FusedInst {
1724                        op: FusedOp::Exp,
1725                        inputs: vec![4],
1726                    },
1727                ],
1728            },
1729            &[a.clone(), b.clone(), c.clone()],
1730        );
1731        run(
1732            FusedPlan {
1733                input_count: 4,
1734                outputs: vec![8],
1735                ops: vec![
1736                    FusedInst {
1737                        op: FusedOp::Divide,
1738                        inputs: vec![0, 1],
1739                    },
1740                    FusedInst {
1741                        op: FusedOp::Maximum,
1742                        inputs: vec![4, 2],
1743                    },
1744                    FusedInst {
1745                        op: FusedOp::Minimum,
1746                        inputs: vec![5, 3],
1747                    },
1748                    FusedInst {
1749                        op: FusedOp::Sqrt,
1750                        inputs: vec![6],
1751                    },
1752                    FusedInst {
1753                        op: FusedOp::Rsqrt,
1754                        inputs: vec![7],
1755                    },
1756                ],
1757            },
1758            &[a, b, c, d],
1759        );
1760
1761        assert_eq!(uninitialized_static_family_hits(), [1; 7]);
1762    }
1763
1764    #[cfg(feature = "parallel")]
1765    #[test]
1766    fn uninitialized_fused_replay_respects_serial_and_bounded_contexts_above_threshold() {
1767        use crate::{with_execution_policy, ExecContext, ExecutionPolicy};
1768        use std::num::NonZeroUsize;
1769
1770        let len = MINTHREADLENGTH + 65;
1771        let lhs = vec![ThreadTracked(1); len];
1772        let rhs = vec![ThreadTracked(2); len];
1773        let lhs = StridedView::new(&lhs, &[len], &[1], 0).unwrap();
1774        let rhs = StridedView::new(&rhs, &[len], &[1], 0).unwrap();
1775        let inputs = [lhs, rhs];
1776        let plan = FusedPlan {
1777            input_count: 2,
1778            outputs: vec![2],
1779            ops: vec![FusedInst {
1780                op: FusedOp::Add,
1781                inputs: vec![0, 1],
1782            }],
1783        };
1784        let four = NonZeroUsize::new(4).unwrap();
1785
1786        let caller = std::thread::current().id();
1787        let mut output = vec![MaybeUninit::uninit(); len];
1788        let mut dest = StridedViewMut::new(&mut output, &[len], &[1], 0).unwrap();
1789        let validated =
1790            validate_destination_layout_without_alloc(dest.dims(), dest.strides()).unwrap();
1791        UNINIT_WORKER_IDS.lock().unwrap().clear();
1792        with_execution_policy(ExecutionPolicy::Rayon { max_threads: four }, || {
1793            fused_elementwise_into_uninit(&mut dest, &inputs, &plan, true, validated).unwrap();
1794        });
1795        assert_eq!(*UNINIT_WORKER_IDS.lock().unwrap(), vec![caller]);
1796
1797        let mut output = vec![MaybeUninit::uninit(); len];
1798        let mut dest = StridedViewMut::new(&mut output, &[len], &[1], 0).unwrap();
1799        let validated =
1800            validate_destination_layout_without_alloc(dest.dims(), dest.strides()).unwrap();
1801        UNINIT_WORKER_IDS.lock().unwrap().clear();
1802        let ctx = ExecContext::max_threads(2).unwrap();
1803        ctx.run(|| {
1804            fused_elementwise_into_uninit(&mut dest, &inputs, &plan, false, validated).unwrap();
1805        });
1806        let workers = UNINIT_WORKER_IDS.lock().unwrap();
1807        assert!(
1808            workers.len() > 1,
1809            "bounded replay must cross the parallel threshold"
1810        );
1811        assert!(workers.len() <= 2, "bounded replay exceeded max_threads(2)");
1812    }
1813
1814    // Single-instruction plan whose sole output is the instruction result. Such
1815    // plans always hit `try_static_specialization`, and both the static and the
1816    // interpreter path dispatch through the scalar `FusedScalar` methods, so
1817    // iterating every op exercises each scalar implementation.
1818    fn single_op(input_count: usize, op: FusedOp, inputs: Vec<usize>) -> FusedPlan {
1819        FusedPlan {
1820            input_count,
1821            outputs: vec![input_count],
1822            ops: vec![FusedInst { op, inputs }],
1823        }
1824    }
1825
1826    #[test]
1827    fn specializes_unary_exp() {
1828        let a = input(&[1.0, 2.0, 3.0]);
1829        let plan = FusedPlan {
1830            input_count: 1,
1831            outputs: vec![1],
1832            ops: vec![FusedInst {
1833                op: FusedOp::Exp,
1834                inputs: vec![0],
1835            }],
1836        };
1837
1838        assert_static_matches_interpreter(plan, &[a]);
1839    }
1840
1841    #[test]
1842    fn specializes_binary_add() {
1843        let a = input(&[1.0, 2.0, 3.0]);
1844        let b = input(&[10.0, 20.0, 30.0]);
1845        let plan = FusedPlan {
1846            input_count: 2,
1847            outputs: vec![2],
1848            ops: vec![FusedInst {
1849                op: FusedOp::Add,
1850                inputs: vec![0, 1],
1851            }],
1852        };
1853
1854        assert_static_matches_interpreter(plan, &[a, b]);
1855    }
1856
1857    #[test]
1858    fn specializes_ternary_clamp() {
1859        let x = input(&[1.0, 2.0, 3.0]);
1860        let lo = input(&[1.5, 1.5, 1.5]);
1861        let hi = input(&[2.5, 2.5, 2.5]);
1862        let plan = FusedPlan {
1863            input_count: 3,
1864            outputs: vec![3],
1865            ops: vec![FusedInst {
1866                op: FusedOp::Clamp,
1867                inputs: vec![0, 1, 2],
1868            }],
1869        };
1870
1871        assert_static_matches_interpreter(plan, &[x, lo, hi]);
1872    }
1873
1874    #[test]
1875    fn specializes_add_then_multiply_reusing_input() {
1876        let a = input(&[1.0, 2.0, 3.0]);
1877        let b = input(&[10.0, 20.0, 30.0]);
1878        let plan = FusedPlan {
1879            input_count: 2,
1880            outputs: vec![3],
1881            ops: vec![
1882                FusedInst {
1883                    op: FusedOp::Add,
1884                    inputs: vec![0, 1],
1885                },
1886                FusedInst {
1887                    op: FusedOp::Multiply,
1888                    inputs: vec![2, 0],
1889                },
1890            ],
1891        };
1892
1893        assert_static_matches_interpreter(plan, &[a, b]);
1894    }
1895
1896    #[test]
1897    fn specializes_exp_of_multiply_add_chain() {
1898        let a = input(&[1.0, 2.0, 3.0]);
1899        let b = input(&[0.5, 1.5, 2.5]);
1900        let c = input(&[2.0, 2.0, 2.0]);
1901        let plan = FusedPlan {
1902            input_count: 3,
1903            outputs: vec![5],
1904            ops: vec![
1905                FusedInst {
1906                    op: FusedOp::Multiply,
1907                    inputs: vec![0, 1],
1908                },
1909                FusedInst {
1910                    op: FusedOp::Add,
1911                    inputs: vec![3, 2],
1912                },
1913                FusedInst {
1914                    op: FusedOp::Exp,
1915                    inputs: vec![4],
1916                },
1917            ],
1918        };
1919
1920        assert_static_matches_interpreter(plan, &[a, b, c]);
1921    }
1922
1923    #[test]
1924    fn specializes_divide_clamp_sqrt_rsqrt_chain() {
1925        let a = input(&[4.0, 9.0, 16.0]);
1926        let b = input(&[2.0, 3.0, 4.0]);
1927        let lo = input(&[1.5, 1.5, 1.5]);
1928        let hi = input(&[8.0, 8.0, 8.0]);
1929        let plan = FusedPlan {
1930            input_count: 4,
1931            outputs: vec![8],
1932            ops: vec![
1933                FusedInst {
1934                    op: FusedOp::Divide,
1935                    inputs: vec![0, 1],
1936                },
1937                FusedInst {
1938                    op: FusedOp::Maximum,
1939                    inputs: vec![4, 2],
1940                },
1941                FusedInst {
1942                    op: FusedOp::Minimum,
1943                    inputs: vec![5, 3],
1944                },
1945                FusedInst {
1946                    op: FusedOp::Sqrt,
1947                    inputs: vec![6],
1948                },
1949                FusedInst {
1950                    op: FusedOp::Rsqrt,
1951                    inputs: vec![7],
1952                },
1953            ],
1954        };
1955
1956        assert_static_matches_interpreter(plan, &[a, b, lo, hi]);
1957    }
1958
1959    // Real negate/conj/abs were the only real scalar ops not reached by the
1960    // chains above; cover them so the real `FusedScalar` impl is fully exercised.
1961    #[test]
1962    fn specializes_real_negate_conj_abs() {
1963        for op in [FusedOp::Negate, FusedOp::Conj, FusedOp::Abs] {
1964            let x = input(&[-1.5, 2.0, -3.5]);
1965            assert_static_matches_interpreter(single_op(1, op, vec![0]), &[x]);
1966        }
1967    }
1968
1969    // The complex `FusedScalar` impl had no coverage at all (every existing test
1970    // used f64). Run every op over Complex64 so both the static and interpreter
1971    // paths dispatch through the complex scalar methods.
1972    #[test]
1973    fn specializes_every_op_over_complex() {
1974        use num_complex::Complex64;
1975
1976        let c = |re: f64, im: f64| Complex64::new(re, im);
1977        let cinput = |values: &[Complex64]| {
1978            StridedArray::from_parts(values.to_vec(), &[values.len()], &[1], 0).unwrap()
1979        };
1980        let assert_complex_match = |plan: FusedPlan, arrays: &[StridedArray<Complex64>]| {
1981            let inputs: Vec<_> = arrays.iter().map(|array| array.view()).collect();
1982            let mut static_out = StridedArray::<Complex64>::col_major(arrays[0].dims());
1983            let used_static = {
1984                let mut dests = [static_out.view_mut()];
1985                try_static_specialization(&mut dests, &inputs, &plan).unwrap()
1986            };
1987            let mut interp_out = StridedArray::<Complex64>::col_major(arrays[0].dims());
1988            {
1989                let mut dests = [interp_out.view_mut()];
1990                interpret_fused_elementwise_into(&mut dests, &inputs, &plan).unwrap();
1991            }
1992            assert!(used_static, "single-op plan should specialize");
1993            for (actual, expected) in static_out.iter().zip(interp_out.iter()) {
1994                assert!((actual - expected).norm() < 1e-9, "{actual} vs {expected}");
1995            }
1996        };
1997
1998        // Positive-real-part, nonzero operands keep div/log/sqrt/pow well defined.
1999        let a = cinput(&[c(1.5, 0.5), c(2.0, -1.0), c(0.7, 0.3)]);
2000        let b = cinput(&[c(1.1, 0.2), c(0.9, 0.4), c(1.3, -0.6)]);
2001        let d = cinput(&[c(2.0, 0.0), c(2.0, 0.0), c(2.0, 0.0)]);
2002
2003        for op in [
2004            FusedOp::Negate,
2005            FusedOp::Conj,
2006            FusedOp::Abs,
2007            FusedOp::Exp,
2008            FusedOp::Log,
2009            FusedOp::Sin,
2010            FusedOp::Cos,
2011            FusedOp::Tanh,
2012            FusedOp::Sqrt,
2013            FusedOp::Rsqrt,
2014            FusedOp::Expm1,
2015            FusedOp::Log1p,
2016        ] {
2017            assert_complex_match(single_op(1, op, vec![0]), std::slice::from_ref(&a));
2018        }
2019        for op in [
2020            FusedOp::Add,
2021            FusedOp::Multiply,
2022            FusedOp::Divide,
2023            FusedOp::Maximum,
2024            FusedOp::Minimum,
2025            FusedOp::Pow,
2026        ] {
2027            assert_complex_match(single_op(2, op, vec![0, 1]), &[a.clone(), b.clone()]);
2028        }
2029        assert_complex_match(
2030            single_op(3, FusedOp::Clamp, vec![0, 1, 2]),
2031            &[a.clone(), b.clone(), d.clone()],
2032        );
2033    }
2034
2035    // Error branches in plan/layout validation that the positive tests skip.
2036    #[test]
2037    fn validate_plan_rejects_out_of_range_output() {
2038        // output id refers to a value that no instruction produces.
2039        let plan = FusedPlan {
2040            input_count: 1,
2041            outputs: vec![5],
2042            ops: vec![FusedInst {
2043                op: FusedOp::Exp,
2044                inputs: vec![0],
2045            }],
2046        };
2047        assert!(validate_plan(&plan, 1, 1).is_err());
2048    }
2049
2050    #[test]
2051    fn validate_plan_rejects_zero_outputs() {
2052        let plan = FusedPlan {
2053            input_count: 1,
2054            outputs: vec![],
2055            ops: vec![],
2056        };
2057        assert!(validate_plan(&plan, 1, 0).is_err());
2058    }
2059
2060    #[test]
2061    fn is_injective_layout_rejects_rank_and_broadcast_mismatch() {
2062        assert!(!is_injective_layout(&[2, 3], &[1]));
2063        assert!(!is_injective_layout(&[2, 2], &[0, 1]));
2064        assert!(is_injective_layout(&[1], &[0]));
2065    }
2066
2067    #[test]
2068    fn is_injective_layout_rejects_unrepresentable_offset_spans() {
2069        let positive = isize::MAX / 2 + 1;
2070        let negative = isize::MIN / 2 - 1;
2071        assert!(!is_injective_layout(&[2, 2], &[positive, isize::MAX]));
2072        assert!(!is_injective_layout(&[2, 2], &[negative, isize::MIN]));
2073    }
2074}