1use 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#[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#[derive(Clone, Debug, Eq, PartialEq)]
75pub struct FusedInst {
76 pub op: FusedOp,
77 pub inputs: Vec<usize>,
78}
79
80#[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
99pub 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, ®s, &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, ®s, &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
1462pub 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 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 #[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 #[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 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 #[test]
2037 fn validate_plan_rejects_out_of_range_output() {
2038 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}