1use 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#[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#[derive(Clone, Debug, Eq, PartialEq)]
43pub struct FusedInst {
44 pub op: FusedOp,
45 pub inputs: Vec<usize>,
46}
47
48#[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
67pub 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, ®s, &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
808pub 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 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 #[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 #[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 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 #[test]
1104 fn validate_plan_rejects_out_of_range_output() {
1105 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}