Skip to main content

strided_kernel/
fused.rs

1//! Runtime-DAG fused elementwise kernels.
2
3use crate::kernel::{
4    build_plan_fused, build_plan_fused_small, ensure_same_shape, for_each_inner_block_preordered,
5    total_len, SMALL_TENSOR_THRESHOLD,
6};
7use crate::map_view::{map_into, zip_map2_into, zip_map3_into, zip_map4_into};
8use crate::{MaybeSendSync, Result, StridedError, StridedView, StridedViewMut};
9
10#[cfg(feature = "parallel")]
11use crate::fuse::compute_costs;
12#[cfg(feature = "parallel")]
13use crate::threading::{
14    for_each_inner_block_with_offsets, mapreduce_threaded, SendPtr, MINTHREADLENGTH,
15};
16
17/// Runtime scalar operation for a fused elementwise plan.
18#[derive(Clone, Copy, Debug, Eq, PartialEq)]
19pub enum FusedOp {
20    Add,
21    Multiply,
22    Negate,
23    Conj,
24    Divide,
25    Abs,
26    Maximum,
27    Minimum,
28    Clamp,
29    Exp,
30    Log,
31    Sin,
32    Cos,
33    Tanh,
34    Sqrt,
35    Rsqrt,
36    Pow,
37    Expm1,
38    Log1p,
39}
40
41/// One SSA instruction in a [`FusedPlan`].
42#[derive(Clone, Debug, Eq, PartialEq)]
43pub struct FusedInst {
44    pub op: FusedOp,
45    pub inputs: Vec<usize>,
46}
47
48/// Topologically ordered fused elementwise SSA DAG.
49///
50/// Values are numbered in evaluation order. Input values occupy
51/// `0..input_count`; each instruction appends one value after the previous
52/// inputs/instructions. For example, with `input_count == 2`, the first
53/// instruction writes value `2`, the second writes value `3`, and so on.
54/// `outputs` contains the value ids to write to `dests` in order.
55///
56/// All inputs and destinations passed to [`fused_elementwise_into`] must have
57/// the same shape and scalar type. Broadcast inputs should be represented with
58/// `StridedView::broadcast` before building the plan; the fused API does not
59/// perform implicit broadcasting.
60#[derive(Clone, Debug, Eq, PartialEq)]
61pub struct FusedPlan {
62    pub input_count: usize,
63    pub outputs: Vec<usize>,
64    pub ops: Vec<FusedInst>,
65}
66
67/// Scalar types supported by [`fused_elementwise_into`].
68pub trait FusedScalar: Copy + MaybeSendSync + 'static {
69    fn fused_add(self, rhs: Self) -> Self;
70    fn fused_multiply(self, rhs: Self) -> Self;
71    fn fused_negate(self) -> Self;
72    fn fused_conj(self) -> Self;
73    fn fused_divide(self, rhs: Self) -> Self;
74    fn fused_abs(self) -> Self;
75    fn fused_maximum(self, rhs: Self) -> Self;
76    fn fused_minimum(self, rhs: Self) -> Self;
77    fn fused_clamp(self, min: Self, max: Self) -> Self;
78    fn fused_exp(self) -> Self;
79    fn fused_log(self) -> Self;
80    fn fused_sin(self) -> Self;
81    fn fused_cos(self) -> Self;
82    fn fused_tanh(self) -> Self;
83    fn fused_sqrt(self) -> Self;
84    fn fused_rsqrt(self) -> Self;
85    fn fused_pow(self, rhs: Self) -> Self;
86    fn fused_expm1(self) -> Self;
87    fn fused_log1p(self) -> Self;
88}
89
90macro_rules! impl_real_fused_scalar {
91    ($ty:ty) => {
92        impl FusedScalar for $ty {
93            #[inline(always)]
94            fn fused_add(self, rhs: Self) -> Self {
95                self + rhs
96            }
97
98            #[inline(always)]
99            fn fused_multiply(self, rhs: Self) -> Self {
100                self * rhs
101            }
102
103            #[inline(always)]
104            fn fused_negate(self) -> Self {
105                -self
106            }
107
108            #[inline(always)]
109            fn fused_conj(self) -> Self {
110                self
111            }
112
113            #[inline(always)]
114            fn fused_divide(self, rhs: Self) -> Self {
115                self / rhs
116            }
117
118            #[inline(always)]
119            fn fused_abs(self) -> Self {
120                self.abs()
121            }
122
123            #[inline(always)]
124            fn fused_maximum(self, rhs: Self) -> Self {
125                self.max(rhs)
126            }
127
128            #[inline(always)]
129            fn fused_minimum(self, rhs: Self) -> Self {
130                self.min(rhs)
131            }
132
133            #[inline(always)]
134            fn fused_clamp(self, min: Self, max: Self) -> Self {
135                self.fused_maximum(min).fused_minimum(max)
136            }
137
138            #[inline(always)]
139            fn fused_exp(self) -> Self {
140                self.exp()
141            }
142
143            #[inline(always)]
144            fn fused_log(self) -> Self {
145                self.ln()
146            }
147
148            #[inline(always)]
149            fn fused_sin(self) -> Self {
150                self.sin()
151            }
152
153            #[inline(always)]
154            fn fused_cos(self) -> Self {
155                self.cos()
156            }
157
158            #[inline(always)]
159            fn fused_tanh(self) -> Self {
160                self.tanh()
161            }
162
163            #[inline(always)]
164            fn fused_sqrt(self) -> Self {
165                self.sqrt()
166            }
167
168            #[inline(always)]
169            fn fused_rsqrt(self) -> Self {
170                1.0 / self.sqrt()
171            }
172
173            #[inline(always)]
174            fn fused_pow(self, rhs: Self) -> Self {
175                self.powf(rhs)
176            }
177
178            #[inline(always)]
179            fn fused_expm1(self) -> Self {
180                self.exp_m1()
181            }
182
183            #[inline(always)]
184            fn fused_log1p(self) -> Self {
185                self.ln_1p()
186            }
187        }
188    };
189}
190
191macro_rules! impl_complex_fused_scalar {
192    ($ty:ty) => {
193        impl FusedScalar for $ty {
194            #[inline(always)]
195            fn fused_add(self, rhs: Self) -> Self {
196                self + rhs
197            }
198
199            #[inline(always)]
200            fn fused_multiply(self, rhs: Self) -> Self {
201                self * rhs
202            }
203
204            #[inline(always)]
205            fn fused_negate(self) -> Self {
206                -self
207            }
208
209            #[inline(always)]
210            fn fused_conj(self) -> Self {
211                num_complex::Complex::conj(&self)
212            }
213
214            #[inline(always)]
215            fn fused_divide(self, rhs: Self) -> Self {
216                self / rhs
217            }
218
219            #[inline(always)]
220            fn fused_abs(self) -> Self {
221                Self::new(self.norm(), 0.0)
222            }
223
224            #[inline(always)]
225            fn fused_maximum(self, rhs: Self) -> Self {
226                if self.norm_sqr() >= rhs.norm_sqr() {
227                    self
228                } else {
229                    rhs
230                }
231            }
232
233            #[inline(always)]
234            fn fused_minimum(self, rhs: Self) -> Self {
235                if self.norm_sqr() <= rhs.norm_sqr() {
236                    self
237                } else {
238                    rhs
239                }
240            }
241
242            #[inline(always)]
243            fn fused_clamp(self, min: Self, max: Self) -> Self {
244                self.fused_maximum(min).fused_minimum(max)
245            }
246
247            #[inline(always)]
248            fn fused_exp(self) -> Self {
249                self.exp()
250            }
251
252            #[inline(always)]
253            fn fused_log(self) -> Self {
254                self.ln()
255            }
256
257            #[inline(always)]
258            fn fused_sin(self) -> Self {
259                self.sin()
260            }
261
262            #[inline(always)]
263            fn fused_cos(self) -> Self {
264                self.cos()
265            }
266
267            #[inline(always)]
268            fn fused_tanh(self) -> Self {
269                self.tanh()
270            }
271
272            #[inline(always)]
273            fn fused_sqrt(self) -> Self {
274                self.sqrt()
275            }
276
277            #[inline(always)]
278            fn fused_rsqrt(self) -> Self {
279                Self::new(1.0, 0.0) / self.sqrt()
280            }
281
282            #[inline(always)]
283            fn fused_pow(self, rhs: Self) -> Self {
284                self.powc(rhs)
285            }
286
287            #[inline(always)]
288            fn fused_expm1(self) -> Self {
289                self.exp() - Self::new(1.0, 0.0)
290            }
291
292            #[inline(always)]
293            fn fused_log1p(self) -> Self {
294                (self + Self::new(1.0, 0.0)).ln()
295            }
296        }
297    };
298}
299
300impl_real_fused_scalar!(f32);
301impl_real_fused_scalar!(f64);
302impl_complex_fused_scalar!(num_complex::Complex32);
303impl_complex_fused_scalar!(num_complex::Complex64);
304
305#[inline]
306fn op_arity(op: FusedOp) -> usize {
307    match op {
308        FusedOp::Negate
309        | FusedOp::Conj
310        | FusedOp::Abs
311        | FusedOp::Exp
312        | FusedOp::Log
313        | FusedOp::Sin
314        | FusedOp::Cos
315        | FusedOp::Tanh
316        | FusedOp::Sqrt
317        | FusedOp::Rsqrt
318        | FusedOp::Expm1
319        | FusedOp::Log1p => 1,
320        FusedOp::Add
321        | FusedOp::Multiply
322        | FusedOp::Divide
323        | FusedOp::Maximum
324        | FusedOp::Minimum
325        | FusedOp::Pow => 2,
326        FusedOp::Clamp => 3,
327    }
328}
329
330fn validate_plan(plan: &FusedPlan, input_count: usize, output_count: usize) -> Result<()> {
331    if input_count != plan.input_count {
332        return Err(StridedError::RankMismatch(input_count, plan.input_count));
333    }
334    if output_count != plan.outputs.len() {
335        return Err(StridedError::RankMismatch(output_count, plan.outputs.len()));
336    }
337    if output_count == 0 {
338        return Err(StridedError::RankMismatch(0, 1));
339    }
340
341    let mut value_count = plan.input_count;
342    for inst in &plan.ops {
343        let expected_arity = op_arity(inst.op);
344        if inst.inputs.len() != expected_arity {
345            return Err(StridedError::RankMismatch(
346                inst.inputs.len(),
347                expected_arity,
348            ));
349        }
350        for &input in &inst.inputs {
351            if input >= value_count {
352                return Err(StridedError::InvalidAxis {
353                    axis: input,
354                    rank: value_count,
355                });
356            }
357        }
358        value_count += 1;
359    }
360
361    for &output in &plan.outputs {
362        if output >= value_count {
363            return Err(StridedError::InvalidAxis {
364                axis: output,
365                rank: value_count,
366            });
367        }
368    }
369
370    Ok(())
371}
372
373fn validate_shapes<T: FusedScalar>(
374    dests: &[StridedViewMut<'_, T>],
375    inputs: &[StridedView<'_, T>],
376) -> Result<()> {
377    let dims = dests[0].dims();
378    for dest in dests {
379        validate_destination_layout(dest)?;
380    }
381    for dest in &dests[1..] {
382        ensure_same_shape(dims, dest.dims())?;
383    }
384    for input in inputs {
385        ensure_same_shape(dims, input.dims())?;
386    }
387    Ok(())
388}
389
390fn validate_destination_layout<T>(dest: &StridedViewMut<'_, T>) -> Result<()> {
391    if is_injective_layout(dest.dims(), dest.strides()) {
392        Ok(())
393    } else {
394        Err(StridedError::NonInjectiveOutputLayout)
395    }
396}
397
398pub(crate) fn is_injective_layout(dims: &[usize], strides: &[isize]) -> bool {
399    if dims.len() != strides.len() {
400        return false;
401    }
402
403    let Some(total) = dims
404        .iter()
405        .try_fold(1usize, |acc, &dim| acc.checked_mul(dim))
406    else {
407        return false;
408    };
409    if total <= 1 {
410        return true;
411    }
412    if dims
413        .iter()
414        .zip(strides.iter())
415        .any(|(&dim, &stride)| dim > 1 && stride == 0)
416    {
417        return false;
418    }
419
420    const EXACT_CHECK_LIMIT: usize = 4096;
421    if total <= EXACT_CHECK_LIMIT {
422        return has_unique_offsets_exact(dims, strides, total);
423    }
424
425    has_disjoint_stride_spans(dims, strides)
426}
427
428fn has_unique_offsets_exact(dims: &[usize], strides: &[isize], total: usize) -> bool {
429    let mut seen = std::collections::HashSet::with_capacity(total);
430    let mut indices = vec![0usize; dims.len()];
431    let mut offset = 0isize;
432
433    for _ in 0..total {
434        if !seen.insert(offset) {
435            return false;
436        }
437
438        for axis in 0..dims.len() {
439            indices[axis] += 1;
440            offset = match offset.checked_add(strides[axis]) {
441                Some(offset) => offset,
442                None => return false,
443            };
444            if indices[axis] < dims[axis] {
445                break;
446            }
447
448            let rewind = match strides[axis].checked_mul(indices[axis] as isize) {
449                Some(rewind) => rewind,
450                None => return false,
451            };
452            offset = match offset.checked_sub(rewind) {
453                Some(offset) => offset,
454                None => return false,
455            };
456            indices[axis] = 0;
457        }
458    }
459
460    true
461}
462
463fn has_disjoint_stride_spans(dims: &[usize], strides: &[isize]) -> bool {
464    let mut axes = Vec::with_capacity(dims.len());
465    for (&dim, &stride) in dims.iter().zip(strides.iter()) {
466        if dim <= 1 {
467            continue;
468        }
469        let stride_abs = match stride.checked_abs() {
470            Some(stride_abs) => stride_abs as u128,
471            None => return false,
472        };
473        axes.push((stride_abs, dim as u128 - 1));
474    }
475    axes.sort_unstable_by_key(|&(stride, _)| stride);
476
477    let mut covered_span = 0u128;
478    for (stride, extent) in axes {
479        if stride <= covered_span {
480            return false;
481        }
482        covered_span = match stride
483            .checked_mul(extent)
484            .and_then(|span| covered_span.checked_add(span))
485        {
486            Some(covered_span) => covered_span,
487            None => return false,
488        };
489    }
490
491    true
492}
493
494#[inline(always)]
495fn eval_op<T: FusedScalar>(op: FusedOp, regs: &[T], inputs: &[usize]) -> T {
496    match op {
497        FusedOp::Negate
498        | FusedOp::Conj
499        | FusedOp::Abs
500        | FusedOp::Exp
501        | FusedOp::Log
502        | FusedOp::Sin
503        | FusedOp::Cos
504        | FusedOp::Tanh
505        | FusedOp::Sqrt
506        | FusedOp::Rsqrt
507        | FusedOp::Expm1
508        | FusedOp::Log1p => eval_unary(op, regs[inputs[0]]),
509        FusedOp::Add
510        | FusedOp::Multiply
511        | FusedOp::Divide
512        | FusedOp::Maximum
513        | FusedOp::Minimum
514        | FusedOp::Pow => eval_binary(op, regs[inputs[0]], regs[inputs[1]]),
515        FusedOp::Clamp => eval_ternary(op, regs[inputs[0]], regs[inputs[1]], regs[inputs[2]]),
516    }
517}
518
519#[inline(always)]
520fn eval_unary<T: FusedScalar>(op: FusedOp, x: T) -> T {
521    match op {
522        FusedOp::Negate => x.fused_negate(),
523        FusedOp::Conj => x.fused_conj(),
524        FusedOp::Abs => x.fused_abs(),
525        FusedOp::Exp => x.fused_exp(),
526        FusedOp::Log => x.fused_log(),
527        FusedOp::Sin => x.fused_sin(),
528        FusedOp::Cos => x.fused_cos(),
529        FusedOp::Tanh => x.fused_tanh(),
530        FusedOp::Sqrt => x.fused_sqrt(),
531        FusedOp::Rsqrt => x.fused_rsqrt(),
532        FusedOp::Expm1 => x.fused_expm1(),
533        FusedOp::Log1p => x.fused_log1p(),
534        _ => unreachable!("not a unary fused op: {op:?}"),
535    }
536}
537
538#[inline(always)]
539fn eval_binary<T: FusedScalar>(op: FusedOp, a: T, b: T) -> T {
540    match op {
541        FusedOp::Add => a.fused_add(b),
542        FusedOp::Multiply => a.fused_multiply(b),
543        FusedOp::Divide => a.fused_divide(b),
544        FusedOp::Maximum => a.fused_maximum(b),
545        FusedOp::Minimum => a.fused_minimum(b),
546        FusedOp::Pow => a.fused_pow(b),
547        _ => unreachable!("not a binary fused op: {op:?}"),
548    }
549}
550
551#[inline(always)]
552fn eval_ternary<T: FusedScalar>(op: FusedOp, a: T, b: T, c: T) -> T {
553    match op {
554        FusedOp::Clamp => a.fused_clamp(b, c),
555        _ => unreachable!("not a ternary fused op: {op:?}"),
556    }
557}
558
559fn try_static_specialization<T: FusedScalar>(
560    dests: &mut [StridedViewMut<'_, T>],
561    inputs: &[StridedView<'_, T>],
562    plan: &FusedPlan,
563) -> Result<bool> {
564    if dests.len() != 1 || plan.outputs.len() != 1 {
565        return Ok(false);
566    }
567
568    if let [inst] = plan.ops.as_slice() {
569        let output_id = plan.input_count;
570        if plan.outputs[0] != output_id {
571            return Ok(false);
572        }
573
574        match op_arity(inst.op) {
575            1 => {
576                map_into(&mut dests[0], &inputs[inst.inputs[0]], |x| {
577                    eval_unary(inst.op, x)
578                })?;
579                return Ok(true);
580            }
581            2 => {
582                zip_map2_into(
583                    &mut dests[0],
584                    &inputs[inst.inputs[0]],
585                    &inputs[inst.inputs[1]],
586                    |a, b| eval_binary(inst.op, a, b),
587                )?;
588                return Ok(true);
589            }
590            3 => {
591                zip_map3_into(
592                    &mut dests[0],
593                    &inputs[inst.inputs[0]],
594                    &inputs[inst.inputs[1]],
595                    &inputs[inst.inputs[2]],
596                    |a, b, c| eval_ternary(inst.op, a, b, c),
597                )?;
598                return Ok(true);
599            }
600            _ => unreachable!("unsupported fused op arity"),
601        }
602    }
603
604    if plan.input_count == 2
605        && plan.outputs.as_slice() == [3]
606        && plan.ops.len() == 2
607        && plan.ops[0].op == FusedOp::Add
608        && plan.ops[0].inputs.as_slice() == [0, 1]
609        && plan.ops[1].op == FusedOp::Multiply
610    {
611        match plan.ops[1].inputs.as_slice() {
612            [2, 0] => {
613                zip_map2_into(&mut dests[0], &inputs[0], &inputs[1], |a, b| {
614                    a.fused_add(b).fused_multiply(a)
615                })?;
616                return Ok(true);
617            }
618            [0, 2] => {
619                zip_map2_into(&mut dests[0], &inputs[0], &inputs[1], |a, b| {
620                    a.fused_multiply(a.fused_add(b))
621                })?;
622                return Ok(true);
623            }
624            _ => {}
625        }
626    }
627
628    if plan.input_count == 3
629        && plan.outputs.as_slice() == [5]
630        && plan.ops.len() == 3
631        && plan.ops[0].op == FusedOp::Multiply
632        && plan.ops[0].inputs.as_slice() == [0, 1]
633        && plan.ops[1].op == FusedOp::Add
634        && plan.ops[1].inputs.as_slice() == [3, 2]
635        && plan.ops[2].op == FusedOp::Exp
636        && plan.ops[2].inputs.as_slice() == [4]
637    {
638        zip_map3_into(
639            &mut dests[0],
640            &inputs[0],
641            &inputs[1],
642            &inputs[2],
643            |a, b, c| a.fused_multiply(b).fused_add(c).fused_exp(),
644        )?;
645        return Ok(true);
646    }
647
648    if plan.input_count == 4
649        && plan.outputs.as_slice() == [8]
650        && plan.ops.len() == 5
651        && plan.ops[0].op == FusedOp::Divide
652        && plan.ops[0].inputs.as_slice() == [0, 1]
653        && plan.ops[1].op == FusedOp::Maximum
654        && plan.ops[1].inputs.as_slice() == [4, 2]
655        && plan.ops[2].op == FusedOp::Minimum
656        && plan.ops[2].inputs.as_slice() == [5, 3]
657        && plan.ops[3].op == FusedOp::Sqrt
658        && plan.ops[3].inputs.as_slice() == [6]
659        && plan.ops[4].op == FusedOp::Rsqrt
660        && plan.ops[4].inputs.as_slice() == [7]
661    {
662        zip_map4_into(
663            &mut dests[0],
664            &inputs[0],
665            &inputs[1],
666            &inputs[2],
667            &inputs[3],
668            |a, b, lo, hi| {
669                a.fused_divide(b)
670                    .fused_maximum(lo)
671                    .fused_minimum(hi)
672                    .fused_sqrt()
673                    .fused_rsqrt()
674            },
675        )?;
676        return Ok(true);
677    }
678
679    Ok(false)
680}
681
682unsafe fn interpret_inner_loop<T: FusedScalar>(
683    dst_ptrs: &[*mut T],
684    input_ptrs: &[*const T],
685    plan: &FusedPlan,
686    offsets: &[isize],
687    len: usize,
688    strides: &[isize],
689) {
690    let output_count = dst_ptrs.len();
691    let mut regs = Vec::with_capacity(plan.input_count + plan.ops.len());
692
693    for i in 0..len {
694        let i = i as isize;
695        regs.clear();
696
697        for (input_index, &input_ptr) in input_ptrs.iter().enumerate() {
698            let stride_index = output_count + input_index;
699            regs.push(*input_ptr.offset(offsets[stride_index] + i * strides[stride_index]));
700        }
701
702        for inst in &plan.ops {
703            regs.push(eval_op(inst.op, &regs, &inst.inputs));
704        }
705
706        for (output_index, &dst_ptr) in dst_ptrs.iter().enumerate() {
707            *dst_ptr.offset(offsets[output_index] + i * strides[output_index]) =
708                regs[plan.outputs[output_index]];
709        }
710    }
711}
712
713fn interpret_fused_elementwise_into<T: FusedScalar>(
714    dests: &mut [StridedViewMut<'_, T>],
715    inputs: &[StridedView<'_, T>],
716    plan: &FusedPlan,
717) -> Result<()> {
718    let dims = dests[0].dims().to_vec();
719    if total_len(&dims) == 0 {
720        return Ok(());
721    }
722
723    let dst_ptrs: Vec<*mut T> = dests.iter_mut().map(|dest| dest.as_mut_ptr()).collect();
724    let input_ptrs: Vec<*const T> = inputs.iter().map(StridedView::ptr).collect();
725
726    let mut strides_list: Vec<&[isize]> = Vec::with_capacity(dests.len() + inputs.len());
727    for dest in dests.iter() {
728        strides_list.push(dest.strides());
729    }
730    for input in inputs {
731        strides_list.push(input.strides());
732    }
733
734    let elem_size = std::mem::size_of::<T>();
735    let total = total_len(&dims);
736    let (fused_dims, ordered_strides, kernel_plan) = if total <= SMALL_TENSOR_THRESHOLD {
737        build_plan_fused_small(&dims, &strides_list)
738    } else {
739        build_plan_fused(&dims, &strides_list, Some(0), elem_size)
740    };
741
742    #[cfg(feature = "parallel")]
743    {
744        let total: usize = fused_dims.iter().product();
745        if total > MINTHREADLENGTH && rayon::current_num_threads() > 1 {
746            let dst_send: Vec<SendPtr<T>> = dst_ptrs.iter().map(|&ptr| SendPtr(ptr)).collect();
747            let input_send: Vec<SendPtr<T>> = input_ptrs
748                .iter()
749                .map(|&ptr| SendPtr(ptr as *mut T))
750                .collect();
751
752            let costs = compute_costs(&ordered_strides);
753            let initial_offsets = vec![0isize; ordered_strides.len()];
754            let nthreads = rayon::current_num_threads();
755
756            return mapreduce_threaded(
757                &fused_dims,
758                &kernel_plan.block,
759                &ordered_strides,
760                &initial_offsets,
761                &costs,
762                nthreads,
763                0,
764                1,
765                &|dims, blocks, strides_list, offsets| {
766                    let dst_ptrs: Vec<*mut T> = dst_send.iter().map(|ptr| ptr.as_ptr()).collect();
767                    let input_ptrs: Vec<*const T> =
768                        input_send.iter().map(|ptr| ptr.as_const()).collect();
769                    for_each_inner_block_with_offsets(
770                        dims,
771                        blocks,
772                        strides_list,
773                        offsets,
774                        |offsets, len, strides| {
775                            unsafe {
776                                interpret_inner_loop(
777                                    &dst_ptrs,
778                                    &input_ptrs,
779                                    plan,
780                                    offsets,
781                                    len,
782                                    strides,
783                                );
784                            }
785                            Ok(())
786                        },
787                    )
788                },
789            );
790        }
791    }
792
793    let initial_offsets = vec![0isize; ordered_strides.len()];
794    for_each_inner_block_preordered(
795        &fused_dims,
796        &kernel_plan.block,
797        &ordered_strides,
798        &initial_offsets,
799        |offsets, len, strides| {
800            unsafe {
801                interpret_inner_loop(&dst_ptrs, &input_ptrs, plan, offsets, len, strides);
802            }
803            Ok(())
804        },
805    )
806}
807
808/// Evaluate a runtime-DAG elementwise plan into one or more destinations.
809///
810/// The plan is validated before any destination is written:
811///
812/// - `inputs.len()` must equal `plan.input_count`;
813/// - `dests.len()` must equal `plan.outputs.len()`;
814/// - instruction operands must reference earlier SSA values with the right
815///   arity for their [`FusedOp`];
816/// - every input and destination must have exactly the destination shape;
817/// - each mutable destination layout must be injective, so two logical output
818///   elements never map to the same memory address.
819///
820/// The implementation dispatches known single-output plans to existing static
821/// `map_into`/`zip_map*_into` kernels and uses a generic interpreter fallback
822/// for arbitrary validated DAGs. Overlapping source/destination memory is not
823/// supported by the strided kernels generally.
824///
825/// Real `Maximum`, `Minimum`, and `Clamp` use Rust `f32`/`f64` `max`/`min`
826/// semantics. Complex `Abs` returns the norm in the real component; complex
827/// `Maximum`, `Minimum`, and `Clamp` compare by squared norm.
828pub fn fused_elementwise_into<T: FusedScalar>(
829    dests: &mut [StridedViewMut<'_, T>],
830    inputs: &[StridedView<'_, T>],
831    plan: &FusedPlan,
832) -> Result<()> {
833    validate_plan(plan, inputs.len(), dests.len())?;
834    validate_shapes(dests, inputs)?;
835    if try_static_specialization(dests, inputs, plan)? {
836        return Ok(());
837    }
838    interpret_fused_elementwise_into(dests, inputs, plan)
839}
840
841#[cfg(test)]
842mod tests {
843    use super::*;
844    use crate::StridedArray;
845
846    fn input(values: &[f64]) -> StridedArray<f64> {
847        StridedArray::from_parts(values.to_vec(), &[values.len()], &[1], 0).unwrap()
848    }
849
850    fn run_static(plan: &FusedPlan, arrays: &[StridedArray<f64>]) -> (bool, Vec<f64>) {
851        let inputs: Vec<_> = arrays.iter().map(|array| array.view()).collect();
852        let mut out = StridedArray::<f64>::col_major(arrays[0].dims());
853        let used_static = {
854            let mut dests = [out.view_mut()];
855            try_static_specialization(&mut dests, &inputs, plan).unwrap()
856        };
857        (used_static, out.iter().copied().collect())
858    }
859
860    fn run_interpreter(plan: &FusedPlan, arrays: &[StridedArray<f64>]) -> Vec<f64> {
861        let inputs: Vec<_> = arrays.iter().map(|array| array.view()).collect();
862        let mut out = StridedArray::<f64>::col_major(arrays[0].dims());
863        {
864            let mut dests = [out.view_mut()];
865            interpret_fused_elementwise_into(&mut dests, &inputs, plan).unwrap();
866        }
867        out.iter().copied().collect()
868    }
869
870    fn assert_static_matches_interpreter(plan: FusedPlan, arrays: &[StridedArray<f64>]) {
871        let (used_static, static_values) = run_static(&plan, arrays);
872        let interpreter_values = run_interpreter(&plan, arrays);
873
874        assert!(used_static, "plan should use static specialization");
875        assert_eq!(static_values.len(), interpreter_values.len());
876        for (actual, expected) in static_values.iter().zip(interpreter_values.iter()) {
877            assert!((actual - expected).abs() < 1e-12);
878        }
879    }
880
881    // Single-instruction plan whose sole output is the instruction result. Such
882    // plans always hit `try_static_specialization`, and both the static and the
883    // interpreter path dispatch through the scalar `FusedScalar` methods, so
884    // iterating every op exercises each scalar implementation.
885    fn single_op(input_count: usize, op: FusedOp, inputs: Vec<usize>) -> FusedPlan {
886        FusedPlan {
887            input_count,
888            outputs: vec![input_count],
889            ops: vec![FusedInst { op, inputs }],
890        }
891    }
892
893    #[test]
894    fn specializes_unary_exp() {
895        let a = input(&[1.0, 2.0, 3.0]);
896        let plan = FusedPlan {
897            input_count: 1,
898            outputs: vec![1],
899            ops: vec![FusedInst {
900                op: FusedOp::Exp,
901                inputs: vec![0],
902            }],
903        };
904
905        assert_static_matches_interpreter(plan, &[a]);
906    }
907
908    #[test]
909    fn specializes_binary_add() {
910        let a = input(&[1.0, 2.0, 3.0]);
911        let b = input(&[10.0, 20.0, 30.0]);
912        let plan = FusedPlan {
913            input_count: 2,
914            outputs: vec![2],
915            ops: vec![FusedInst {
916                op: FusedOp::Add,
917                inputs: vec![0, 1],
918            }],
919        };
920
921        assert_static_matches_interpreter(plan, &[a, b]);
922    }
923
924    #[test]
925    fn specializes_ternary_clamp() {
926        let x = input(&[1.0, 2.0, 3.0]);
927        let lo = input(&[1.5, 1.5, 1.5]);
928        let hi = input(&[2.5, 2.5, 2.5]);
929        let plan = FusedPlan {
930            input_count: 3,
931            outputs: vec![3],
932            ops: vec![FusedInst {
933                op: FusedOp::Clamp,
934                inputs: vec![0, 1, 2],
935            }],
936        };
937
938        assert_static_matches_interpreter(plan, &[x, lo, hi]);
939    }
940
941    #[test]
942    fn specializes_add_then_multiply_reusing_input() {
943        let a = input(&[1.0, 2.0, 3.0]);
944        let b = input(&[10.0, 20.0, 30.0]);
945        let plan = FusedPlan {
946            input_count: 2,
947            outputs: vec![3],
948            ops: vec![
949                FusedInst {
950                    op: FusedOp::Add,
951                    inputs: vec![0, 1],
952                },
953                FusedInst {
954                    op: FusedOp::Multiply,
955                    inputs: vec![2, 0],
956                },
957            ],
958        };
959
960        assert_static_matches_interpreter(plan, &[a, b]);
961    }
962
963    #[test]
964    fn specializes_exp_of_multiply_add_chain() {
965        let a = input(&[1.0, 2.0, 3.0]);
966        let b = input(&[0.5, 1.5, 2.5]);
967        let c = input(&[2.0, 2.0, 2.0]);
968        let plan = FusedPlan {
969            input_count: 3,
970            outputs: vec![5],
971            ops: vec![
972                FusedInst {
973                    op: FusedOp::Multiply,
974                    inputs: vec![0, 1],
975                },
976                FusedInst {
977                    op: FusedOp::Add,
978                    inputs: vec![3, 2],
979                },
980                FusedInst {
981                    op: FusedOp::Exp,
982                    inputs: vec![4],
983                },
984            ],
985        };
986
987        assert_static_matches_interpreter(plan, &[a, b, c]);
988    }
989
990    #[test]
991    fn specializes_divide_clamp_sqrt_rsqrt_chain() {
992        let a = input(&[4.0, 9.0, 16.0]);
993        let b = input(&[2.0, 3.0, 4.0]);
994        let lo = input(&[1.5, 1.5, 1.5]);
995        let hi = input(&[8.0, 8.0, 8.0]);
996        let plan = FusedPlan {
997            input_count: 4,
998            outputs: vec![8],
999            ops: vec![
1000                FusedInst {
1001                    op: FusedOp::Divide,
1002                    inputs: vec![0, 1],
1003                },
1004                FusedInst {
1005                    op: FusedOp::Maximum,
1006                    inputs: vec![4, 2],
1007                },
1008                FusedInst {
1009                    op: FusedOp::Minimum,
1010                    inputs: vec![5, 3],
1011                },
1012                FusedInst {
1013                    op: FusedOp::Sqrt,
1014                    inputs: vec![6],
1015                },
1016                FusedInst {
1017                    op: FusedOp::Rsqrt,
1018                    inputs: vec![7],
1019                },
1020            ],
1021        };
1022
1023        assert_static_matches_interpreter(plan, &[a, b, lo, hi]);
1024    }
1025
1026    // Real negate/conj/abs were the only real scalar ops not reached by the
1027    // chains above; cover them so the real `FusedScalar` impl is fully exercised.
1028    #[test]
1029    fn specializes_real_negate_conj_abs() {
1030        for op in [FusedOp::Negate, FusedOp::Conj, FusedOp::Abs] {
1031            let x = input(&[-1.5, 2.0, -3.5]);
1032            assert_static_matches_interpreter(single_op(1, op, vec![0]), &[x]);
1033        }
1034    }
1035
1036    // The complex `FusedScalar` impl had no coverage at all (every existing test
1037    // used f64). Run every op over Complex64 so both the static and interpreter
1038    // paths dispatch through the complex scalar methods.
1039    #[test]
1040    fn specializes_every_op_over_complex() {
1041        use num_complex::Complex64;
1042
1043        let c = |re: f64, im: f64| Complex64::new(re, im);
1044        let cinput = |values: &[Complex64]| {
1045            StridedArray::from_parts(values.to_vec(), &[values.len()], &[1], 0).unwrap()
1046        };
1047        let assert_complex_match = |plan: FusedPlan, arrays: &[StridedArray<Complex64>]| {
1048            let inputs: Vec<_> = arrays.iter().map(|array| array.view()).collect();
1049            let mut static_out = StridedArray::<Complex64>::col_major(arrays[0].dims());
1050            let used_static = {
1051                let mut dests = [static_out.view_mut()];
1052                try_static_specialization(&mut dests, &inputs, &plan).unwrap()
1053            };
1054            let mut interp_out = StridedArray::<Complex64>::col_major(arrays[0].dims());
1055            {
1056                let mut dests = [interp_out.view_mut()];
1057                interpret_fused_elementwise_into(&mut dests, &inputs, &plan).unwrap();
1058            }
1059            assert!(used_static, "single-op plan should specialize");
1060            for (actual, expected) in static_out.iter().zip(interp_out.iter()) {
1061                assert!((actual - expected).norm() < 1e-9, "{actual} vs {expected}");
1062            }
1063        };
1064
1065        // Positive-real-part, nonzero operands keep div/log/sqrt/pow well defined.
1066        let a = cinput(&[c(1.5, 0.5), c(2.0, -1.0), c(0.7, 0.3)]);
1067        let b = cinput(&[c(1.1, 0.2), c(0.9, 0.4), c(1.3, -0.6)]);
1068        let d = cinput(&[c(2.0, 0.0), c(2.0, 0.0), c(2.0, 0.0)]);
1069
1070        for op in [
1071            FusedOp::Negate,
1072            FusedOp::Conj,
1073            FusedOp::Abs,
1074            FusedOp::Exp,
1075            FusedOp::Log,
1076            FusedOp::Sin,
1077            FusedOp::Cos,
1078            FusedOp::Tanh,
1079            FusedOp::Sqrt,
1080            FusedOp::Rsqrt,
1081            FusedOp::Expm1,
1082            FusedOp::Log1p,
1083        ] {
1084            assert_complex_match(single_op(1, op, vec![0]), std::slice::from_ref(&a));
1085        }
1086        for op in [
1087            FusedOp::Add,
1088            FusedOp::Multiply,
1089            FusedOp::Divide,
1090            FusedOp::Maximum,
1091            FusedOp::Minimum,
1092            FusedOp::Pow,
1093        ] {
1094            assert_complex_match(single_op(2, op, vec![0, 1]), &[a.clone(), b.clone()]);
1095        }
1096        assert_complex_match(
1097            single_op(3, FusedOp::Clamp, vec![0, 1, 2]),
1098            &[a.clone(), b.clone(), d.clone()],
1099        );
1100    }
1101
1102    // Error branches in plan/layout validation that the positive tests skip.
1103    #[test]
1104    fn validate_plan_rejects_out_of_range_output() {
1105        // output id refers to a value that no instruction produces.
1106        let plan = FusedPlan {
1107            input_count: 1,
1108            outputs: vec![5],
1109            ops: vec![FusedInst {
1110                op: FusedOp::Exp,
1111                inputs: vec![0],
1112            }],
1113        };
1114        assert!(validate_plan(&plan, 1, 1).is_err());
1115    }
1116
1117    #[test]
1118    fn validate_plan_rejects_zero_outputs() {
1119        let plan = FusedPlan {
1120            input_count: 1,
1121            outputs: vec![],
1122            ops: vec![],
1123        };
1124        assert!(validate_plan(&plan, 1, 0).is_err());
1125    }
1126
1127    #[test]
1128    fn is_injective_layout_rejects_rank_and_broadcast_mismatch() {
1129        assert!(!is_injective_layout(&[2, 3], &[1]));
1130        assert!(!is_injective_layout(&[2, 2], &[0, 1]));
1131        assert!(is_injective_layout(&[1], &[0]));
1132    }
1133}