1use std::cell::RefCell;
2use std::collections::VecDeque;
3
4use candle_core::{DType, Device, Result, Tensor};
5
6use crate::backend::execute_tensor_permute_and_compose;
7
8#[doc(hidden)]
10#[derive(Clone, Copy, Debug)]
11pub struct UnaryEinsumSpec<'a> {
12 input_rank: usize,
13 output_rank: usize,
14 permutation: &'a [usize],
15}
16
17impl<'a> UnaryEinsumSpec<'a> {
18 #[doc(hidden)]
20 pub const fn new(input_rank: usize, output_rank: usize, permutation: &'a [usize]) -> Self {
21 Self {
22 input_rank,
23 output_rank,
24 permutation,
25 }
26 }
27}
28
29#[doc(hidden)]
31pub fn execute_unary_einsum<T>(operand: &T, spec: UnaryEinsumSpec<'_>) -> Result<Tensor>
32where
33 T: AsRef<Tensor> + ?Sized,
34{
35 let operand = operand.as_ref();
36 if operand.rank() != spec.input_rank {
37 candle_core::bail!(
38 "einsum operand 0 has rank {}, expected {} for the equation input",
39 operand.rank(),
40 spec.input_rank,
41 )
42 }
43 if spec.output_rank > spec.input_rank {
44 candle_core::bail!(
45 "invalid unary einsum plan: output rank {} exceeds input rank {}",
46 spec.output_rank,
47 spec.input_rank,
48 )
49 }
50 if spec.permutation.len() != spec.input_rank {
51 candle_core::bail!(
52 "invalid unary einsum plan: permutation has {} axes, expected {}",
53 spec.permutation.len(),
54 spec.input_rank,
55 )
56 }
57
58 let mut seen = vec![false; spec.input_rank];
59 for &axis in spec.permutation {
60 if axis >= spec.input_rank {
61 candle_core::bail!(
62 "invalid unary einsum plan: permutation axis {axis} is out of range for rank {}",
63 spec.input_rank,
64 )
65 }
66 if seen[axis] {
67 candle_core::bail!(
68 "invalid unary einsum plan: permutation contains axis {axis} more than once"
69 )
70 }
71 seen[axis] = true;
72 }
73
74 let is_identity = spec.permutation.iter().copied().eq(0..spec.input_rank);
75 let mut output = if is_identity {
76 operand.clone()
77 } else {
78 operand
79 .permute(spec.permutation)
80 .map_err(|error| error.context("einsum unary permutation"))?
81 };
82
83 if spec.output_rank < spec.input_rank {
84 let reduction_axes = (spec.output_rank..spec.input_rank).collect::<Vec<_>>();
85 output = output
86 .sum(reduction_axes.as_slice())
87 .map_err(|error| error.context("einsum unary reduction"))?;
88 }
89
90 Ok(output)
91}
92
93#[doc(hidden)]
95#[derive(Clone, Copy, Debug)]
96pub struct BinaryEinsumSpec<'a> {
97 input_ranks: [usize; 2],
98 reduction_axes: [&'a [usize]; 2],
99 permutations: [&'a [usize]; 2],
100 batch_rank: usize,
101 left_free_rank: usize,
102 contracted_rank: usize,
103 right_free_rank: usize,
104 batch_labels: &'a [&'a str],
105 contracted_labels: &'a [&'a str],
106 output_permutation: &'a [usize],
107}
108
109#[doc(hidden)]
111#[derive(Clone, Copy, Debug)]
112pub struct EinsumAxisPattern<'a> {
113 labels: &'a [&'a str],
114 ellipsis_position: Option<usize>,
115}
116
117impl<'a> EinsumAxisPattern<'a> {
118 #[doc(hidden)]
120 pub const fn new(labels: &'a [&'a str], ellipsis_position: Option<usize>) -> Self {
121 Self {
122 labels,
123 ellipsis_position,
124 }
125 }
126}
127
128#[doc(hidden)]
131#[derive(Clone, Copy, Debug)]
132pub struct EllipsisEinsumSpec<'a> {
133 operands: &'a [EinsumAxisPattern<'a>],
134 output: EinsumAxisPattern<'a>,
135}
136
137impl<'a> EllipsisEinsumSpec<'a> {
138 #[doc(hidden)]
140 pub const fn new(operands: &'a [EinsumAxisPattern<'a>], output: EinsumAxisPattern<'a>) -> Self {
141 Self { operands, output }
142 }
143}
144
145impl<'a> BinaryEinsumSpec<'a> {
146 #[doc(hidden)]
148 #[allow(clippy::too_many_arguments)]
149 pub const fn new(
150 input_ranks: [usize; 2],
151 reduction_axes: [&'a [usize]; 2],
152 permutations: [&'a [usize]; 2],
153 batch_rank: usize,
154 left_free_rank: usize,
155 contracted_rank: usize,
156 right_free_rank: usize,
157 batch_labels: &'a [&'a str],
158 contracted_labels: &'a [&'a str],
159 output_permutation: &'a [usize],
160 ) -> Self {
161 Self {
162 input_ranks,
163 reduction_axes,
164 permutations,
165 batch_rank,
166 left_free_rank,
167 contracted_rank,
168 right_free_rank,
169 batch_labels,
170 contracted_labels,
171 output_permutation,
172 }
173 }
174}
175
176#[derive(Clone, Copy, Debug, Eq, PartialEq)]
177enum BinaryExecution {
178 Multiply,
179 CanonicalMatmul,
180 General,
181}
182
183#[doc(hidden)]
185pub fn execute_binary_einsum<L, R>(
186 left: &L,
187 right: &R,
188 spec: BinaryEinsumSpec<'_>,
189) -> Result<Tensor>
190where
191 L: AsRef<Tensor> + ?Sized,
192 R: AsRef<Tensor> + ?Sized,
193{
194 execute_binary_with(left, right, spec, BinaryExecution::General)
195}
196
197#[doc(hidden)]
199pub fn execute_binary_multiply<L, R>(
200 left: &L,
201 right: &R,
202 spec: BinaryEinsumSpec<'_>,
203) -> Result<Tensor>
204where
205 L: AsRef<Tensor> + ?Sized,
206 R: AsRef<Tensor> + ?Sized,
207{
208 execute_binary_with(left, right, spec, BinaryExecution::Multiply)
209}
210
211#[doc(hidden)]
213pub fn execute_canonical_binary_einsum<L, R>(
214 left: &L,
215 right: &R,
216 spec: BinaryEinsumSpec<'_>,
217) -> Result<Tensor>
218where
219 L: AsRef<Tensor> + ?Sized,
220 R: AsRef<Tensor> + ?Sized,
221{
222 execute_binary_with(left, right, spec, BinaryExecution::CanonicalMatmul)
223}
224
225fn execute_binary_with<L, R>(
226 left: &L,
227 right: &R,
228 spec: BinaryEinsumSpec<'_>,
229 requested_execution: BinaryExecution,
230) -> Result<Tensor>
231where
232 L: AsRef<Tensor> + ?Sized,
233 R: AsRef<Tensor> + ?Sized,
234{
235 let left = left.as_ref();
236 let right = right.as_ref();
237 for (index, (operand, expected_rank)) in
238 [left, right].into_iter().zip(spec.input_ranks).enumerate()
239 {
240 if operand.rank() != expected_rank {
241 candle_core::bail!(
242 "einsum operand {index} has rank {}, expected {expected_rank} for the equation input",
243 operand.rank(),
244 )
245 }
246 }
247 if left.dtype() != right.dtype() {
248 candle_core::bail!(
249 "einsum operands have different dtypes: left {:?}, right {:?}",
250 left.dtype(),
251 right.dtype(),
252 )
253 }
254 if !left.device().same_device(right.device()) {
255 candle_core::bail!(
256 "einsum operands are on different devices: left {:?}, right {:?}",
257 left.device(),
258 right.device(),
259 )
260 }
261
262 let left = prepare_operand(
263 left,
264 0,
265 spec.input_ranks[0],
266 spec.reduction_axes[0],
267 spec.permutations[0],
268 )?;
269 let right = prepare_operand(
270 right,
271 1,
272 spec.input_ranks[1],
273 spec.reduction_axes[1],
274 spec.permutations[1],
275 )?;
276
277 let left_expected_rank = spec
278 .batch_rank
279 .checked_add(spec.left_free_rank)
280 .and_then(|rank| rank.checked_add(spec.contracted_rank))
281 .ok_or_else(|| {
282 candle_core::Error::msg("einsum binary canonical left rank overflows usize")
283 })?;
284 let right_expected_rank = spec
285 .batch_rank
286 .checked_add(spec.contracted_rank)
287 .and_then(|rank| rank.checked_add(spec.right_free_rank))
288 .ok_or_else(|| {
289 candle_core::Error::msg("einsum binary canonical right rank overflows usize")
290 })?;
291 if left.rank() != left_expected_rank || right.rank() != right_expected_rank {
292 candle_core::bail!(
293 "invalid binary einsum plan: canonical ranks are left {}, right {}, expected left {left_expected_rank}, right {right_expected_rank}",
294 left.rank(),
295 right.rank(),
296 )
297 }
298 if spec.batch_labels.len() != spec.batch_rank
299 || spec.contracted_labels.len() != spec.contracted_rank
300 {
301 candle_core::bail!(
302 "invalid binary einsum plan: shared-label metadata does not match canonical ranks"
303 )
304 }
305
306 let left_dims = left.dims().to_vec();
307 let right_dims = right.dims().to_vec();
308 let mut batch_dims = Vec::with_capacity(spec.batch_rank);
309 for axis in 0..spec.batch_rank {
310 batch_dims.push(resolve_extent(
311 spec.batch_labels[axis],
312 left_dims[axis],
313 right_dims[axis],
314 )?);
315 }
316 let left_free_start = spec.batch_rank;
317 let contracted_left_start = left_free_start + spec.left_free_rank;
318 let contracted_right_start = spec.batch_rank;
319 let mut contracted_dims = Vec::with_capacity(spec.contracted_rank);
320 for axis in 0..spec.contracted_rank {
321 contracted_dims.push(resolve_extent(
322 spec.contracted_labels[axis],
323 left_dims[contracted_left_start + axis],
324 right_dims[contracted_right_start + axis],
325 )?);
326 }
327 let left_free_dims = &left_dims[left_free_start..contracted_left_start];
328 let right_free_start = contracted_right_start + spec.contracted_rank;
329 let right_free_dims = &right_dims[right_free_start..];
330
331 let mut left_shape = batch_dims.clone();
332 left_shape.extend_from_slice(left_free_dims);
333 left_shape.extend_from_slice(&contracted_dims);
334 let mut right_shape = batch_dims.clone();
335 right_shape.extend_from_slice(&contracted_dims);
336 right_shape.extend_from_slice(right_free_dims);
337
338 let b = checked_product(&batch_dims, "batch (B)")?;
339 let m = checked_product(left_free_dims, "left-free (M)")?;
340 let k = checked_product(&contracted_dims, "contracted (K)")?;
341 let n = checked_product(right_free_dims, "right-free (N)")?;
342
343 let mut canonical_output_shape = batch_dims.clone();
344 canonical_output_shape.extend_from_slice(left_free_dims);
345 canonical_output_shape.extend_from_slice(right_free_dims);
346 validate_permutation(
347 spec.output_permutation,
348 canonical_output_shape.len(),
349 "output",
350 )?;
351
352 let execution = if spec.contracted_rank == 0 {
353 BinaryExecution::Multiply
354 } else {
355 requested_execution
356 };
357
358 if execution == BinaryExecution::CanonicalMatmul {
359 let canonical = spec.left_free_rank == 1
360 && spec.contracted_rank == 1
361 && spec.right_free_rank == 1
362 && spec.reduction_axes.iter().all(|axes| axes.is_empty())
363 && spec
364 .permutations
365 .iter()
366 .all(|permutation| permutation.iter().copied().eq(0..permutation.len()));
367 if !canonical {
368 candle_core::bail!("invalid binary einsum plan: direct matmul path is not canonical")
369 }
370 }
371
372 if execution != BinaryExecution::Multiply && (b == 0 || m == 0 || k == 0 || n == 0) {
373 let output = graph_preserving_zero(&left, &right, &canonical_output_shape)?;
374 return apply_output_permutation(output, spec.output_permutation);
375 }
376
377 if execution == BinaryExecution::Multiply {
378 if spec.contracted_rank != 0 {
379 candle_core::bail!(
380 "invalid binary einsum plan: multiply fast path received contracted axes"
381 )
382 }
383 let mut left = left;
384 for _ in 0..spec.right_free_rank {
385 left = left.unsqueeze(left.rank()).map_err(|error| {
386 error.context("einsum binary multiply left free-axis alignment")
387 })?;
388 }
389 let mut right = right;
390 for _ in 0..spec.left_free_rank {
391 right = right.unsqueeze(spec.batch_rank).map_err(|error| {
392 error.context("einsum binary multiply right free-axis alignment")
393 })?;
394 }
395 let output = left
396 .broadcast_mul(&right)
397 .map_err(|error| error.context("einsum binary broadcast multiplication"))?;
398 return apply_output_permutation(output, spec.output_permutation);
399 }
400
401 if execution == BinaryExecution::CanonicalMatmul {
402 let left =
403 materialize_broadcast_if_needed(&left, &left_shape, "einsum binary left broadcast")?;
404 let right =
405 materialize_broadcast_if_needed(&right, &right_shape, "einsum binary right broadcast")?;
406 let output = left
407 .matmul(&right)
408 .map_err(|error| error.context("einsum binary B/M/K/N matmul"))?;
409 return apply_output_permutation(output, spec.output_permutation);
410 }
411
412 let left = left
413 .broadcast_as(left_shape)
414 .map_err(|error| error.context("einsum binary left broadcast"))?;
415 let left = pack_canonical_operand(
416 &left,
417 &[b, m, k],
418 &[spec.batch_rank, spec.left_free_rank, spec.contracted_rank],
419 "einsum binary left B/M/K reshape",
420 )?;
421 let right = right
422 .broadcast_as(right_shape)
423 .map_err(|error| error.context("einsum binary right broadcast"))?;
424 let right = pack_canonical_operand(
425 &right,
426 &[b, k, n],
427 &[spec.batch_rank, spec.contracted_rank, spec.right_free_rank],
428 "einsum binary right B/K/N reshape",
429 )?;
430 let output = left
431 .matmul(&right)
432 .map_err(|error| error.context("einsum binary B/M/K/N matmul"))?;
433 let output = output
434 .reshape(canonical_output_shape)
435 .map_err(|error| error.context("einsum binary canonical output reshape"))?;
436 apply_output_permutation(output, spec.output_permutation)
437}
438
439fn graph_preserving_zero(left: &Tensor, right: &Tensor, shape: &[usize]) -> Result<Tensor> {
440 let left = left
441 .flatten_all()
442 .map_err(|error| error.context("einsum binary left zero flatten"))?;
443 let right = right
444 .flatten_all()
445 .map_err(|error| error.context("einsum binary right zero flatten"))?;
446 Tensor::cat(&[&left, &right], 0)
447 .and_then(|operands| operands.sum_all())
448 .map_err(|error| error.context("einsum binary zero operand anchor"))?
449 .broadcast_as(shape)
450 .map_err(|error| error.context("einsum binary zero output broadcast"))
451}
452
453#[doc(hidden)]
455pub fn execute_unary_ellipsis_einsum<T>(operand: &T, spec: EllipsisEinsumSpec<'_>) -> Result<Tensor>
456where
457 T: AsRef<Tensor> + ?Sized,
458{
459 if spec.operands.len() != 1 {
460 candle_core::bail!(
461 "invalid ellipsis einsum plan: unary execution received {} operand patterns",
462 spec.operands.len()
463 )
464 }
465 let operand = operand.as_ref();
466 let capture = ellipsis_capture(operand, 0, spec.operands[0])?;
467 let normalized = normalize_ellipsis_operand(operand, spec.operands[0], capture, capture, 0)?;
468 let input_axes = expand_axis_pattern(spec.operands[0], capture, true);
469 let (normalized, input_axes) = normalize_repeated_axes(normalized, input_axes, 0)?;
470 let output_axes = expand_axis_pattern(spec.output, capture, false);
471 validate_expanded_output(&[&input_axes], &output_axes)?;
472 let permutation = output_axes
473 .iter()
474 .chain(input_axes.iter().filter(|axis| !output_axes.contains(axis)))
475 .map(|axis| {
476 input_axes
477 .iter()
478 .position(|candidate| candidate == axis)
479 .expect("validated unary ellipsis axis")
480 })
481 .collect::<Vec<_>>();
482 execute_unary_einsum(
483 &normalized,
484 UnaryEinsumSpec::new(input_axes.len(), output_axes.len(), &permutation),
485 )
486}
487
488#[doc(hidden)]
490pub fn execute_binary_ellipsis_einsum<L, R>(
491 left: &L,
492 right: &R,
493 spec: EllipsisEinsumSpec<'_>,
494) -> Result<Tensor>
495where
496 L: AsRef<Tensor> + ?Sized,
497 R: AsRef<Tensor> + ?Sized,
498{
499 if spec.operands.len() != 2 {
500 candle_core::bail!(
501 "invalid ellipsis einsum plan: binary execution received {} operand patterns",
502 spec.operands.len()
503 )
504 }
505 let left = left.as_ref();
506 let right = right.as_ref();
507 let captures = [
508 ellipsis_capture(left, 0, spec.operands[0])?,
509 ellipsis_capture(right, 1, spec.operands[1])?,
510 ];
511 let maximum_capture = captures[0].max(captures[1]);
512 let left = normalize_ellipsis_operand(left, spec.operands[0], captures[0], maximum_capture, 0)?;
513 let right =
514 normalize_ellipsis_operand(right, spec.operands[1], captures[1], maximum_capture, 1)?;
515 let left_axes = expand_axis_pattern(spec.operands[0], maximum_capture, true);
516 let right_axes = expand_axis_pattern(spec.operands[1], maximum_capture, true);
517 let (left, left_axes) = normalize_repeated_axes(left, left_axes, 0)?;
518 let (right, right_axes) = normalize_repeated_axes(right, right_axes, 1)?;
519 let output_axes = expand_axis_pattern(spec.output, maximum_capture, false);
520 validate_expanded_output(&[&left_axes, &right_axes], &output_axes)?;
521 execute_expanded_binary(&left, &right, &left_axes, &right_axes, &output_axes)
522}
523
524#[doc(hidden)]
527pub fn einsum_operand_ref<T>(operand: &T) -> &Tensor
528where
529 T: AsRef<Tensor> + ?Sized,
530{
531 operand.as_ref()
532}
533
534#[doc(hidden)]
536pub fn execute_nary_einsum(operands: &[&Tensor], spec: EllipsisEinsumSpec<'_>) -> Result<Tensor> {
537 execute_nary_einsum_internal(operands, spec, NaryExecutionStrategy::Selected)
538 .map(|(tensor, _)| tensor)
539}
540
541#[derive(Clone, Copy, Debug, Eq, PartialEq)]
542enum NaryExecutionStrategy {
543 Selected,
544 #[cfg(test)]
545 StreamingGreedy,
546}
547
548#[derive(Clone, Debug, Default, Eq, PartialEq)]
549struct NaryExecutionTrace {
550 used_exact: bool,
551 used_cached_greedy: bool,
552 member_sequence: Vec<(u64, u64)>,
553 final_permutations: usize,
554 intermediates: Vec<NaryIntermediateTrace>,
555}
556
557#[derive(Clone, Debug, Eq, PartialEq)]
558struct NaryIntermediateTrace {
559 canonical: bool,
560 estimated: NaryPairCost,
561 execution_graph: NaryPairCost,
562 output_layout: NaryLayoutEstimate,
563}
564
565fn prepare_nary_einsum<'a>(
566 operands: &[&Tensor],
567 spec: EllipsisEinsumSpec<'a>,
568) -> Result<(Vec<PlannedOperand<'a>>, Vec<ExpandedAxis<'a>>)> {
569 if operands.is_empty() {
570 candle_core::bail!("invalid n-ary einsum plan: at least one operand is required")
571 }
572 if spec.operands.len() != operands.len() {
573 candle_core::bail!(
574 "invalid n-ary einsum plan: received {} tensors but {} operand patterns",
575 operands.len(),
576 spec.operands.len()
577 )
578 }
579 let first = operands[0];
580 for (index, operand) in operands.iter().copied().enumerate().skip(1) {
581 if operand.dtype() != first.dtype() {
582 candle_core::bail!(
583 "einsum operands have different dtypes: operand 0 {:?}, operand {index} {:?}",
584 first.dtype(),
585 operand.dtype()
586 )
587 }
588 if !operand.device().same_device(first.device()) {
589 candle_core::bail!(
590 "einsum operands are on different devices: operand 0 {:?}, operand {index} {:?}",
591 first.device(),
592 operand.device()
593 )
594 }
595 }
596 let captures = operands
597 .iter()
598 .zip(spec.operands)
599 .enumerate()
600 .map(|(index, (operand, pattern))| ellipsis_capture(operand, index, *pattern))
601 .collect::<Result<Vec<_>>>()?;
602 let maximum_capture = captures.iter().copied().max().unwrap_or(0);
603 let mut planned = Vec::with_capacity(operands.len());
604 for (index, ((operand, pattern), capture)) in
605 operands.iter().zip(spec.operands).zip(captures).enumerate()
606 {
607 let normalized =
608 normalize_ellipsis_operand(operand, *pattern, capture, maximum_capture, index)?;
609 let axes = expand_axis_pattern(*pattern, maximum_capture, true);
610 let (tensor, axes) = normalize_repeated_axes(normalized, axes, index)?;
611 planned.push(PlannedOperand {
612 tensor,
613 axes,
614 stable_ordinal: index,
615 members: 1_u64.checked_shl(index as u32).unwrap_or(0),
616 });
617 }
618 let output_axes = expand_axis_pattern(spec.output, maximum_capture, false);
619 let input_axes = planned
620 .iter()
621 .map(|operand| operand.axes.as_slice())
622 .collect::<Vec<_>>();
623 validate_expanded_output(&input_axes, &output_axes)?;
624 validate_nary_broadcasts(&planned)?;
625 Ok((planned, output_axes))
626}
627
628fn select_prepared_nary_plan<'a>(
629 planned: &[PlannedOperand<'a>],
630 output_axes: &[ExpandedAxis<'a>],
631) -> NaryPlannerDecision<'a> {
632 let first = &planned[0].tensor;
633 let metadata = prepared_nary_metadata(planned);
634 select_layout_aware_plan(
635 &metadata,
636 output_axes,
637 first.dtype(),
638 first.device().is_cpu(),
639 )
640}
641
642fn prepared_nary_metadata<'a>(planned: &[PlannedOperand<'a>]) -> Vec<NaryPlannerMetadata<'a>> {
643 planned
644 .iter()
645 .map(|operand| NaryPlannerMetadata {
646 stable_ordinal: operand.stable_ordinal,
647 axes: operand
648 .axes
649 .iter()
650 .copied()
651 .zip(operand.tensor.dims().iter().copied())
652 .collect(),
653 layout: if operand.tensor.is_contiguous() {
654 NaryLayoutEstimate::Contiguous
655 } else {
656 NaryLayoutEstimate::Strided(operand.tensor.stride().to_vec())
657 },
658 members: operand.members,
659 })
660 .collect()
661}
662
663#[cfg(feature = "benchmark-internals")]
665#[doc(hidden)]
666pub fn benchmark_nary_planner_selects_exact(
667 operands: &[&Tensor],
668 spec: EllipsisEinsumSpec<'_>,
669) -> Result<bool> {
670 let (planned, output_axes) = prepare_nary_einsum(operands, spec)?;
671 Ok(matches!(
672 select_prepared_nary_plan(&planned, &output_axes),
673 NaryPlannerDecision::Exact(_)
674 ))
675}
676
677#[cfg(feature = "benchmark-internals")]
679#[doc(hidden)]
680#[derive(Clone, Debug, Eq, PartialEq)]
681pub struct BenchmarkBinaryGraphEstimate {
682 pub work: u128,
683 pub output_elements: u128,
684 pub copy_bytes: u128,
685 pub submissions: u128,
686}
687
688#[cfg(feature = "benchmark-internals")]
690#[doc(hidden)]
691pub fn benchmark_binary_graph_estimate(
692 left_labels: &[&str],
693 left_dims: &[usize],
694 left_strides: &[usize],
695 right_labels: &[&str],
696 right_dims: &[usize],
697 right_strides: &[usize],
698 output_labels: &[&str],
699) -> Result<BenchmarkBinaryGraphEstimate> {
700 let left_axes = left_labels
701 .iter()
702 .copied()
703 .map(ExpandedAxis::Named)
704 .collect::<Vec<_>>();
705 let right_axes = right_labels
706 .iter()
707 .copied()
708 .map(ExpandedAxis::Named)
709 .collect::<Vec<_>>();
710 let output_axes = output_labels
711 .iter()
712 .copied()
713 .map(ExpandedAxis::Named)
714 .collect::<Vec<_>>();
715 let graph = classify_expanded_binary_graph(
716 BinaryGraphOperand {
717 axes: &left_axes,
718 dims: left_dims,
719 layout: BinaryOperandLayout::Strided(left_strides),
720 },
721 BinaryGraphOperand {
722 axes: &right_axes,
723 dims: right_dims,
724 layout: BinaryOperandLayout::Strided(right_strides),
725 },
726 &output_axes,
727 4,
728 )?;
729 Ok(BenchmarkBinaryGraphEstimate {
730 work: graph.work,
731 output_elements: graph.output_elements,
732 copy_bytes: graph.copy_bytes,
733 submissions: graph.submissions,
734 })
735}
736
737fn execute_nary_einsum_internal<'a>(
738 operands: &[&Tensor],
739 spec: EllipsisEinsumSpec<'a>,
740 strategy: NaryExecutionStrategy,
741) -> Result<(Tensor, NaryExecutionTrace)> {
742 let (mut planned, output_axes) = prepare_nary_einsum(operands, spec)?;
743 let global_axis_order = stable_axis_order(&planned);
744
745 let decision = if strategy == NaryExecutionStrategy::Selected {
746 select_prepared_nary_plan(&planned, &output_axes)
747 } else {
748 NaryPlannerDecision::Greedy(NaryGreedyReason::Arity)
749 };
750 let calibrated_greedy = matches!(
751 &decision,
752 NaryPlannerDecision::Greedy(NaryGreedyReason::Calibration)
753 );
754 let greedy_cache_key = calibrated_greedy
755 .then(|| nary_plan_cache_key(&prepared_nary_metadata(&planned), &output_axes));
756 let cached_greedy = greedy_cache_key.as_ref().and_then(cached_nary_sequence);
757 let mut trace = NaryExecutionTrace::default();
758 match decision {
759 NaryPlannerDecision::Exact(plan) => {
760 trace.used_exact = true;
761 for step in plan.steps {
762 let left_index = planned
763 .iter()
764 .position(|operand| operand.members == step.members.0)
765 .expect("exact plan left member set remains live");
766 let right_index = planned
767 .iter()
768 .position(|operand| operand.members == step.members.1)
769 .expect("exact plan right member set remains live");
770 debug_assert!(left_index < right_index);
771 let right = planned.remove(right_index);
772 let left = planned.remove(left_index);
773 let graph = classify_expanded_binary_graph(
774 BinaryGraphOperand {
775 axes: &left.axes,
776 dims: left.tensor.dims(),
777 layout: BinaryOperandLayout::Strided(left.tensor.stride()),
778 },
779 BinaryGraphOperand {
780 axes: &right.axes,
781 dims: right.tensor.dims(),
782 layout: BinaryOperandLayout::Strided(right.tensor.stride()),
783 },
784 &step.output_axes,
785 left.tensor.dtype().size_in_bytes(),
786 )?;
787 let execution_graph = graph.cost();
788 debug_assert_eq!(step.estimate, execution_graph);
789 let (tensor, axes) = execute_expanded_binary_canonical(
790 &left.tensor,
791 &right.tensor,
792 &left.axes,
793 &right.axes,
794 &step.output_axes,
795 )?;
796 debug_assert_eq!(axes, step.output_axes);
797 planned.insert(
798 left_index,
799 PlannedOperand {
800 tensor,
801 axes,
802 stable_ordinal: left.stable_ordinal.min(right.stable_ordinal),
803 members: left.members | right.members,
804 },
805 );
806 trace.member_sequence.push(step.members);
807 trace.intermediates.push(NaryIntermediateTrace {
808 canonical: true,
809 estimated: step.estimate,
810 execution_graph,
811 output_layout: graph.output_layout,
812 });
813 }
814 }
815 NaryPlannerDecision::Greedy(_) => {
816 trace.used_cached_greedy = cached_greedy.is_some();
817 let mut cached_steps = cached_greedy.as_deref().unwrap_or_default().iter();
818 while planned.len() > 1 {
819 let selected = if let Some(&(left_members, right_members)) = cached_steps.next() {
820 let left = planned
821 .iter()
822 .position(|operand| operand.members == left_members)
823 .ok_or_else(|| {
824 candle_core::Error::msg("cached greedy left members are not live")
825 })?;
826 let right = planned
827 .iter()
828 .position(|operand| operand.members == right_members)
829 .ok_or_else(|| {
830 candle_core::Error::msg("cached greedy right members are not live")
831 })?;
832 estimate_pair_with_order(
833 &planned,
834 left,
835 right,
836 &output_axes,
837 &global_axis_order,
838 )?
839 } else {
840 select_nary_pair_with_order(&planned, &output_axes, &global_axis_order)?
841 };
842 let right = planned.remove(selected.right);
843 let left = planned.remove(selected.left);
844 let tensor = execute_expanded_binary(
845 &left.tensor,
846 &right.tensor,
847 &left.axes,
848 &right.axes,
849 &selected.output_axes,
850 )?;
851 trace.member_sequence.push((left.members, right.members));
852 planned.insert(
853 selected.left,
854 PlannedOperand {
855 tensor,
856 axes: selected.output_axes,
857 stable_ordinal: left.stable_ordinal.min(right.stable_ordinal),
858 members: left.members | right.members,
859 },
860 );
861 }
862 if let Some(key) = greedy_cache_key
863 && !trace.used_cached_greedy
864 {
865 cache_nary_sequence(key, trace.member_sequence.clone());
866 }
867 }
868 }
869
870 let final_operand = planned
871 .pop()
872 .expect("non-empty n-ary plan retains one operand");
873 let permutation = output_axes
874 .iter()
875 .chain(
876 final_operand
877 .axes
878 .iter()
879 .filter(|axis| !output_axes.contains(axis)),
880 )
881 .map(|axis| {
882 final_operand
883 .axes
884 .iter()
885 .position(|candidate| candidate == axis)
886 .expect("validated final n-ary output axis")
887 })
888 .collect::<Vec<_>>();
889 let tensor = execute_unary_einsum(
890 &final_operand.tensor,
891 UnaryEinsumSpec::new(final_operand.axes.len(), output_axes.len(), &permutation),
892 )?;
893 trace.final_permutations = 1;
894 Ok((tensor, trace))
895}
896
897#[cfg(test)]
898fn execute_nary_einsum_for_test<'a>(
899 operands: &[&Tensor],
900 spec: EllipsisEinsumSpec<'a>,
901 strategy: NaryExecutionStrategy,
902) -> Result<(Tensor, NaryExecutionTrace)> {
903 execute_nary_einsum_internal(operands, spec, strategy)
904}
905
906fn execute_expanded_binary(
907 left: &Tensor,
908 right: &Tensor,
909 left_axes: &[ExpandedAxis<'_>],
910 right_axes: &[ExpandedAxis<'_>],
911 output_axes: &[ExpandedAxis<'_>],
912) -> Result<Tensor> {
913 let graph = classify_expanded_binary_graph(
914 BinaryGraphOperand {
915 axes: left_axes,
916 dims: left.dims(),
917 layout: BinaryOperandLayout::Strided(left.stride()),
918 },
919 BinaryGraphOperand {
920 axes: right_axes,
921 dims: right.dims(),
922 layout: BinaryOperandLayout::Strided(right.stride()),
923 },
924 output_axes,
925 left.dtype().size_in_bytes(),
926 )?;
927 let plan = graph.plan;
928 let batch_label_storage = plan
929 .batch
930 .iter()
931 .map(ExpandedAxis::display_name)
932 .collect::<Vec<_>>();
933 let contracted_label_storage = plan
934 .contracted
935 .iter()
936 .map(ExpandedAxis::display_name)
937 .collect::<Vec<_>>();
938 let batch_labels = batch_label_storage
939 .iter()
940 .map(String::as_str)
941 .collect::<Vec<_>>();
942 let contracted_labels = contracted_label_storage
943 .iter()
944 .map(String::as_str)
945 .collect::<Vec<_>>();
946 execute_binary_einsum(
947 left,
948 right,
949 BinaryEinsumSpec::new(
950 [left_axes.len(), right_axes.len()],
951 [&plan.left_reductions, &plan.right_reductions],
952 [&plan.left_permutation, &plan.right_permutation],
953 plan.batch.len(),
954 plan.left_free.len(),
955 plan.contracted.len(),
956 plan.right_free.len(),
957 &batch_labels,
958 &contracted_labels,
959 &plan.output_permutation,
960 ),
961 )
962}
963
964fn execute_expanded_binary_canonical<'a>(
965 left: &Tensor,
966 right: &Tensor,
967 left_axes: &[ExpandedAxis<'a>],
968 right_axes: &[ExpandedAxis<'a>],
969 retained_axes: &[ExpandedAxis<'a>],
970) -> Result<(Tensor, Vec<ExpandedAxis<'a>>)> {
971 let plan = classify_expanded_binary(left_axes, right_axes, retained_axes);
972 let canonical = plan
973 .batch
974 .iter()
975 .chain(&plan.left_free)
976 .chain(&plan.right_free)
977 .copied()
978 .collect::<Vec<_>>();
979 let tensor = execute_expanded_binary(left, right, left_axes, right_axes, &canonical)?;
980 Ok((tensor, canonical))
981}
982
983struct PlannedOperand<'a> {
984 tensor: Tensor,
985 axes: Vec<ExpandedAxis<'a>>,
986 stable_ordinal: usize,
987 members: u64,
988}
989
990impl<'a> PlannedOperand<'a> {
991 #[cfg(test)]
992 fn new_for_test(
993 shape: (usize, usize),
994 labels: &[&'a str],
995 stable_ordinal: usize,
996 ) -> Result<Self> {
997 Ok(Self {
998 tensor: Tensor::zeros(shape, candle_core::DType::F32, &candle_core::Device::Cpu)?,
999 axes: labels.iter().copied().map(ExpandedAxis::Named).collect(),
1000 stable_ordinal,
1001 members: 1_u64 << stable_ordinal,
1002 })
1003 }
1004}
1005
1006#[derive(Clone, Debug, Eq, PartialEq)]
1007enum NaryLayoutEstimate {
1008 Contiguous,
1009 Strided(Vec<usize>),
1010 Unsupported,
1011}
1012
1013impl NaryLayoutEstimate {
1014 fn as_binary(&self) -> BinaryOperandLayout<'_> {
1015 match self {
1016 Self::Contiguous => BinaryOperandLayout::Contiguous,
1017 Self::Strided(strides) => BinaryOperandLayout::Strided(strides),
1018 Self::Unsupported => BinaryOperandLayout::Unsupported,
1019 }
1020 }
1021}
1022
1023#[derive(Clone, Debug, Eq, PartialEq)]
1024struct NaryPlannerMetadata<'a> {
1025 stable_ordinal: usize,
1026 axes: Vec<(ExpandedAxis<'a>, usize)>,
1027 layout: NaryLayoutEstimate,
1028 members: u64,
1029}
1030
1031impl<'a> NaryPlannerMetadata<'a> {
1032 #[cfg(test)]
1033 fn new_for_test(
1034 stable_ordinal: usize,
1035 axes: &[(&'a str, usize)],
1036 layout: NaryLayoutEstimate,
1037 ) -> Self {
1038 Self {
1039 stable_ordinal,
1040 axes: axes
1041 .iter()
1042 .map(|&(axis, extent)| (ExpandedAxis::Named(axis), extent))
1043 .collect(),
1044 layout,
1045 members: 1_u64.checked_shl(stable_ordinal as u32).unwrap_or(0),
1046 }
1047 }
1048
1049 fn elements(&self) -> Result<u128> {
1050 checked_nary_product(
1051 &self
1052 .axes
1053 .iter()
1054 .map(|(_, extent)| *extent)
1055 .collect::<Vec<_>>(),
1056 )
1057 }
1058}
1059
1060#[derive(Clone, Debug, Eq, PartialEq)]
1061struct NaryPairCost {
1062 flops: u128,
1063 output_elements: u128,
1064 copy_bytes: u128,
1065 submissions: u128,
1066}
1067
1068#[derive(Clone, Debug, Eq, PartialEq)]
1069struct NaryPlanStep<'a> {
1070 members: (u64, u64),
1071 output_axes: Vec<ExpandedAxis<'a>>,
1072 estimate: NaryPairCost,
1073}
1074
1075#[derive(Clone, Debug, Eq, PartialEq)]
1076struct NaryPlanMetrics {
1077 flops: u128,
1078 intermediate_elements: u128,
1079 output_elements: u128,
1080 copy_bytes: u128,
1081 peak_live_elements: u128,
1082 submissions: u128,
1083 score: u128,
1084}
1085
1086#[derive(Clone, Debug, Eq, PartialEq)]
1087struct NaryContractionPlan<'a> {
1088 steps: Vec<NaryPlanStep<'a>>,
1089 metrics: NaryPlanMetrics,
1090}
1091
1092#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1093enum NaryGreedyReason {
1094 Arity,
1095 DType,
1096 Backend,
1097 UnsupportedLayout,
1098 BelowFlopThreshold,
1099 Calibration,
1100 ModelFailure,
1101}
1102
1103const EXACT_NARY_EXECUTION_CALIBRATED: bool = false;
1107
1108#[derive(Clone, Debug, Eq, PartialEq)]
1109enum NaryPlannerDecision<'a> {
1110 Greedy(NaryGreedyReason),
1111 Exact(NaryContractionPlan<'a>),
1112}
1113
1114#[derive(Clone, Debug, Eq, PartialEq)]
1115struct NaryPlanCacheOperand {
1116 axes: Vec<String>,
1117 dims: Vec<usize>,
1118 layout: u8,
1119 strides: Vec<usize>,
1120}
1121
1122type NaryMemberPair = (u64, u64);
1123
1124#[derive(Clone, Debug, Eq, PartialEq)]
1125struct NaryPlanCacheKey {
1126 operands: Vec<NaryPlanCacheOperand>,
1127 output: Vec<String>,
1128}
1129
1130struct NaryCachedPlan {
1131 key: NaryPlanCacheKey,
1132 sequence: Vec<NaryMemberPair>,
1133}
1134
1135thread_local! {
1136 static NARY_PLAN_CACHE: RefCell<VecDeque<NaryCachedPlan>> =
1137 const { RefCell::new(VecDeque::new()) };
1138}
1139
1140fn nary_plan_cache_key(
1141 operands: &[NaryPlannerMetadata<'_>],
1142 output: &[ExpandedAxis<'_>],
1143) -> NaryPlanCacheKey {
1144 NaryPlanCacheKey {
1145 operands: operands
1146 .iter()
1147 .map(|operand| {
1148 let (layout, strides) = match &operand.layout {
1149 NaryLayoutEstimate::Contiguous => (0, Vec::new()),
1150 NaryLayoutEstimate::Strided(strides) => (1, strides.clone()),
1151 NaryLayoutEstimate::Unsupported => (2, Vec::new()),
1152 };
1153 NaryPlanCacheOperand {
1154 axes: operand
1155 .axes
1156 .iter()
1157 .map(|(axis, _)| axis.display_name())
1158 .collect(),
1159 dims: operand.axes.iter().map(|(_, extent)| *extent).collect(),
1160 layout,
1161 strides,
1162 }
1163 })
1164 .collect(),
1165 output: output.iter().map(ExpandedAxis::display_name).collect(),
1166 }
1167}
1168
1169fn cached_nary_sequence(key: &NaryPlanCacheKey) -> Option<Vec<NaryMemberPair>> {
1170 NARY_PLAN_CACHE.with(|cache| {
1171 cache
1172 .borrow()
1173 .iter()
1174 .find(|candidate| candidate.key == *key)
1175 .map(|candidate| candidate.sequence.clone())
1176 })
1177}
1178
1179fn cache_nary_sequence(key: NaryPlanCacheKey, sequence: Vec<NaryMemberPair>) {
1180 NARY_PLAN_CACHE.with(|cache| {
1181 let mut cache = cache.borrow_mut();
1182 if cache.iter().any(|candidate| candidate.key == key) {
1183 return;
1184 }
1185 if cache.len() == 16 {
1186 cache.pop_front();
1187 }
1188 cache.push_back(NaryCachedPlan { key, sequence });
1189 });
1190}
1191
1192#[cfg(test)]
1193fn clear_nary_plan_cache_for_test() {
1194 NARY_PLAN_CACHE.with(|cache| cache.borrow_mut().clear());
1195}
1196
1197fn model_axis_extent(operand: &NaryPlannerMetadata<'_>, axis: ExpandedAxis<'_>) -> Option<usize> {
1198 operand
1199 .axes
1200 .iter()
1201 .find(|(candidate, _)| *candidate == axis)
1202 .map(|(_, extent)| *extent)
1203}
1204
1205fn model_pair_details<'a>(
1206 state: &[NaryPlannerMetadata<'a>],
1207 left: usize,
1208 right: usize,
1209 final_output: &[ExpandedAxis<'a>],
1210 global_axis_order: &[ExpandedAxis<'a>],
1211) -> Result<(NaryPairCost, NaryPlannerMetadata<'a>)> {
1212 let left_operand = &state[left];
1213 let right_operand = &state[right];
1214 let union = global_axis_order
1215 .iter()
1216 .copied()
1217 .filter(|&axis| {
1218 model_axis_extent(left_operand, axis).is_some()
1219 || model_axis_extent(right_operand, axis).is_some()
1220 })
1221 .collect::<Vec<_>>();
1222 let retained = union
1223 .iter()
1224 .copied()
1225 .filter(|&axis| {
1226 final_output.contains(&axis)
1227 || state.iter().enumerate().any(|(index, operand)| {
1228 index != left && index != right && model_axis_extent(operand, axis).is_some()
1229 })
1230 })
1231 .collect::<Vec<_>>();
1232 let resolve = |axis| match (
1233 model_axis_extent(left_operand, axis),
1234 model_axis_extent(right_operand, axis),
1235 ) {
1236 (Some(left), Some(right)) => resolve_extent(&axis.display_name(), left, right),
1237 (Some(extent), None) | (None, Some(extent)) => Ok(extent),
1238 (None, None) => candle_core::bail!("n-ary model axis is absent from both operands"),
1239 };
1240 let left_axes = left_operand
1241 .axes
1242 .iter()
1243 .map(|(axis, _)| *axis)
1244 .collect::<Vec<_>>();
1245 let left_dims = left_operand
1246 .axes
1247 .iter()
1248 .map(|(_, extent)| *extent)
1249 .collect::<Vec<_>>();
1250 let right_axes = right_operand
1251 .axes
1252 .iter()
1253 .map(|(axis, _)| *axis)
1254 .collect::<Vec<_>>();
1255 let right_dims = right_operand
1256 .axes
1257 .iter()
1258 .map(|(_, extent)| *extent)
1259 .collect::<Vec<_>>();
1260 let graph = classify_expanded_binary_graph(
1261 BinaryGraphOperand {
1262 axes: &left_axes,
1263 dims: &left_dims,
1264 layout: left_operand.layout.as_binary(),
1265 },
1266 BinaryGraphOperand {
1267 axes: &right_axes,
1268 dims: &right_dims,
1269 layout: right_operand.layout.as_binary(),
1270 },
1271 &retained,
1272 4,
1273 )?;
1274 let classification = graph.plan;
1275 let canonical_axes = classification
1276 .batch
1277 .iter()
1278 .chain(&classification.left_free)
1279 .chain(&classification.right_free)
1280 .copied()
1281 .collect::<Vec<_>>();
1282 let axes = canonical_axes
1283 .iter()
1284 .copied()
1285 .map(|axis| resolve(axis).map(|extent| (axis, extent)))
1286 .collect::<Result<Vec<_>>>()?;
1287 Ok((
1288 NaryPairCost {
1289 flops: graph.work,
1290 output_elements: graph.output_elements,
1291 copy_bytes: graph.copy_bytes,
1292 submissions: graph.submissions,
1293 },
1294 NaryPlannerMetadata {
1295 stable_ordinal: left_operand
1296 .stable_ordinal
1297 .min(right_operand.stable_ordinal),
1298 axes,
1299 layout: graph.output_layout,
1300 members: left_operand.members | right_operand.members,
1301 },
1302 ))
1303}
1304
1305fn model_initial_metrics(state: &[NaryPlannerMetadata<'_>]) -> Result<NaryPlanMetrics> {
1306 let peak_live_elements = state.iter().try_fold(0_u128, |sum, operand| {
1307 sum.checked_add(operand.elements()?)
1308 .ok_or_else(|| candle_core::Error::msg("n-ary live estimate overflows u128"))
1309 })?;
1310 Ok(NaryPlanMetrics {
1311 flops: 0,
1312 intermediate_elements: 0,
1313 output_elements: 0,
1314 copy_bytes: 0,
1315 peak_live_elements,
1316 submissions: 0,
1317 score: 0,
1318 })
1319}
1320
1321fn model_accumulate(
1322 metrics: &mut NaryPlanMetrics,
1323 state: &[NaryPlannerMetadata<'_>],
1324 estimate: &NaryPairCost,
1325) -> Result<()> {
1326 let live = state.iter().try_fold(0_u128, |sum, operand| {
1327 sum.checked_add(operand.elements()?)
1328 .ok_or_else(|| candle_core::Error::msg("n-ary live estimate overflows u128"))
1329 })?;
1330 metrics.peak_live_elements = metrics.peak_live_elements.max(
1331 live.checked_add(estimate.output_elements)
1332 .ok_or_else(|| candle_core::Error::msg("n-ary peak estimate overflows u128"))?,
1333 );
1334 metrics.flops = metrics
1335 .flops
1336 .checked_add(estimate.flops)
1337 .ok_or_else(|| candle_core::Error::msg("n-ary FLOP estimate overflows u128"))?;
1338 metrics.intermediate_elements = metrics
1339 .intermediate_elements
1340 .checked_add(estimate.output_elements)
1341 .ok_or_else(|| candle_core::Error::msg("n-ary intermediate estimate overflows u128"))?;
1342 metrics.copy_bytes = metrics
1343 .copy_bytes
1344 .checked_add(estimate.copy_bytes)
1345 .ok_or_else(|| candle_core::Error::msg("n-ary copy estimate overflows u128"))?;
1346 metrics.submissions = metrics
1347 .submissions
1348 .checked_add(estimate.submissions)
1349 .ok_or_else(|| candle_core::Error::msg("n-ary submission estimate overflows u128"))?;
1350 metrics.output_elements = estimate.output_elements;
1351 Ok(())
1352}
1353
1354fn model_score(metrics: &NaryPlanMetrics) -> Result<u128> {
1355 [
1356 (metrics.flops, 1),
1357 (metrics.copy_bytes, 1),
1358 (metrics.intermediate_elements, 2),
1359 (metrics.peak_live_elements, 2),
1360 (metrics.submissions, 1_024),
1361 ]
1362 .into_iter()
1363 .try_fold(0_u128, |sum, (value, weight)| {
1364 sum.checked_add(
1365 value
1366 .checked_mul(weight)
1367 .ok_or_else(|| candle_core::Error::msg("n-ary weighted estimate overflows u128"))?,
1368 )
1369 .ok_or_else(|| candle_core::Error::msg("n-ary score estimate overflows u128"))
1370 })
1371}
1372
1373fn model_apply_pair<'a>(
1374 state: &mut Vec<NaryPlannerMetadata<'a>>,
1375 left: usize,
1376 right: usize,
1377 output: NaryPlannerMetadata<'a>,
1378) {
1379 state.remove(right);
1380 state.remove(left);
1381 state.insert(left, output);
1382}
1383
1384fn model_greedy_plan<'a>(
1385 operands: &[NaryPlannerMetadata<'a>],
1386 final_output: &[ExpandedAxis<'a>],
1387 global: &[ExpandedAxis<'a>],
1388) -> Result<NaryContractionPlan<'a>> {
1389 let mut state = operands.to_vec();
1390 let mut metrics = model_initial_metrics(&state)?;
1391 let mut steps = Vec::new();
1392 while state.len() > 1 {
1393 let mut best = None;
1394 for left in 0..state.len() - 1 {
1395 for right in left + 1..state.len() {
1396 let (estimate, output) =
1397 model_pair_details(&state, left, right, final_output, global)?;
1398 let key = (
1399 estimate.output_elements,
1400 estimate.flops,
1401 state[left].stable_ordinal,
1402 state[right].stable_ordinal,
1403 left,
1404 right,
1405 );
1406 if best.as_ref().is_none_or(
1407 |(bl, br, be, _): &(usize, usize, NaryPairCost, NaryPlannerMetadata<'a>)| {
1408 key < (
1409 be.output_elements,
1410 be.flops,
1411 state[*bl].stable_ordinal,
1412 state[*br].stable_ordinal,
1413 *bl,
1414 *br,
1415 )
1416 },
1417 ) {
1418 best = Some((left, right, estimate, output));
1419 }
1420 }
1421 }
1422 let (left, right, estimate, output) =
1423 best.ok_or_else(|| candle_core::Error::msg("n-ary greedy model found no pair"))?;
1424 model_accumulate(&mut metrics, &state, &estimate)?;
1425 steps.push(NaryPlanStep {
1426 members: (state[left].members, state[right].members),
1427 output_axes: output.axes.iter().map(|(axis, _)| *axis).collect(),
1428 estimate,
1429 });
1430 model_apply_pair(&mut state, left, right, output);
1431 }
1432 metrics.score = model_score(&metrics)?;
1433 Ok(NaryContractionPlan { steps, metrics })
1434}
1435
1436fn model_sequence_plan<'a>(
1437 operands: &[NaryPlannerMetadata<'a>],
1438 final_output: &[ExpandedAxis<'a>],
1439 global: &[ExpandedAxis<'a>],
1440 sequence: &[(u64, u64)],
1441) -> Result<NaryContractionPlan<'a>> {
1442 if sequence.len() + 1 != operands.len() {
1443 candle_core::bail!("cached n-ary sequence has the wrong number of steps")
1444 }
1445 let mut state = operands.to_vec();
1446 let mut metrics = model_initial_metrics(&state)?;
1447 let mut steps = Vec::with_capacity(sequence.len());
1448 for &members in sequence {
1449 let left = state
1450 .iter()
1451 .position(|operand| operand.members == members.0)
1452 .ok_or_else(|| candle_core::Error::msg("cached n-ary left members are not live"))?;
1453 let right = state
1454 .iter()
1455 .position(|operand| operand.members == members.1)
1456 .ok_or_else(|| candle_core::Error::msg("cached n-ary right members are not live"))?;
1457 if left >= right {
1458 candle_core::bail!("cached n-ary members are not in stable order")
1459 }
1460 let (estimate, pair_output) =
1461 model_pair_details(&state, left, right, final_output, global)?;
1462 model_accumulate(&mut metrics, &state, &estimate)?;
1463 steps.push(NaryPlanStep {
1464 members,
1465 output_axes: pair_output.axes.iter().map(|(axis, _)| *axis).collect(),
1466 estimate,
1467 });
1468 model_apply_pair(&mut state, left, right, pair_output);
1469 }
1470 metrics.score = model_score(&metrics)?;
1471 Ok(NaryContractionPlan { steps, metrics })
1472}
1473
1474fn model_exact_search<'a>(
1475 operands: &[NaryPlannerMetadata<'a>],
1476 final_output: &[ExpandedAxis<'a>],
1477 global: &[ExpandedAxis<'a>],
1478) -> Result<NaryContractionPlan<'a>> {
1479 fn visit<'a>(
1480 state: Vec<NaryPlannerMetadata<'a>>,
1481 steps: Vec<NaryPlanStep<'a>>,
1482 metrics: NaryPlanMetrics,
1483 output: &[ExpandedAxis<'a>],
1484 global: &[ExpandedAxis<'a>],
1485 best: &mut Option<NaryContractionPlan<'a>>,
1486 ) -> Result<()> {
1487 if state.len() == 1 {
1488 let mut metrics = metrics;
1489 metrics.score = model_score(&metrics)?;
1490 let candidate = NaryContractionPlan { steps, metrics };
1491 let masks = |plan: &NaryContractionPlan<'a>| {
1492 plan.steps
1493 .iter()
1494 .map(|step| step.members)
1495 .collect::<Vec<_>>()
1496 };
1497 if best.as_ref().is_none_or(|current| {
1498 (candidate.metrics.score, masks(&candidate))
1499 < (current.metrics.score, masks(current))
1500 }) {
1501 *best = Some(candidate);
1502 }
1503 return Ok(());
1504 }
1505 for left in 0..state.len() - 1 {
1506 for right in left + 1..state.len() {
1507 let (estimate, pair_output) =
1508 model_pair_details(&state, left, right, output, global)?;
1509 let mut next_state = state.clone();
1510 let mut next_metrics = metrics.clone();
1511 model_accumulate(&mut next_metrics, &state, &estimate)?;
1512 let mut next_steps = steps.clone();
1513 next_steps.push(NaryPlanStep {
1514 members: (state[left].members, state[right].members),
1515 output_axes: pair_output.axes.iter().map(|(axis, _)| *axis).collect(),
1516 estimate,
1517 });
1518 model_apply_pair(&mut next_state, left, right, pair_output);
1519 visit(next_state, next_steps, next_metrics, output, global, best)?;
1520 }
1521 }
1522 Ok(())
1523 }
1524 if !(3..=4).contains(&operands.len()) {
1525 candle_core::bail!("exact n-ary planner supports arity 3 through 4")
1526 }
1527 let metrics = model_initial_metrics(operands)?;
1528 let mut best = None;
1529 visit(
1530 operands.to_vec(),
1531 Vec::new(),
1532 metrics,
1533 final_output,
1534 global,
1535 &mut best,
1536 )?;
1537 best.ok_or_else(|| candle_core::Error::msg("exact n-ary planner found no plan"))
1538}
1539
1540fn select_layout_aware_plan<'a>(
1541 operands: &[NaryPlannerMetadata<'a>],
1542 final_output: &[ExpandedAxis<'a>],
1543 dtype: DType,
1544 cpu: bool,
1545) -> NaryPlannerDecision<'a> {
1546 if !(3..=4).contains(&operands.len()) {
1547 return NaryPlannerDecision::Greedy(NaryGreedyReason::Arity);
1548 }
1549 if dtype != DType::F32 {
1550 return NaryPlannerDecision::Greedy(NaryGreedyReason::DType);
1551 }
1552 if !cpu {
1553 return NaryPlannerDecision::Greedy(NaryGreedyReason::Backend);
1554 }
1555 if operands
1556 .iter()
1557 .any(|operand| operand.layout == NaryLayoutEstimate::Unsupported)
1558 {
1559 return NaryPlannerDecision::Greedy(NaryGreedyReason::UnsupportedLayout);
1560 }
1561 if operands
1562 .iter()
1563 .any(|operand| operand.axes.iter().any(|(_, extent)| *extent == 0))
1564 {
1565 return NaryPlannerDecision::Greedy(NaryGreedyReason::BelowFlopThreshold);
1566 }
1567 if !EXACT_NARY_EXECUTION_CALIBRATED {
1568 return NaryPlannerDecision::Greedy(NaryGreedyReason::Calibration);
1569 }
1570 let mut global = Vec::new();
1571 for operand in operands {
1572 for &(axis, _) in &operand.axes {
1573 if !global.contains(&axis) {
1574 global.push(axis);
1575 }
1576 }
1577 }
1578 let cache_key = nary_plan_cache_key(operands, final_output);
1579 if let Some(sequence) = cached_nary_sequence(&cache_key)
1580 && let Ok(plan) = model_sequence_plan(operands, final_output, &global, &sequence)
1581 {
1582 return NaryPlannerDecision::Exact(plan);
1583 }
1584 let greedy = match model_greedy_plan(operands, final_output, &global) {
1585 Ok(plan) => plan,
1586 Err(_) => return NaryPlannerDecision::Greedy(NaryGreedyReason::ModelFailure),
1587 };
1588 if greedy.metrics.flops < 100_000 {
1589 return NaryPlannerDecision::Greedy(NaryGreedyReason::BelowFlopThreshold);
1590 }
1591 match model_exact_search(operands, final_output, &global) {
1592 Ok(plan) => {
1593 cache_nary_sequence(
1594 cache_key,
1595 plan.steps.iter().map(|step| step.members).collect(),
1596 );
1597 NaryPlannerDecision::Exact(plan)
1598 }
1599 Err(_) => NaryPlannerDecision::Greedy(NaryGreedyReason::ModelFailure),
1600 }
1601}
1602
1603#[cfg(test)]
1604fn select_layout_aware_plan_for_test<'a>(
1605 operands: &[NaryPlannerMetadata<'a>],
1606 final_output: &[&'a str],
1607 dtype: DType,
1608 cpu: bool,
1609) -> NaryPlannerDecision<'a> {
1610 select_layout_aware_plan(
1611 operands,
1612 &final_output
1613 .iter()
1614 .copied()
1615 .map(ExpandedAxis::Named)
1616 .collect::<Vec<_>>(),
1617 dtype,
1618 cpu,
1619 )
1620}
1621
1622#[cfg(test)]
1623fn plan_layout_exact_for_test<'a>(
1624 operands: &[NaryPlannerMetadata<'a>],
1625 final_output: &[&'a str],
1626) -> Result<NaryContractionPlan<'a>> {
1627 let mut global = Vec::new();
1628 for operand in operands {
1629 for &(axis, _) in &operand.axes {
1630 if !global.contains(&axis) {
1631 global.push(axis);
1632 }
1633 }
1634 }
1635 model_exact_search(
1636 operands,
1637 &final_output
1638 .iter()
1639 .copied()
1640 .map(ExpandedAxis::Named)
1641 .collect::<Vec<_>>(),
1642 &global,
1643 )
1644}
1645
1646#[derive(Debug)]
1647struct PairEstimate<'a> {
1648 left: usize,
1649 right: usize,
1650 output_axes: Vec<ExpandedAxis<'a>>,
1651 output_elements: u128,
1652 flops: u128,
1653}
1654
1655fn stable_axis_order<'a>(operands: &[PlannedOperand<'a>]) -> Vec<ExpandedAxis<'a>> {
1656 let mut axes = Vec::new();
1657 let mut ordered = operands.iter().collect::<Vec<_>>();
1658 ordered.sort_by_key(|operand| operand.stable_ordinal);
1659 for operand in ordered {
1660 for &axis in &operand.axes {
1661 if !axes.contains(&axis) {
1662 axes.push(axis);
1663 }
1664 }
1665 }
1666 axes
1667}
1668
1669fn validate_nary_broadcasts(operands: &[PlannedOperand<'_>]) -> Result<()> {
1670 let mut dimensions = Vec::<(ExpandedAxis<'_>, usize)>::new();
1671 for operand in operands {
1672 for (&axis, &extent) in operand.axes.iter().zip(operand.tensor.dims()) {
1673 if let Some((_, resolved)) = dimensions
1674 .iter_mut()
1675 .find(|(candidate, _)| *candidate == axis)
1676 {
1677 *resolved = resolve_extent(&axis.display_name(), *resolved, extent)?;
1678 } else {
1679 dimensions.push((axis, extent));
1680 }
1681 }
1682 }
1683 Ok(())
1684}
1685
1686#[cfg(test)]
1687fn select_nary_pair<'a>(
1688 operands: &[PlannedOperand<'a>],
1689 final_output: &[ExpandedAxis<'a>],
1690) -> Result<PairEstimate<'a>> {
1691 let global_axis_order = stable_axis_order(operands);
1692 select_nary_pair_with_order(operands, final_output, &global_axis_order)
1693}
1694
1695fn select_nary_pair_with_order<'a>(
1696 operands: &[PlannedOperand<'a>],
1697 final_output: &[ExpandedAxis<'a>],
1698 global_axis_order: &[ExpandedAxis<'a>],
1699) -> Result<PairEstimate<'a>> {
1700 if operands.len() < 2 {
1701 candle_core::bail!("invalid n-ary einsum planner state: fewer than two operands")
1702 }
1703 let mut best: Option<PairEstimate<'a>> = None;
1704 for left in 0..operands.len() - 1 {
1705 for right in left + 1..operands.len() {
1706 let candidate =
1707 estimate_pair_with_order(operands, left, right, final_output, global_axis_order)?;
1708 let candidate_key = (
1709 candidate.output_elements,
1710 candidate.flops,
1711 operands[left].stable_ordinal,
1712 operands[right].stable_ordinal,
1713 left,
1714 right,
1715 );
1716 let replace = best.as_ref().is_none_or(|current| {
1717 candidate_key
1718 < (
1719 current.output_elements,
1720 current.flops,
1721 operands[current.left].stable_ordinal,
1722 operands[current.right].stable_ordinal,
1723 current.left,
1724 current.right,
1725 )
1726 });
1727 if replace {
1728 best = Some(candidate);
1729 }
1730 }
1731 }
1732 best.ok_or_else(|| candle_core::Error::msg("n-ary einsum planner found no operand pair"))
1733}
1734
1735#[cfg(test)]
1736fn estimate_pair<'a>(
1737 operands: &[PlannedOperand<'a>],
1738 left: usize,
1739 right: usize,
1740 final_output: &[ExpandedAxis<'a>],
1741) -> Result<PairEstimate<'a>> {
1742 let global_axis_order = stable_axis_order(operands);
1743 estimate_pair_with_order(operands, left, right, final_output, &global_axis_order)
1744}
1745
1746fn estimate_pair_with_order<'a>(
1747 operands: &[PlannedOperand<'a>],
1748 left: usize,
1749 right: usize,
1750 final_output: &[ExpandedAxis<'a>],
1751 global_axis_order: &[ExpandedAxis<'a>],
1752) -> Result<PairEstimate<'a>> {
1753 let left_operand = operands.get(left).ok_or_else(|| {
1754 candle_core::Error::msg(format!(
1755 "n-ary einsum planner left index {left} is out of range"
1756 ))
1757 })?;
1758 let right_operand = operands.get(right).ok_or_else(|| {
1759 candle_core::Error::msg(format!(
1760 "n-ary einsum planner right index {right} is out of range"
1761 ))
1762 })?;
1763 if left >= right {
1764 candle_core::bail!("n-ary einsum planner pair must be ordered, received ({left}, {right})")
1765 }
1766
1767 let mut pair_axes = Vec::new();
1768 for &axis in left_operand.axes.iter().chain(&right_operand.axes) {
1769 if !pair_axes.contains(&axis) {
1770 pair_axes.push(axis);
1771 }
1772 }
1773 let mut live_axes = final_output.to_vec();
1774 for (index, operand) in operands.iter().enumerate() {
1775 if index != left && index != right {
1776 for &axis in &operand.axes {
1777 if !live_axes.contains(&axis) {
1778 live_axes.push(axis);
1779 }
1780 }
1781 }
1782 }
1783 let output_axes = global_axis_order
1784 .iter()
1785 .copied()
1786 .filter(|axis| pair_axes.contains(axis) && live_axes.contains(axis))
1787 .collect::<Vec<_>>();
1788 let output_extents = output_axes
1789 .iter()
1790 .map(|axis| pair_axis_extent(left_operand, right_operand, *axis))
1791 .collect::<Result<Vec<_>>>()?;
1792 let flop_extents = pair_axes
1793 .iter()
1794 .map(|axis| pair_axis_extent(left_operand, right_operand, *axis))
1795 .collect::<Result<Vec<_>>>()?;
1796 let output_elements = checked_nary_product(&output_extents).map_err(|error| {
1797 error.context(format!(
1798 "einsum n-ary pair ({left}, {right}) intermediate estimate"
1799 ))
1800 })?;
1801 let flops = checked_nary_product(&flop_extents).map_err(|error| {
1802 error.context(format!("einsum n-ary pair ({left}, {right}) FLOP estimate"))
1803 })?;
1804 Ok(PairEstimate {
1805 left,
1806 right,
1807 output_axes,
1808 output_elements,
1809 flops,
1810 })
1811}
1812
1813fn pair_axis_extent(
1814 left: &PlannedOperand<'_>,
1815 right: &PlannedOperand<'_>,
1816 axis: ExpandedAxis<'_>,
1817) -> Result<usize> {
1818 let left_extent = left
1819 .axes
1820 .iter()
1821 .position(|candidate| *candidate == axis)
1822 .map(|position| left.tensor.dims()[position]);
1823 let right_extent = right
1824 .axes
1825 .iter()
1826 .position(|candidate| *candidate == axis)
1827 .map(|position| right.tensor.dims()[position]);
1828 match (left_extent, right_extent) {
1829 (Some(left), Some(right)) => resolve_extent(&axis.display_name(), left, right),
1830 (Some(extent), None) | (None, Some(extent)) => Ok(extent),
1831 (None, None) => candle_core::bail!(
1832 "invalid n-ary einsum planner axis `{}` is absent from both operands",
1833 axis.display_name()
1834 ),
1835 }
1836}
1837
1838fn checked_nary_product(extents: &[usize]) -> Result<u128> {
1839 if extents.contains(&0) {
1840 return Ok(0);
1841 }
1842 extents.iter().try_fold(1_u128, |product, &extent| {
1843 product
1844 .checked_mul(extent as u128)
1845 .ok_or_else(|| candle_core::Error::msg("einsum n-ary cost estimate overflows u128"))
1846 })
1847}
1848
1849#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1850enum ExpandedAxis<'a> {
1851 Named(&'a str),
1852 Ellipsis(usize),
1853}
1854
1855impl ExpandedAxis<'_> {
1856 fn display_name(&self) -> String {
1857 match self {
1858 Self::Named(name) => (*name).to_owned(),
1859 Self::Ellipsis(index) => format!("..[{index}]"),
1860 }
1861 }
1862}
1863
1864fn ellipsis_capture(
1865 operand: &Tensor,
1866 operand_index: usize,
1867 pattern: EinsumAxisPattern<'_>,
1868) -> Result<usize> {
1869 if let Some(position) = pattern.ellipsis_position {
1870 if position > pattern.labels.len() {
1871 candle_core::bail!(
1872 "invalid ellipsis einsum plan: operand {operand_index} ellipsis position {position} exceeds {} explicit labels",
1873 pattern.labels.len()
1874 )
1875 }
1876 operand.rank().checked_sub(pattern.labels.len()).ok_or_else(|| {
1877 candle_core::Error::msg(format!(
1878 "einsum operand {operand_index} has rank {}, but {} explicit axes leave no valid ellipsis capture",
1879 operand.rank(),
1880 pattern.labels.len()
1881 ))
1882 })
1883 } else if operand.rank() == pattern.labels.len() {
1884 Ok(0)
1885 } else {
1886 candle_core::bail!(
1887 "einsum operand {operand_index} has rank {}, expected {} because its axis list has no ellipsis",
1888 operand.rank(),
1889 pattern.labels.len()
1890 )
1891 }
1892}
1893
1894fn normalize_ellipsis_operand(
1895 operand: &Tensor,
1896 pattern: EinsumAxisPattern<'_>,
1897 capture: usize,
1898 maximum_capture: usize,
1899 operand_index: usize,
1900) -> Result<Tensor> {
1901 let missing = maximum_capture.checked_sub(capture).ok_or_else(|| {
1902 candle_core::Error::msg("invalid ellipsis einsum plan: capture exceeds maximum")
1903 })?;
1904 let insertion = pattern.ellipsis_position.unwrap_or(0);
1905 let mut normalized = operand.clone();
1906 for _ in 0..missing {
1907 normalized = normalized.unsqueeze(insertion).map_err(|error| {
1908 error.context(format!(
1909 "einsum operand {operand_index} ellipsis right-alignment"
1910 ))
1911 })?;
1912 }
1913 Ok(normalized)
1914}
1915
1916#[derive(Debug, PartialEq, Eq)]
1917enum RepeatedAxisLoweringPlan {
1918 Sequential,
1919 OriginalFlatGather {
1920 output_shape: Vec<usize>,
1921 offsets: Vec<u32>,
1922 },
1923}
1924
1925#[derive(Debug)]
1932pub struct PreparedDiagonalPlan {
1933 input_shape: Vec<usize>,
1934 axis_ids: Vec<usize>,
1935 output_shape: Vec<usize>,
1936 indices: Tensor,
1937}
1938
1939impl PreparedDiagonalPlan {
1940 pub fn new(input_shape: &[usize], axis_ids: &[usize], device: &Device) -> Result<Self> {
1942 if input_shape.len() != axis_ids.len() {
1943 candle_core::bail!(
1944 "prepared diagonal shape rank {} does not match axis-id rank {}",
1945 input_shape.len(),
1946 axis_ids.len()
1947 )
1948 }
1949 let axes = axis_ids
1950 .iter()
1951 .copied()
1952 .map(ExpandedAxis::Ellipsis)
1953 .collect::<Vec<_>>();
1954 validate_repeated_extents(input_shape, &axes, 0)?;
1955 if !(0..axis_ids.len()).any(|position| axis_ids[..position].contains(&axis_ids[position])) {
1956 candle_core::bail!("prepared diagonal plan requires at least one repeated axis id")
1957 }
1958 let Some((output_shape, offsets)) = original_flat_gather_offsets(input_shape, &axes, 0)?
1959 else {
1960 candle_core::bail!("prepared diagonal offsets exceed the supported u32 index range")
1961 };
1962 let index_count = offsets.len();
1963 let indices = Tensor::from_vec(offsets, index_count, device)
1964 .map_err(|error| error.context("prepared diagonal device-local indices"))?;
1965 Ok(Self {
1966 input_shape: input_shape.to_vec(),
1967 axis_ids: axis_ids.to_vec(),
1968 output_shape,
1969 indices,
1970 })
1971 }
1972
1973 pub fn repeated(extent: usize, multiplicity: usize, device: &Device) -> Result<Self> {
1975 if multiplicity < 2 {
1976 candle_core::bail!("prepared repeated diagonal requires multiplicity of at least two")
1977 }
1978 Self::new(&vec![extent; multiplicity], &vec![0; multiplicity], device)
1979 }
1980
1981 pub fn interleaved(first_extent: usize, second_extent: usize, device: &Device) -> Result<Self> {
1983 Self::new(
1984 &[first_extent, second_extent, first_extent, second_extent],
1985 &[0, 1, 0, 1],
1986 device,
1987 )
1988 }
1989
1990 pub fn execute(&self, input: &Tensor) -> Result<Tensor> {
1992 if input.dims() != self.input_shape {
1993 candle_core::bail!(
1994 "prepared diagonal plan expects shape {:?}, received {:?}",
1995 self.input_shape,
1996 input.dims()
1997 )
1998 }
1999 if !input.is_contiguous() {
2000 candle_core::bail!("prepared diagonal plan requires contiguous input")
2001 }
2002 if !input.device().same_device(self.indices.device()) {
2003 candle_core::bail!("prepared diagonal plan and input are on different devices")
2004 }
2005 input
2006 .flatten_all()
2007 .and_then(|flat| flat.index_select(&self.indices, 0))
2008 .and_then(|selected| selected.reshape(self.output_shape.as_slice()))
2009 .map_err(|error| error.context("prepared diagonal extraction"))
2010 }
2011
2012 #[must_use]
2014 pub fn input_shape(&self) -> &[usize] {
2015 &self.input_shape
2016 }
2017
2018 #[must_use]
2020 pub fn axis_ids(&self) -> &[usize] {
2021 &self.axis_ids
2022 }
2023
2024 #[must_use]
2026 pub fn output_shape(&self) -> &[usize] {
2027 &self.output_shape
2028 }
2029
2030 #[must_use]
2032 pub const fn index_dtype(&self) -> DType {
2033 DType::U32
2034 }
2035
2036 #[must_use]
2038 pub fn indices(&self) -> &Tensor {
2039 &self.indices
2040 }
2041}
2042
2043fn validate_repeated_extents(
2044 dims: &[usize],
2045 axes: &[ExpandedAxis<'_>],
2046 operand_index: usize,
2047) -> Result<()> {
2048 for (position, axis) in axes.iter().copied().enumerate() {
2049 let Some(previous) = axes[..position]
2050 .iter()
2051 .position(|candidate| *candidate == axis)
2052 else {
2053 continue;
2054 };
2055 let extent = dims[previous];
2056 let other = dims[position];
2057 if other != extent {
2058 candle_core::bail!(
2059 "einsum operand {operand_index} repeated label `{}` has unequal extents {extent} and {other}",
2060 axis.display_name()
2061 )
2062 }
2063 }
2064 Ok(())
2065}
2066
2067fn checked_contiguous_strides(dims: &[usize]) -> Option<Vec<usize>> {
2068 let mut product = 1_usize;
2069 let mut strides = vec![0; dims.len()];
2070 for (position, &extent) in dims.iter().enumerate().rev() {
2071 strides[position] = product;
2072 product = product.checked_mul(extent)?;
2073 }
2074 Some(strides)
2075}
2076
2077fn simulated_contiguous(dims: &[usize], strides: &[usize]) -> Option<bool> {
2078 let mut expected = 1_usize;
2079 for (&extent, &stride) in dims.iter().zip(strides).rev() {
2080 if extent > 1 && stride != expected {
2081 return Some(false);
2082 }
2083 expected = expected.checked_mul(extent)?;
2084 }
2085 Some(true)
2086}
2087
2088fn sequential_diagonal_would_materialize(
2089 original_dims: &[usize],
2090 original_axes: &[ExpandedAxis<'_>],
2091) -> Option<bool> {
2092 let mut dims = original_dims.to_vec();
2093 let mut axes = original_axes.to_vec();
2094 let mut strides = checked_contiguous_strides(&dims)?;
2095 loop {
2096 let Some(repeated_axis) = axes
2097 .iter()
2098 .copied()
2099 .find(|axis| axes.iter().filter(|candidate| **candidate == *axis).count() > 1)
2100 else {
2101 return Some(false);
2102 };
2103 let positions = axes
2104 .iter()
2105 .enumerate()
2106 .filter_map(|(position, axis)| (*axis == repeated_axis).then_some(position))
2107 .collect::<Vec<_>>();
2108 let other_positions = (0..axes.len())
2109 .filter(|position| !positions.contains(position))
2110 .collect::<Vec<_>>();
2111 let permutation = positions
2112 .iter()
2113 .chain(&other_positions)
2114 .copied()
2115 .collect::<Vec<_>>();
2116 let adjacent_dims = permutation
2117 .iter()
2118 .map(|&position| dims[position])
2119 .collect::<Vec<_>>();
2120 let adjacent_strides = permutation
2121 .iter()
2122 .map(|&position| strides[position])
2123 .collect::<Vec<_>>();
2124 if !simulated_contiguous(&adjacent_dims, &adjacent_strides)? {
2125 return Some(true);
2126 }
2127
2128 let selected_dims = std::iter::once(dims[positions[0]])
2129 .chain(other_positions.iter().map(|&position| dims[position]))
2130 .collect::<Vec<_>>();
2131 let selected_strides = checked_contiguous_strides(&selected_dims)?;
2132 let result_order = std::iter::once(positions[0])
2133 .chain(other_positions.iter().copied())
2134 .collect::<Vec<_>>();
2135 let desired_order = (0..axes.len())
2136 .filter(|position| !positions[1..].contains(position))
2137 .collect::<Vec<_>>();
2138 let restoration = desired_order
2139 .iter()
2140 .map(|position| {
2141 result_order
2142 .iter()
2143 .position(|candidate| candidate == position)
2144 })
2145 .collect::<Option<Vec<_>>>()?;
2146 dims = restoration
2147 .iter()
2148 .map(|&position| selected_dims[position])
2149 .collect();
2150 strides = restoration
2151 .iter()
2152 .map(|&position| selected_strides[position])
2153 .collect();
2154 axes = desired_order
2155 .iter()
2156 .map(|&position| axes[position])
2157 .collect();
2158 }
2159}
2160
2161fn sequential_adjacency_is_always_identity(original_axes: &[ExpandedAxis<'_>]) -> bool {
2162 if original_axes
2163 .first()
2164 .is_some_and(|first| original_axes.iter().all(|axis| axis == first))
2165 {
2166 return true;
2167 }
2168 let mut axes = original_axes.to_vec();
2169 loop {
2170 let Some(repeated_axis) = axes
2171 .iter()
2172 .copied()
2173 .find(|axis| axes.iter().filter(|candidate| **candidate == *axis).count() > 1)
2174 else {
2175 return true;
2176 };
2177 let positions = axes
2178 .iter()
2179 .enumerate()
2180 .filter_map(|(position, axis)| (*axis == repeated_axis).then_some(position))
2181 .collect::<Vec<_>>();
2182 if positions.iter().copied().ne(0..positions.len()) {
2183 return false;
2184 }
2185 axes = axes
2186 .iter()
2187 .enumerate()
2188 .filter_map(|(position, axis)| (!positions[1..].contains(&position)).then_some(*axis))
2189 .collect();
2190 }
2191}
2192
2193fn original_flat_gather_offsets(
2194 dims: &[usize],
2195 axes: &[ExpandedAxis<'_>],
2196 operand_index: usize,
2197) -> Result<Option<(Vec<usize>, Vec<u32>)>> {
2198 let mut unique_axes = Vec::new();
2199 let mut unique_positions = Vec::with_capacity(axes.len());
2200 for axis in axes.iter().copied() {
2201 let position =
2202 if let Some(position) = unique_axes.iter().position(|candidate| *candidate == axis) {
2203 position
2204 } else {
2205 unique_axes.push(axis);
2206 unique_axes.len() - 1
2207 };
2208 unique_positions.push(position);
2209 }
2210 let output_shape = unique_axes
2211 .iter()
2212 .map(|axis| {
2213 let position = axes
2214 .iter()
2215 .position(|candidate| candidate == axis)
2216 .expect("a unique diagonal axis came from the original axes");
2217 dims[position]
2218 })
2219 .collect::<Vec<_>>();
2220
2221 if output_shape.contains(&0) {
2222 return Ok(Some((output_shape, Vec::new())));
2223 }
2224 let Some(original_strides) = checked_contiguous_strides(dims) else {
2225 return Ok(None);
2226 };
2227 let Some(output_elements) = output_shape
2228 .iter()
2229 .try_fold(1_usize, |product, &extent| product.checked_mul(extent))
2230 else {
2231 return Ok(None);
2232 };
2233 let mut maximum_offset = 0_usize;
2234 for (position, &stride) in original_strides.iter().enumerate() {
2235 let coordinate = output_shape[unique_positions[position]] - 1;
2236 let Some(contribution) = coordinate.checked_mul(stride) else {
2237 return Ok(None);
2238 };
2239 let Some(next) = maximum_offset.checked_add(contribution) else {
2240 return Ok(None);
2241 };
2242 maximum_offset = next;
2243 }
2244 if maximum_offset >= u32::MAX as usize {
2245 return Ok(None);
2246 }
2247
2248 let Some(output_strides) = checked_contiguous_strides(&output_shape) else {
2249 return Ok(None);
2250 };
2251 let mut offsets = Vec::new();
2252 offsets
2253 .try_reserve_exact(output_elements)
2254 .map_err(|error| {
2255 candle_core::Error::msg(format!(
2256 "einsum operand {operand_index} diagonal offset allocation failed: {error}"
2257 ))
2258 })?;
2259 for output_index in 0..output_elements {
2260 let mut offset = 0_usize;
2261 for (position, &stride) in original_strides.iter().enumerate() {
2262 let unique_position = unique_positions[position];
2263 let coordinate =
2264 (output_index / output_strides[unique_position]) % output_shape[unique_position];
2265 offset += coordinate * stride;
2266 }
2267 offsets.push(u32::try_from(offset).expect("maximum checked original-layout offset"));
2268 }
2269 Ok(Some((output_shape, offsets)))
2270}
2271
2272fn plan_repeated_axis_lowering(
2273 dims: &[usize],
2274 axes: &[ExpandedAxis<'_>],
2275 original_contiguous: bool,
2276 operand_index: usize,
2277) -> Result<RepeatedAxisLoweringPlan> {
2278 validate_repeated_extents(dims, axes, operand_index)?;
2279 if !original_contiguous
2280 || sequential_adjacency_is_always_identity(axes)
2281 || sequential_diagonal_would_materialize(dims, axes) != Some(true)
2282 {
2283 return Ok(RepeatedAxisLoweringPlan::Sequential);
2284 }
2285 let Some((output_shape, offsets)) = original_flat_gather_offsets(dims, axes, operand_index)?
2286 else {
2287 return Ok(RepeatedAxisLoweringPlan::Sequential);
2288 };
2289 Ok(RepeatedAxisLoweringPlan::OriginalFlatGather {
2290 output_shape,
2291 offsets,
2292 })
2293}
2294
2295fn original_flat_diagonal_gather(
2296 operand: &Tensor,
2297 output_shape: &[usize],
2298 offsets: Vec<u32>,
2299 operand_index: usize,
2300) -> Result<Tensor> {
2301 let index_count = offsets.len();
2302 let indices = Tensor::from_vec(offsets, index_count, operand.device()).map_err(|error| {
2303 error.context(format!(
2304 "einsum operand {operand_index} device-local combined diagonal indices"
2305 ))
2306 })?;
2307 operand
2308 .flatten_all()
2309 .map_err(|error| {
2310 error.context(format!(
2311 "einsum operand {operand_index} original-layout diagonal flatten"
2312 ))
2313 })?
2314 .index_select(&indices, 0)
2315 .map_err(|error| {
2316 error.context(format!(
2317 "einsum operand {operand_index} differentiable combined diagonal selection"
2318 ))
2319 })?
2320 .reshape(output_shape)
2321 .map_err(|error| {
2322 error.context(format!(
2323 "einsum operand {operand_index} combined diagonal reshape"
2324 ))
2325 })
2326}
2327
2328fn normalize_repeated_axes<'a>(
2329 mut operand: Tensor,
2330 mut axes: Vec<ExpandedAxis<'a>>,
2331 operand_index: usize,
2332) -> Result<(Tensor, Vec<ExpandedAxis<'a>>)> {
2333 match plan_repeated_axis_lowering(
2334 operand.dims(),
2335 &axes,
2336 operand.is_contiguous(),
2337 operand_index,
2338 )? {
2339 RepeatedAxisLoweringPlan::Sequential => {}
2340 RepeatedAxisLoweringPlan::OriginalFlatGather {
2341 output_shape,
2342 offsets,
2343 } => {
2344 let mut unique_axes = Vec::new();
2345 for axis in axes {
2346 if !unique_axes.contains(&axis) {
2347 unique_axes.push(axis);
2348 }
2349 }
2350 return Ok((
2351 original_flat_diagonal_gather(&operand, &output_shape, offsets, operand_index)?,
2352 unique_axes,
2353 ));
2354 }
2355 }
2356 loop {
2357 let Some(repeated_axis) = axes
2358 .iter()
2359 .copied()
2360 .find(|axis| axes.iter().filter(|candidate| **candidate == *axis).count() > 1)
2361 else {
2362 return Ok((operand, axes));
2363 };
2364 let positions = axes
2365 .iter()
2366 .enumerate()
2367 .filter_map(|(position, axis)| (*axis == repeated_axis).then_some(position))
2368 .collect::<Vec<_>>();
2369 let extent = operand.dims()[positions[0]];
2370
2371 let other_positions = (0..axes.len())
2372 .filter(|position| !positions.contains(position))
2373 .collect::<Vec<_>>();
2374 let adjacency_permutation = positions
2375 .iter()
2376 .chain(&other_positions)
2377 .copied()
2378 .collect::<Vec<_>>();
2379 let device = operand.device().clone();
2380 let adjacent = if adjacency_permutation.iter().copied().eq(0..axes.len()) {
2381 operand
2382 } else {
2383 operand
2384 .permute(adjacency_permutation.as_slice())
2385 .map_err(|error| {
2386 error.context(format!(
2387 "einsum operand {operand_index} diagonal adjacency permutation"
2388 ))
2389 })?
2390 };
2391
2392 let (repeated_flat_extent, diagonal_stride) =
2393 checked_diagonal_layout(extent, positions.len(), operand_index)?;
2394 let mut flattened_shape = vec![repeated_flat_extent];
2395 flattened_shape.extend_from_slice(&adjacent.dims()[positions.len()..]);
2396 let flattened = adjacent.reshape(flattened_shape).map_err(|error| {
2397 error.context(format!(
2398 "einsum operand {operand_index} repeated-axis flatten"
2399 ))
2400 })?;
2401
2402 let mut diagonal_indices = Vec::with_capacity(extent);
2403 for coordinate in 0..extent {
2404 let index = coordinate.checked_mul(diagonal_stride).ok_or_else(|| {
2405 candle_core::Error::msg(format!(
2406 "einsum operand {operand_index} diagonal index overflows usize"
2407 ))
2408 })?;
2409 diagonal_indices.push(u32::try_from(index).map_err(|_| {
2410 candle_core::Error::msg(format!(
2411 "einsum operand {operand_index} diagonal index {index} exceeds u32"
2412 ))
2413 })?);
2414 }
2415 let indices = Tensor::from_vec(diagonal_indices, extent, &device).map_err(|error| {
2416 error.context(format!(
2417 "einsum operand {operand_index} device-local diagonal indices"
2418 ))
2419 })?;
2420 let selected = flattened.index_select(&indices, 0).map_err(|error| {
2421 error.context(format!(
2422 "einsum operand {operand_index} differentiable diagonal selection"
2423 ))
2424 })?;
2425
2426 let result_order = std::iter::once(positions[0])
2427 .chain(other_positions.iter().copied())
2428 .collect::<Vec<_>>();
2429 let desired_order = (0..axes.len())
2430 .filter(|position| !positions[1..].contains(position))
2431 .collect::<Vec<_>>();
2432 let restoration = desired_order
2433 .iter()
2434 .map(|position| {
2435 result_order
2436 .iter()
2437 .position(|candidate| candidate == position)
2438 .expect("diagonal result order contains every surviving axis")
2439 })
2440 .collect::<Vec<_>>();
2441 operand = if restoration.iter().copied().eq(0..restoration.len()) {
2442 selected
2443 } else {
2444 selected.permute(restoration.as_slice()).map_err(|error| {
2445 error.context(format!(
2446 "einsum operand {operand_index} diagonal axis restoration"
2447 ))
2448 })?
2449 };
2450 axes = desired_order
2451 .iter()
2452 .map(|&position| axes[position])
2453 .collect();
2454 }
2455}
2456
2457fn checked_diagonal_layout(
2458 extent: usize,
2459 multiplicity: usize,
2460 operand_index: usize,
2461) -> Result<(usize, usize)> {
2462 let mut diagonal_stride = 0_usize;
2463 let mut power = 1_usize;
2464 for _ in 0..multiplicity {
2465 diagonal_stride = diagonal_stride.checked_add(power).ok_or_else(|| {
2466 candle_core::Error::msg(format!(
2467 "einsum operand {operand_index} diagonal stride overflows usize"
2468 ))
2469 })?;
2470 power = power.checked_mul(extent).ok_or_else(|| {
2471 candle_core::Error::msg(format!(
2472 "einsum operand {operand_index} repeated-axis extent product overflows usize"
2473 ))
2474 })?;
2475 }
2476 Ok((power, diagonal_stride))
2477}
2478
2479fn expand_axis_pattern<'a>(
2480 pattern: EinsumAxisPattern<'a>,
2481 ellipsis_rank: usize,
2482 implicit_operand_ellipsis: bool,
2483) -> Vec<ExpandedAxis<'a>> {
2484 let synthetic = (0..ellipsis_rank).map(ExpandedAxis::Ellipsis);
2485 match pattern.ellipsis_position {
2486 Some(position) => pattern.labels[..position]
2487 .iter()
2488 .copied()
2489 .map(ExpandedAxis::Named)
2490 .chain(synthetic)
2491 .chain(
2492 pattern.labels[position..]
2493 .iter()
2494 .copied()
2495 .map(ExpandedAxis::Named),
2496 )
2497 .collect(),
2498 None if implicit_operand_ellipsis => synthetic
2499 .chain(pattern.labels.iter().copied().map(ExpandedAxis::Named))
2500 .collect(),
2501 None => pattern
2502 .labels
2503 .iter()
2504 .copied()
2505 .map(ExpandedAxis::Named)
2506 .collect(),
2507 }
2508}
2509
2510fn validate_expanded_output(
2511 inputs: &[&[ExpandedAxis<'_>]],
2512 output: &[ExpandedAxis<'_>],
2513) -> Result<()> {
2514 for axis in output {
2515 if !inputs.iter().any(|input| input.contains(axis)) {
2516 candle_core::bail!(
2517 "invalid ellipsis einsum plan: output axis `{}` does not occur in an input",
2518 axis.display_name()
2519 )
2520 }
2521 }
2522 Ok(())
2523}
2524
2525struct ExpandedBinaryPlan<'a> {
2526 batch: Vec<ExpandedAxis<'a>>,
2527 left_free: Vec<ExpandedAxis<'a>>,
2528 contracted: Vec<ExpandedAxis<'a>>,
2529 right_free: Vec<ExpandedAxis<'a>>,
2530 left_reductions: Vec<usize>,
2531 right_reductions: Vec<usize>,
2532 left_permutation: Vec<usize>,
2533 right_permutation: Vec<usize>,
2534 output_permutation: Vec<usize>,
2535}
2536
2537struct ExpandedBinaryGraph<'a> {
2538 plan: ExpandedBinaryPlan<'a>,
2539 work: u128,
2540 output_elements: u128,
2541 copy_bytes: u128,
2542 submissions: u128,
2543 output_layout: NaryLayoutEstimate,
2544}
2545
2546impl ExpandedBinaryGraph<'_> {
2547 fn cost(&self) -> NaryPairCost {
2548 NaryPairCost {
2549 flops: self.work,
2550 output_elements: self.output_elements,
2551 copy_bytes: self.copy_bytes,
2552 submissions: self.submissions,
2553 }
2554 }
2555}
2556
2557#[derive(Clone, Copy)]
2558enum BinaryOperandLayout<'a> {
2559 Contiguous,
2560 Strided(&'a [usize]),
2561 Unsupported,
2562}
2563
2564#[derive(Clone, Copy)]
2565struct BinaryGraphOperand<'axes, 'labels> {
2566 axes: &'axes [ExpandedAxis<'labels>],
2567 dims: &'axes [usize],
2568 layout: BinaryOperandLayout<'axes>,
2569}
2570
2571fn classify_expanded_binary_graph<'a>(
2572 left: BinaryGraphOperand<'_, 'a>,
2573 right: BinaryGraphOperand<'_, 'a>,
2574 output_axes: &[ExpandedAxis<'a>],
2575 element_bytes: usize,
2576) -> Result<ExpandedBinaryGraph<'a>> {
2577 let BinaryGraphOperand {
2578 axes: left_axes,
2579 dims: left_dims,
2580 layout: left_layout,
2581 } = left;
2582 let BinaryGraphOperand {
2583 axes: right_axes,
2584 dims: right_dims,
2585 layout: right_layout,
2586 } = right;
2587 if left_axes.len() != left_dims.len() || right_axes.len() != right_dims.len() {
2588 candle_core::bail!("binary graph axes and dimensions have different ranks")
2589 }
2590 let plan = classify_expanded_binary(left_axes, right_axes, output_axes);
2591 let extent = |axis: ExpandedAxis<'a>| -> Result<usize> {
2592 let left = left_axes
2593 .iter()
2594 .position(|candidate| *candidate == axis)
2595 .map(|index| left_dims[index]);
2596 let right = right_axes
2597 .iter()
2598 .position(|candidate| *candidate == axis)
2599 .map(|index| right_dims[index]);
2600 match (left, right) {
2601 (Some(left), Some(right)) => resolve_extent(&axis.display_name(), left, right),
2602 (Some(extent), None) | (None, Some(extent)) => Ok(extent),
2603 (None, None) => candle_core::bail!("binary graph output axis is absent from inputs"),
2604 }
2605 };
2606 let product = |axes: &[ExpandedAxis<'a>]| -> Result<u128> {
2607 let mut value = 1_u128;
2608 for &axis in axes {
2609 let axis = extent(axis)? as u128;
2610 if axis == 0 {
2611 return Ok(0);
2612 }
2613 value = value
2614 .checked_mul(axis)
2615 .ok_or_else(|| candle_core::Error::msg("n-ary dimension product overflows u128"))?;
2616 }
2617 Ok(value)
2618 };
2619 let output_elements = product(output_axes)?;
2620 let left_reduction_work = if plan.left_reductions.is_empty() {
2621 0
2622 } else {
2623 checked_nary_product(left_dims)?
2624 };
2625 let right_reduction_work = if plan.right_reductions.is_empty() {
2626 0
2627 } else {
2628 checked_nary_product(right_dims)?
2629 };
2630 let reduction_submissions = u128::from(!plan.left_reductions.is_empty())
2631 + u128::from(!plan.right_reductions.is_empty());
2632 let canonical_axes = plan
2633 .batch
2634 .iter()
2635 .chain(&plan.left_free)
2636 .chain(&plan.contracted)
2637 .chain(&plan.right_free)
2638 .copied()
2639 .collect::<Vec<_>>();
2640 let core_work = if plan.contracted.is_empty() {
2641 output_elements
2642 } else {
2643 product(&canonical_axes)?
2644 };
2645 let mut zero_core = false;
2646 if !plan.contracted.is_empty() {
2647 for &axis in &canonical_axes {
2648 zero_core |= extent(axis)? == 0;
2649 }
2650 }
2651
2652 let contiguous_strides = |dims: &[usize]| -> Result<Vec<usize>> {
2653 let mut strides = vec![0; dims.len()];
2654 let mut stride = 1usize;
2655 for (axis, &dimension) in dims.iter().enumerate().rev() {
2656 strides[axis] = stride;
2657 stride = stride.checked_mul(dimension).ok_or_else(|| {
2658 candle_core::Error::msg("binary graph stride estimate overflows usize")
2659 })?;
2660 }
2661 Ok(strides)
2662 };
2663 let materialized_bytes = |axes: &[ExpandedAxis<'a>],
2664 dims: &[usize],
2665 layout: BinaryOperandLayout<'_>,
2666 reductions: &[usize],
2667 permutation: &[usize],
2668 group_lengths: &[usize]|
2669 -> Result<u128> {
2670 let mut broadcasted = false;
2671 let mut target_elements = 1_u128;
2672 let mut remaining_dims = Vec::with_capacity(axes.len() - reductions.len());
2673 for (index, (&axis, &dimension)) in axes.iter().zip(dims).enumerate() {
2674 if reductions.contains(&index) {
2675 continue;
2676 }
2677 let resolved = extent(axis)?;
2678 broadcasted |= resolved != dimension;
2679 if resolved == 0 {
2680 target_elements = 0;
2681 } else if target_elements != 0 {
2682 target_elements =
2683 target_elements
2684 .checked_mul(resolved as u128)
2685 .ok_or_else(|| {
2686 candle_core::Error::msg("binary graph copy estimate overflows u128")
2687 })?;
2688 }
2689 remaining_dims.push(dimension);
2690 }
2691 let target_bytes = || {
2692 target_elements
2693 .checked_mul(element_bytes as u128)
2694 .ok_or_else(|| candle_core::Error::msg("binary graph copy estimate overflows u128"))
2695 };
2696 if broadcasted {
2697 return target_bytes();
2698 }
2699 let naturally_contiguous =
2700 !reductions.is_empty() || matches!(layout, BinaryOperandLayout::Contiguous);
2701 if naturally_contiguous && permutation.iter().copied().eq(0..permutation.len()) {
2702 return Ok(0);
2703 }
2704 if naturally_contiguous && group_lengths.iter().all(|&length| length <= 1) {
2705 return Ok(0);
2706 }
2707 let strides = if reductions.is_empty() {
2708 match layout {
2709 BinaryOperandLayout::Contiguous => contiguous_strides(dims)?,
2710 BinaryOperandLayout::Strided(strides) if strides.len() == dims.len() => {
2711 strides.to_vec()
2712 }
2713 BinaryOperandLayout::Strided(_) => {
2714 candle_core::bail!("binary graph dimensions and strides have different ranks")
2715 }
2716 BinaryOperandLayout::Unsupported => return target_bytes(),
2717 }
2718 } else {
2719 contiguous_strides(&remaining_dims)?
2720 };
2721 let canonical_dims = permutation
2722 .iter()
2723 .map(|&axis| remaining_dims[axis])
2724 .collect::<Vec<_>>();
2725 let canonical_strides = permutation
2726 .iter()
2727 .map(|&axis| strides[axis])
2728 .collect::<Vec<_>>();
2729 let nonempty_groups = group_lengths
2730 .iter()
2731 .copied()
2732 .filter(|&length| length != 0)
2733 .collect::<Vec<_>>();
2734 if nonempty_groups.is_empty()
2735 || crate::backend::plan_permute_compose_group_order(
2736 &canonical_dims,
2737 &canonical_strides,
2738 &(0..canonical_dims.len()).collect::<Vec<_>>(),
2739 &nonempty_groups,
2740 )?
2741 .is_some()
2742 {
2743 return Ok(0);
2744 }
2745 target_bytes()
2746 };
2747 let copy_bytes = if plan.contracted.is_empty() || zero_core {
2748 0
2749 } else {
2750 materialized_bytes(
2751 left_axes,
2752 left_dims,
2753 left_layout,
2754 &plan.left_reductions,
2755 &plan.left_permutation,
2756 &[
2757 plan.batch.len(),
2758 plan.left_free.len(),
2759 plan.contracted.len(),
2760 ],
2761 )?
2762 .checked_add(materialized_bytes(
2763 right_axes,
2764 right_dims,
2765 right_layout,
2766 &plan.right_reductions,
2767 &plan.right_permutation,
2768 &[
2769 plan.batch.len(),
2770 plan.contracted.len(),
2771 plan.right_free.len(),
2772 ],
2773 )?)
2774 .ok_or_else(|| candle_core::Error::msg("binary graph copy estimate overflows u128"))?
2775 };
2776 let work = left_reduction_work
2777 .checked_add(right_reduction_work)
2778 .and_then(|work| work.checked_add(if zero_core { 0 } else { core_work }))
2779 .ok_or_else(|| candle_core::Error::msg("binary graph work estimate overflows u128"))?;
2780 Ok(ExpandedBinaryGraph {
2781 plan,
2782 work,
2783 output_elements,
2784 copy_bytes,
2785 submissions: reduction_submissions + if zero_core { 2 } else { 1 },
2786 output_layout: if zero_core {
2787 NaryLayoutEstimate::Unsupported
2788 } else {
2789 NaryLayoutEstimate::Contiguous
2790 },
2791 })
2792}
2793
2794fn classify_expanded_binary<'a>(
2795 left: &[ExpandedAxis<'a>],
2796 right: &[ExpandedAxis<'a>],
2797 output: &[ExpandedAxis<'a>],
2798) -> ExpandedBinaryPlan<'a> {
2799 let mut all = Vec::new();
2800 for axis in left.iter().chain(right) {
2801 if !all.contains(axis) {
2802 all.push(*axis);
2803 }
2804 }
2805 let batch = all
2806 .iter()
2807 .copied()
2808 .filter(|axis| left.contains(axis) && right.contains(axis) && output.contains(axis))
2809 .collect::<Vec<_>>();
2810 let left_free = all
2811 .iter()
2812 .copied()
2813 .filter(|axis| left.contains(axis) && !right.contains(axis) && output.contains(axis))
2814 .collect::<Vec<_>>();
2815 let contracted = all
2816 .iter()
2817 .copied()
2818 .filter(|axis| left.contains(axis) && right.contains(axis) && !output.contains(axis))
2819 .collect::<Vec<_>>();
2820 let right_free = all
2821 .iter()
2822 .copied()
2823 .filter(|axis| !left.contains(axis) && right.contains(axis) && output.contains(axis))
2824 .collect::<Vec<_>>();
2825 let left_reductions = left
2826 .iter()
2827 .enumerate()
2828 .filter_map(|(index, axis)| {
2829 (!right.contains(axis) && !output.contains(axis)).then_some(index)
2830 })
2831 .collect();
2832 let right_reductions = right
2833 .iter()
2834 .enumerate()
2835 .filter_map(|(index, axis)| {
2836 (!left.contains(axis) && !output.contains(axis)).then_some(index)
2837 })
2838 .collect();
2839 let left_remaining = left
2840 .iter()
2841 .copied()
2842 .filter(|axis| right.contains(axis) || output.contains(axis))
2843 .collect::<Vec<_>>();
2844 let right_remaining = right
2845 .iter()
2846 .copied()
2847 .filter(|axis| left.contains(axis) || output.contains(axis))
2848 .collect::<Vec<_>>();
2849 let left_canonical = batch
2850 .iter()
2851 .chain(&left_free)
2852 .chain(&contracted)
2853 .copied()
2854 .collect::<Vec<_>>();
2855 let right_canonical = batch
2856 .iter()
2857 .chain(&contracted)
2858 .chain(&right_free)
2859 .copied()
2860 .collect::<Vec<_>>();
2861 let canonical_output = batch
2862 .iter()
2863 .chain(&left_free)
2864 .chain(&right_free)
2865 .copied()
2866 .collect::<Vec<_>>();
2867 ExpandedBinaryPlan {
2868 batch,
2869 left_free,
2870 contracted,
2871 right_free,
2872 left_reductions,
2873 right_reductions,
2874 left_permutation: dynamic_permutation(&left_remaining, &left_canonical),
2875 right_permutation: dynamic_permutation(&right_remaining, &right_canonical),
2876 output_permutation: dynamic_permutation(&canonical_output, output),
2877 }
2878}
2879
2880fn dynamic_permutation<T: Eq>(current: &[T], desired: &[T]) -> Vec<usize> {
2881 desired
2882 .iter()
2883 .map(|axis| {
2884 current
2885 .iter()
2886 .position(|candidate| candidate == axis)
2887 .expect("validated ellipsis classification")
2888 })
2889 .collect()
2890}
2891
2892fn materialize_broadcast_if_needed(
2893 tensor: &Tensor,
2894 shape: &[usize],
2895 context: &'static str,
2896) -> Result<Tensor> {
2897 if tensor.dims() == shape {
2898 Ok(tensor.clone())
2899 } else {
2900 tensor
2901 .broadcast_as(shape)
2902 .and_then(|tensor| tensor.contiguous())
2903 .map_err(|error| error.context(context))
2904 }
2905}
2906
2907fn pack_canonical_operand(
2908 tensor: &Tensor,
2909 packed_shape: &[usize],
2910 group_lengths: &[usize],
2911 context: &'static str,
2912) -> Result<Tensor> {
2913 if packed_shape.len() != group_lengths.len() {
2914 candle_core::bail!(
2915 "{context}: packed rank {} does not match group rank {}",
2916 packed_shape.len(),
2917 group_lengths.len()
2918 )
2919 }
2920
2921 let materialized;
2922 let tensor = if tensor
2923 .dims()
2924 .iter()
2925 .zip(tensor.stride())
2926 .any(|(&extent, &stride)| extent > 1 && stride == 0)
2927 {
2928 materialized = tensor
2929 .contiguous()
2930 .map_err(|error| error.context(context))?;
2931 &materialized
2932 } else {
2933 tensor
2934 };
2935
2936 let mut nonempty_shape = Vec::with_capacity(packed_shape.len());
2937 let mut nonempty_lengths = Vec::with_capacity(group_lengths.len());
2938 for (&extent, &length) in packed_shape.iter().zip(group_lengths) {
2939 if length == 0 {
2940 if extent != 1 {
2941 candle_core::bail!("{context}: an empty axis group must have extent one")
2942 }
2943 } else {
2944 nonempty_shape.push(extent);
2945 nonempty_lengths.push(length);
2946 }
2947 }
2948
2949 let mut output = if nonempty_lengths.is_empty() {
2950 tensor.clone()
2951 } else {
2952 let identity = (0..tensor.rank()).collect::<Vec<_>>();
2953 execute_tensor_permute_and_compose(tensor, &identity, &nonempty_shape, &nonempty_lengths)
2954 .map_err(|error| error.context(context))?
2955 };
2956 for (axis, &length) in group_lengths.iter().enumerate() {
2957 if length == 0 {
2958 output = output
2959 .unsqueeze(axis)
2960 .map_err(|error| error.context(context))?;
2961 }
2962 }
2963 Ok(output)
2964}
2965
2966#[cfg(feature = "benchmark-internals")]
2968#[doc(hidden)]
2969pub fn benchmark_pack_canonical_operand(
2970 tensor: &Tensor,
2971 packed_shape: &[usize],
2972 group_lengths: &[usize],
2973) -> Result<Tensor> {
2974 pack_canonical_operand(
2975 tensor,
2976 packed_shape,
2977 group_lengths,
2978 "benchmark canonical operand packing",
2979 )
2980}
2981
2982fn apply_output_permutation(output: Tensor, permutation: &[usize]) -> Result<Tensor> {
2983 if permutation.iter().copied().eq(0..permutation.len()) {
2984 Ok(output)
2985 } else {
2986 output
2987 .permute(permutation)
2988 .map_err(|error| error.context("einsum binary explicit output permutation"))
2989 }
2990}
2991
2992fn prepare_operand(
2993 operand: &Tensor,
2994 operand_index: usize,
2995 input_rank: usize,
2996 reduction_axes: &[usize],
2997 permutation: &[usize],
2998) -> Result<Tensor> {
2999 let mut reduced = vec![false; input_rank];
3000 for &axis in reduction_axes {
3001 if axis >= input_rank {
3002 candle_core::bail!(
3003 "invalid binary einsum plan: operand {operand_index} reduction axis {axis} is out of range for rank {input_rank}"
3004 )
3005 }
3006 if std::mem::replace(&mut reduced[axis], true) {
3007 candle_core::bail!(
3008 "invalid binary einsum plan: operand {operand_index} reduction axis {axis} occurs more than once"
3009 )
3010 }
3011 }
3012 let remaining_rank = input_rank - reduction_axes.len();
3013 validate_permutation(permutation, remaining_rank, "operand")?;
3014 let operand = if reduction_axes.is_empty() {
3015 operand.clone()
3016 } else {
3017 operand.sum(reduction_axes).map_err(|error| {
3018 error.context(format!("einsum operand {operand_index} pre-reduction"))
3019 })?
3020 };
3021 if permutation.iter().copied().eq(0..remaining_rank) {
3022 Ok(operand)
3023 } else {
3024 operand.permute(permutation).map_err(|error| {
3025 error.context(format!(
3026 "einsum operand {operand_index} canonical permutation"
3027 ))
3028 })
3029 }
3030}
3031
3032fn validate_permutation(permutation: &[usize], rank: usize, context: &str) -> Result<()> {
3033 if permutation.len() != rank {
3034 candle_core::bail!(
3035 "invalid binary einsum plan: {context} permutation has {} axes, expected {rank}",
3036 permutation.len(),
3037 )
3038 }
3039 let mut seen = vec![false; rank];
3040 for &axis in permutation {
3041 if axis >= rank || std::mem::replace(&mut seen[axis], true) {
3042 candle_core::bail!(
3043 "invalid binary einsum plan: {context} permutation is not a permutation of 0..{rank}"
3044 )
3045 }
3046 }
3047 Ok(())
3048}
3049
3050fn resolve_extent(label: &str, left: usize, right: usize) -> Result<usize> {
3051 match (left, right) {
3052 (left, right) if left == right => Ok(left),
3053 (1, right) => Ok(right),
3054 (left, 1) => Ok(left),
3055 _ => {
3056 candle_core::bail!("einsum label `{label}` cannot broadcast extents {left} and {right}")
3057 }
3058 }
3059}
3060
3061fn checked_product(dimensions: &[usize], category: &str) -> Result<usize> {
3062 dimensions.iter().try_fold(1_usize, |product, &extent| {
3063 product.checked_mul(extent).ok_or_else(|| {
3064 candle_core::Error::msg(format!(
3065 "einsum binary {category} flattened extent overflows usize"
3066 ))
3067 })
3068 })
3069}
3070
3071#[cfg(test)]
3072mod tests {
3073 use super::*;
3074 use candle_core::{DType, Device, Storage, Var};
3075
3076 fn storage_address(tensor: &Tensor) -> *const Storage {
3077 let (storage, _) = tensor.storage_and_layout();
3078 std::ptr::from_ref(&*storage)
3079 }
3080
3081 #[test]
3082 fn canonical_group_packing_uses_the_calibrated_cpu_rank_two_layout() -> Result<()> {
3083 let source = Tensor::arange(0f32, 24., &Device::Cpu)?.reshape((4, 2, 3))?;
3084 let canonical = source.permute((1, 2, 0))?;
3085 assert!(!canonical.is_contiguous());
3086 let historical = canonical.reshape((1, 6, 4))?;
3087 assert_ne!(storage_address(&historical), storage_address(&source));
3088
3089 let packed =
3090 pack_canonical_operand(&canonical, &[1, 6, 4], &[0, 2, 1], "test canonical operand")?;
3091 assert_eq!(packed.dims(), [1, 6, 4]);
3092 assert_ne!(storage_address(&packed), storage_address(&source));
3093 assert!(packed.is_contiguous());
3094 assert_eq!(
3095 packed.flatten_all()?.to_vec1::<f32>()?,
3096 historical.flatten_all()?.to_vec1::<f32>()?
3097 );
3098 Ok(())
3099 }
3100
3101 #[test]
3102 fn canonical_group_packing_preserves_broadcast_and_reduction_order() -> Result<()> {
3103 let left = Tensor::arange(1f32, 13., &Device::Cpu)?
3104 .reshape((2, 1, 2, 3))?
3105 .sum(0)?
3106 .broadcast_as((2, 2, 3))?;
3107 let right = Tensor::ones((2, 3, 4, 2), DType::F32, &Device::Cpu)?.sum(3)?;
3108 for (tensor, shape) in [(&left, [2, 2, 3]), (&right, [2, 3, 4])] {
3109 let historical = tensor.reshape(&shape)?;
3110 let packed = pack_canonical_operand(tensor, &shape, &[1, 1, 1], "test packing")?;
3111 assert_eq!(
3112 packed.flatten_all()?.to_vec1::<f32>()?,
3113 historical.flatten_all()?.to_vec1::<f32>()?,
3114 "dims={:?} strides={:?}",
3115 tensor.dims(),
3116 tensor.stride()
3117 );
3118 }
3119 let historical = left
3120 .reshape((2, 2, 3))?
3121 .matmul(&right.reshape((2, 3, 4))?)?;
3122 let packed_left = pack_canonical_operand(&left, &[2, 2, 3], &[1, 1, 1], "test left")?;
3123 let packed_right = pack_canonical_operand(&right, &[2, 3, 4], &[1, 1, 1], "test right")?;
3124 assert!(
3125 packed_left.is_contiguous(),
3126 "left strides {:?}",
3127 packed_left.stride()
3128 );
3129 assert!(
3130 packed_right.is_contiguous(),
3131 "right strides {:?}",
3132 packed_right.stride()
3133 );
3134 let packed = packed_left.matmul(&packed_right)?;
3135 assert_eq!(
3136 packed.flatten_all()?.to_vec1::<f32>()?,
3137 historical.flatten_all()?.to_vec1::<f32>()?
3138 );
3139 Ok(())
3140 }
3141
3142 #[test]
3143 fn rejects_invalid_runtime_specs_without_panicking() -> Result<()> {
3144 let input = Tensor::zeros((2, 3), DType::F32, &Device::Cpu)?;
3145
3146 assert!(execute_unary_einsum(&input, UnaryEinsumSpec::new(3, 2, &[0, 1, 2])).is_err());
3147 assert!(execute_unary_einsum(&input, UnaryEinsumSpec::new(2, 3, &[0, 1])).is_err());
3148 assert!(execute_unary_einsum(&input, UnaryEinsumSpec::new(2, 2, &[0])).is_err());
3149 assert!(execute_unary_einsum(&input, UnaryEinsumSpec::new(2, 2, &[0, 2])).is_err());
3150 assert!(execute_unary_einsum(&input, UnaryEinsumSpec::new(2, 2, &[0, 0])).is_err());
3151
3152 Ok(())
3153 }
3154
3155 #[test]
3156 fn rejects_invalid_binary_specs_and_checked_shape_overflow() -> Result<()> {
3157 let left = Tensor::zeros((2, 3), DType::F32, &Device::Cpu)?;
3158 let right = Tensor::zeros((3, 4), DType::F32, &Device::Cpu)?;
3159 let valid = BinaryEinsumSpec::new(
3160 [2, 2],
3161 [&[], &[]],
3162 [&[0, 1], &[0, 1]],
3163 0,
3164 1,
3165 1,
3166 1,
3167 &[],
3168 &["inner"],
3169 &[0, 1],
3170 );
3171 assert_eq!(execute_binary_einsum(&left, &right, valid)?.dims(), &[2, 4]);
3172
3173 let invalid = BinaryEinsumSpec::new(
3174 [2, 2],
3175 [&[], &[]],
3176 [&[0, 0], &[0, 1]],
3177 0,
3178 1,
3179 1,
3180 1,
3181 &[],
3182 &["inner"],
3183 &[0, 1],
3184 );
3185 assert!(execute_binary_einsum(&left, &right, invalid).is_err());
3186 assert!(checked_product(&[usize::MAX, 2], "test").is_err());
3187 Ok(())
3188 }
3189
3190 #[test]
3191 fn ellipsis_runtime_validates_rank_and_normalizes_right_alignment() -> Result<()> {
3192 let vector = Tensor::zeros(1, DType::F32, &Device::Cpu)?;
3193 let invalid_operands = [EinsumAxisPattern::new(&["row", "inner"], Some(0))];
3194 let invalid =
3195 EllipsisEinsumSpec::new(&invalid_operands, EinsumAxisPattern::new(&["row"], Some(0)));
3196 assert!(execute_unary_ellipsis_einsum(&vector, invalid).is_err());
3197
3198 let left = Tensor::ones((2, 1, 2, 3), DType::F32, &Device::Cpu)?;
3199 let right = Tensor::ones((4, 3, 2), DType::F32, &Device::Cpu)?;
3200 let valid_operands = [
3201 EinsumAxisPattern::new(&["row", "inner"], Some(0)),
3202 EinsumAxisPattern::new(&["inner", "column"], Some(0)),
3203 ];
3204 let valid = EllipsisEinsumSpec::new(
3205 &valid_operands,
3206 EinsumAxisPattern::new(&["row", "column"], Some(1)),
3207 );
3208 assert_eq!(
3209 execute_binary_ellipsis_einsum(&left, &right, valid)?.dims(),
3210 &[2, 2, 4, 2]
3211 );
3212 Ok(())
3213 }
3214
3215 #[test]
3216 fn diagonal_layout_arithmetic_is_checked() {
3217 assert!(checked_diagonal_layout(usize::MAX, 2, 0).is_err());
3218 assert_eq!(checked_diagonal_layout(3, 3, 0).unwrap(), (27, 13));
3219 assert_eq!(checked_diagonal_layout(0, 3, 0).unwrap(), (0, 1));
3220 }
3221
3222 fn named_axes(labels: &[&'static str]) -> Vec<ExpandedAxis<'static>> {
3223 labels.iter().copied().map(ExpandedAxis::Named).collect()
3224 }
3225
3226 #[test]
3227 fn diagonal_plan_selects_only_when_sequential_flatten_would_copy() -> Result<()> {
3228 assert_eq!(
3229 plan_repeated_axis_lowering(&[3, 3], &named_axes(&["i", "i"]), true, 0)?,
3230 RepeatedAxisLoweringPlan::Sequential
3231 );
3232 assert_eq!(
3233 plan_repeated_axis_lowering(&[2, 3, 3], &named_axes(&["batch", "i", "i"]), true, 0,)?,
3234 RepeatedAxisLoweringPlan::OriginalFlatGather {
3235 output_shape: vec![2, 3],
3236 offsets: vec![0, 4, 8, 9, 13, 17],
3237 }
3238 );
3239 assert_eq!(
3240 plan_repeated_axis_lowering(
3241 &[2, 3, 2, 3],
3242 &named_axes(&["i", "j", "i", "j"]),
3243 true,
3244 0,
3245 )?,
3246 RepeatedAxisLoweringPlan::OriginalFlatGather {
3247 output_shape: vec![2, 3],
3248 offsets: vec![0, 7, 14, 21, 28, 35],
3249 }
3250 );
3251 Ok(())
3252 }
3253
3254 #[test]
3255 fn diagonal_plan_preserves_fallbacks_and_checks_zero_before_offsets() -> Result<()> {
3256 assert_eq!(
3257 plan_repeated_axis_lowering(
3258 &[2, 3, 2, 3],
3259 &named_axes(&["i", "j", "i", "j"]),
3260 false,
3261 0,
3262 )?,
3263 RepeatedAxisLoweringPlan::Sequential
3264 );
3265 assert_eq!(
3266 plan_repeated_axis_lowering(
3267 &[2, 65_536, 65_536],
3268 &named_axes(&["batch", "i", "i"]),
3269 true,
3270 0,
3271 )?,
3272 RepeatedAxisLoweringPlan::Sequential
3273 );
3274 assert_eq!(
3275 plan_repeated_axis_lowering(
3276 &[256, 256, 256, 256],
3277 &named_axes(&["i", "j", "i", "j"]),
3278 true,
3279 0,
3280 )?,
3281 RepeatedAxisLoweringPlan::Sequential,
3282 "u32::MAX is Candle's zero-fill index sentinel"
3283 );
3284 assert_eq!(
3285 plan_repeated_axis_lowering(&[2, 0, 0], &named_axes(&["batch", "i", "i"]), true, 0,)?,
3286 RepeatedAxisLoweringPlan::OriginalFlatGather {
3287 output_shape: vec![2, 0],
3288 offsets: vec![],
3289 }
3290 );
3291 Ok(())
3292 }
3293
3294 #[test]
3295 fn diagonal_plan_validates_every_repeated_extent_before_arithmetic() {
3296 let error = plan_repeated_axis_lowering(
3297 &[usize::MAX, usize::MAX, 2, 3],
3298 &named_axes(&["i", "i", "j", "j"]),
3299 true,
3300 0,
3301 )
3302 .expect_err("the later unequal group must be rejected first");
3303 assert!(error.to_string().contains("repeated label `j`"));
3304 assert!(error.to_string().contains("unequal extents 2 and 3"));
3305 }
3306
3307 #[test]
3308 fn nary_planner_avoids_a_pathological_left_to_right_intermediate() -> Result<()> {
3309 let operands = vec![
3310 PlannedOperand::new_for_test((100, 2), &["a", "b"], 0)?,
3311 PlannedOperand::new_for_test((2, 100), &["b", "c"], 1)?,
3312 PlannedOperand::new_for_test((100, 2), &["c", "d"], 2)?,
3313 ];
3314 let output = [ExpandedAxis::Named("a"), ExpandedAxis::Named("d")];
3315 let left_to_right = estimate_pair(&operands, 0, 1, &output)?;
3316 let selected = select_nary_pair(&operands, &output)?;
3317 assert_eq!((selected.left, selected.right), (1, 2));
3318 assert_eq!(left_to_right.output_elements, 10_000);
3319 assert_eq!(selected.output_elements, 4);
3320 Ok(())
3321 }
3322
3323 #[test]
3324 fn nary_planner_breaks_equal_cost_ties_by_operand_order() -> Result<()> {
3325 let operands = vec![
3326 PlannedOperand::new_for_test((2, 2), &["a", "b"], 0)?,
3327 PlannedOperand::new_for_test((2, 2), &["b", "c"], 1)?,
3328 PlannedOperand::new_for_test((2, 2), &["c", "d"], 2)?,
3329 ];
3330 let output = [ExpandedAxis::Named("a"), ExpandedAxis::Named("d")];
3331 let selected = select_nary_pair(&operands, &output)?;
3332 assert_eq!((selected.left, selected.right), (0, 1));
3333 assert_eq!((selected.output_elements, selected.flops), (4, 8));
3334 Ok(())
3335 }
3336
3337 #[test]
3338 fn nary_cost_estimate_overflow_is_checked() {
3339 assert!(checked_nary_product(&[usize::MAX; 5]).is_err());
3340 assert_eq!(
3341 checked_nary_product(&[usize::MAX, usize::MAX, 0]).unwrap(),
3342 0
3343 );
3344 }
3345
3346 #[test]
3347 fn nary_pair_model_counts_local_reduction_and_core_submission() -> Result<()> {
3348 let state = vec![
3349 planner_meta(
3350 0,
3351 &[("m", 2), ("k", 3), ("discard", 5)],
3352 NaryLayoutEstimate::Contiguous,
3353 ),
3354 planner_meta(1, &[("k", 3), ("n", 7)], NaryLayoutEstimate::Contiguous),
3355 ];
3356 let output = [ExpandedAxis::Named("m"), ExpandedAxis::Named("n")];
3357 let order = named_axes(&["m", "k", "discard", "n"]);
3358 let (estimate, intermediate) = model_pair_details(&state, 0, 1, &output, &order)?;
3359
3360 assert_eq!(estimate.submissions, 2, "one reduction plus one GEMM");
3361 assert_eq!(estimate.flops, 30 + 42, "reduction work plus GEMM work");
3362 assert_eq!(estimate.output_elements, 14);
3363 assert_eq!(estimate.copy_bytes, 0);
3364 assert_eq!(intermediate.layout, NaryLayoutEstimate::Contiguous);
3365 Ok(())
3366 }
3367
3368 #[test]
3369 fn nary_pair_model_distinguishes_recoverable_and_copying_strides() -> Result<()> {
3370 let pair = |layout| {
3371 vec![
3372 planner_meta(0, &[("a", 2), ("b", 3), ("k", 5)], layout),
3373 planner_meta(1, &[("k", 5), ("n", 7)], NaryLayoutEstimate::Contiguous),
3374 ]
3375 };
3376 let output = named_axes(&["a", "b", "n"]);
3377 let order = named_axes(&["a", "b", "k", "n"]);
3378
3379 let (recovered, _) = model_pair_details(
3380 &pair(NaryLayoutEstimate::Strided(vec![3, 1, 6])),
3381 0,
3382 1,
3383 &output,
3384 &order,
3385 )?;
3386 let (copied, _) = model_pair_details(
3387 &pair(NaryLayoutEstimate::Strided(vec![15, 1, 3])),
3388 0,
3389 1,
3390 &output,
3391 &order,
3392 )?;
3393
3394 assert_eq!(recovered.copy_bytes, 0);
3395 assert_eq!(copied.copy_bytes, 2 * 3 * 5 * 4);
3396 Ok(())
3397 }
3398
3399 #[test]
3400 fn nary_pair_model_separates_multiply_gemm_broadcast_and_zero_graphs() -> Result<()> {
3401 let multiply = vec![
3402 planner_meta(
3403 0,
3404 &[("i", 4), ("discard", 3)],
3405 NaryLayoutEstimate::Contiguous,
3406 ),
3407 planner_meta(1, &[("j", 5)], NaryLayoutEstimate::Contiguous),
3408 ];
3409 let (multiply, multiply_output) = model_pair_details(
3410 &multiply,
3411 0,
3412 1,
3413 &named_axes(&["i", "j"]),
3414 &named_axes(&["i", "discard", "j"]),
3415 )?;
3416 assert_eq!(multiply.submissions, 2);
3417 assert_eq!(multiply.flops, 12 + 20);
3418 assert_eq!(multiply.copy_bytes, 0);
3419 assert_eq!(multiply_output.layout, NaryLayoutEstimate::Contiguous);
3420
3421 let broadcast = vec![
3422 planner_meta(
3423 0,
3424 &[("batch", 1), ("m", 4), ("k", 8)],
3425 NaryLayoutEstimate::Contiguous,
3426 ),
3427 planner_meta(
3428 1,
3429 &[("batch", 16), ("k", 8), ("n", 2)],
3430 NaryLayoutEstimate::Contiguous,
3431 ),
3432 ];
3433 let output = named_axes(&["batch", "m", "n"]);
3434 let order = named_axes(&["batch", "m", "k", "n"]);
3435 let (broadcast, broadcast_output) = model_pair_details(&broadcast, 0, 1, &output, &order)?;
3436 assert_eq!(broadcast.submissions, 1);
3437 assert_eq!(broadcast.copy_bytes, 16 * 4 * 8 * 4);
3438 assert_eq!(broadcast_output.layout, NaryLayoutEstimate::Contiguous);
3439
3440 let zero = vec![
3441 planner_meta(0, &[("m", 4), ("k", 0)], NaryLayoutEstimate::Contiguous),
3442 planner_meta(1, &[("k", 0), ("n", 2)], NaryLayoutEstimate::Contiguous),
3443 ];
3444 let (zero, zero_output) = model_pair_details(
3445 &zero,
3446 0,
3447 1,
3448 &named_axes(&["m", "n"]),
3449 &named_axes(&["m", "k", "n"]),
3450 )?;
3451 assert_eq!(zero.submissions, 2, "empty cat plus one zero reduction");
3452 assert_eq!(zero.flops, 0);
3453 assert_eq!(zero.copy_bytes, 0);
3454 assert_eq!(zero_output.layout, NaryLayoutEstimate::Unsupported);
3455 Ok(())
3456 }
3457
3458 fn planner_meta(
3459 ordinal: usize,
3460 axes: &[(&'static str, usize)],
3461 layout: NaryLayoutEstimate,
3462 ) -> NaryPlannerMetadata<'static> {
3463 NaryPlannerMetadata::new_for_test(ordinal, axes, layout)
3464 }
3465
3466 fn matrix_chain_metadata(
3467 dimensions: [usize; 5],
3468 batch: Option<usize>,
3469 ) -> Vec<NaryPlannerMetadata<'static>> {
3470 let [a, b, c, d, e] = dimensions;
3471 let operand = |ordinal, axes: [(&'static str, usize); 2]| {
3472 let mut metadata = batch
3473 .map(|extent| vec![("batch", if ordinal == 0 { 1 } else { extent })])
3474 .unwrap_or_default();
3475 metadata.extend(axes);
3476 planner_meta(ordinal, &metadata, NaryLayoutEstimate::Contiguous)
3477 };
3478 vec![
3479 operand(0, [("a", a), ("b", b)]),
3480 operand(1, [("b", b), ("c", c)]),
3481 operand(2, [("c", c), ("d", d)]),
3482 operand(3, [("d", d), ("e", e)]),
3483 ]
3484 }
3485
3486 #[test]
3487 fn layout_aware_selection_freezes_counterexamples_and_every_boundary() {
3488 let balanced = matrix_chain_metadata([128, 8, 1, 8, 128], None);
3489 let selected = select_layout_aware_plan_for_test(&balanced, &["a", "e"], DType::F32, true);
3490 assert!(matches!(
3491 selected,
3492 NaryPlannerDecision::Greedy(NaryGreedyReason::Calibration)
3493 ));
3494
3495 let broadcast = matrix_chain_metadata([32, 32, 15, 5, 10], Some(32));
3496 assert!(matches!(
3497 select_layout_aware_plan_for_test(&broadcast, &["batch", "a", "e"], DType::F32, true),
3498 NaryPlannerDecision::Greedy(NaryGreedyReason::Calibration)
3499 ));
3500
3501 let linear = matrix_chain_metadata([30, 35, 15, 5, 10], None);
3502 assert!(matches!(
3503 select_layout_aware_plan_for_test(&linear, &["a", "e"], DType::F32, true),
3504 NaryPlannerDecision::Greedy(NaryGreedyReason::Calibration)
3505 ));
3506 let just_below = (0..3)
3507 .map(|ordinal| planner_meta(ordinal, &[("k", 49_999)], NaryLayoutEstimate::Contiguous))
3508 .collect::<Vec<_>>();
3509 assert!(matches!(
3510 select_layout_aware_plan_for_test(&just_below, &[], DType::F32, true),
3511 NaryPlannerDecision::Greedy(NaryGreedyReason::Calibration)
3512 ));
3513 let at_threshold = (0..3)
3514 .map(|ordinal| planner_meta(ordinal, &[("k", 50_000)], NaryLayoutEstimate::Contiguous))
3515 .collect::<Vec<_>>();
3516 assert!(matches!(
3517 select_layout_aware_plan_for_test(&at_threshold, &[], DType::F32, true),
3518 NaryPlannerDecision::Greedy(NaryGreedyReason::Calibration)
3519 ));
3520 assert!(matches!(
3521 select_layout_aware_plan_for_test(&balanced, &["a", "e"], DType::F64, true),
3522 NaryPlannerDecision::Greedy(NaryGreedyReason::DType)
3523 ));
3524 assert!(matches!(
3525 select_layout_aware_plan_for_test(&balanced, &["a", "e"], DType::F32, false),
3526 NaryPlannerDecision::Greedy(NaryGreedyReason::Backend)
3527 ));
3528 let mut unsupported = balanced.clone();
3529 unsupported[0] = planner_meta(0, &[("a", 128), ("b", 8)], NaryLayoutEstimate::Unsupported);
3530 assert!(matches!(
3531 select_layout_aware_plan_for_test(&unsupported, &["a", "e"], DType::F32, true),
3532 NaryPlannerDecision::Greedy(NaryGreedyReason::UnsupportedLayout)
3533 ));
3534 let five = (0..5)
3535 .map(|ordinal| planner_meta(ordinal, &[("i", 128)], NaryLayoutEstimate::Contiguous))
3536 .collect::<Vec<_>>();
3537 assert!(matches!(
3538 select_layout_aware_plan_for_test(&five, &["i"], DType::F32, true),
3539 NaryPlannerDecision::Greedy(NaryGreedyReason::Arity)
3540 ));
3541 }
3542
3543 #[test]
3544 fn exact_search_is_structurally_bounded_zero_first_checked_and_fully_stable() {
3545 let equal = (0..4)
3546 .map(|ordinal| planner_meta(ordinal, &[("i", 128)], NaryLayoutEstimate::Contiguous))
3547 .collect::<Vec<_>>();
3548 let first = plan_layout_exact_for_test(&equal, &["i"]).unwrap();
3549 let second = plan_layout_exact_for_test(&equal, &["i"]).unwrap();
3550 assert_eq!(first, second);
3551 assert_eq!(
3552 first
3553 .steps
3554 .iter()
3555 .map(|step| step.members)
3556 .collect::<Vec<_>>(),
3557 [(1, 2), (3, 4), (7, 8)]
3558 );
3559
3560 let five = (0..5)
3561 .map(|ordinal| planner_meta(ordinal, &[("i", 128)], NaryLayoutEstimate::Contiguous))
3562 .collect::<Vec<_>>();
3563 assert!(plan_layout_exact_for_test(&five, &["i"]).is_err());
3564
3565 let late_zero = vec![
3566 planner_meta(
3567 0,
3568 &[("huge_a", usize::MAX), ("huge_b", usize::MAX)],
3569 NaryLayoutEstimate::Contiguous,
3570 ),
3571 planner_meta(
3572 1,
3573 &[("huge_c", usize::MAX), ("zero", 0)],
3574 NaryLayoutEstimate::Contiguous,
3575 ),
3576 planner_meta(2, &[], NaryLayoutEstimate::Contiguous),
3577 ];
3578 let decision = select_layout_aware_plan_for_test(&late_zero, &[], DType::F32, true);
3579 assert!(matches!(
3580 decision,
3581 NaryPlannerDecision::Greedy(NaryGreedyReason::BelowFlopThreshold)
3582 ));
3583
3584 let overflow = vec![
3585 planner_meta(
3586 0,
3587 &[("a", usize::MAX), ("b", usize::MAX)],
3588 NaryLayoutEstimate::Contiguous,
3589 ),
3590 planner_meta(
3591 1,
3592 &[("b", usize::MAX), ("c", usize::MAX)],
3593 NaryLayoutEstimate::Contiguous,
3594 ),
3595 planner_meta(2, &[("c", 1)], NaryLayoutEstimate::Contiguous),
3596 ];
3597 assert!(matches!(
3598 select_layout_aware_plan_for_test(&overflow, &["a"], DType::F32, true),
3599 NaryPlannerDecision::Greedy(NaryGreedyReason::Calibration)
3600 ));
3601 }
3602
3603 fn nary_spec<'a>(
3604 operands: &'a [EinsumAxisPattern<'a>],
3605 output: EinsumAxisPattern<'a>,
3606 ) -> EllipsisEinsumSpec<'a> {
3607 EllipsisEinsumSpec::new(operands, output)
3608 }
3609
3610 fn assert_mixed_close(left: &Tensor, right: &Tensor) -> Result<()> {
3611 assert_eq!(left.dims(), right.dims());
3612 let left = left.flatten_all()?.to_vec1::<f32>()?;
3613 let right = right.flatten_all()?.to_vec1::<f32>()?;
3614 for (&left, &right) in left.iter().zip(&right) {
3615 assert!((left - right).abs() <= 0.002 * right.abs().max(1.));
3616 }
3617 Ok(())
3618 }
3619
3620 #[test]
3621 fn broadcast_selected_execution_matches_greedy_forward_and_every_gradient() -> Result<()> {
3622 let device = Device::Cpu;
3623 let patterns = [
3624 EinsumAxisPattern::new(&["batch", "a", "b"], None),
3625 EinsumAxisPattern::new(&["batch", "b", "c"], None),
3626 EinsumAxisPattern::new(&["batch", "c", "d"], None),
3627 EinsumAxisPattern::new(&["batch", "d", "e"], None),
3628 ];
3629 let output = EinsumAxisPattern::new(&["batch", "a", "e"], None);
3630 let shapes = [[1, 32, 32], [32, 32, 15], [32, 15, 5], [32, 5, 10]];
3631 let values = shapes
3632 .iter()
3633 .enumerate()
3634 .map(|(ordinal, shape)| {
3635 let elements = shape.iter().product::<usize>();
3636 let values = (0..elements)
3637 .map(|index| ((index + ordinal * 7) % 23) as f32 / 23.)
3638 .collect::<Vec<_>>();
3639 Tensor::from_vec(values, shape.as_slice(), &device)
3640 })
3641 .collect::<Result<Vec<_>>>()?;
3642 let selected_vars = values
3643 .iter()
3644 .map(Var::from_tensor)
3645 .collect::<Result<Vec<_>>>()?;
3646 let greedy_vars = values
3647 .iter()
3648 .map(Var::from_tensor)
3649 .collect::<Result<Vec<_>>>()?;
3650 let selected_refs = selected_vars.iter().map(Var::as_tensor).collect::<Vec<_>>();
3651 let greedy_refs = greedy_vars.iter().map(Var::as_tensor).collect::<Vec<_>>();
3652 let (selected, trace) = execute_nary_einsum_for_test(
3653 &selected_refs,
3654 nary_spec(&patterns, output),
3655 NaryExecutionStrategy::Selected,
3656 )?;
3657 let (greedy, greedy_trace) = execute_nary_einsum_for_test(
3658 &greedy_refs,
3659 nary_spec(&patterns, output),
3660 NaryExecutionStrategy::StreamingGreedy,
3661 )?;
3662 assert_mixed_close(&selected, &greedy)?;
3663 assert!(!trace.used_exact);
3664 assert_eq!(trace.member_sequence, greedy_trace.member_sequence);
3665
3666 let selected_gradients = selected.sum_all()?.backward()?;
3667 let greedy_gradients = greedy.sum_all()?.backward()?;
3668 for (selected, greedy) in selected_vars.iter().zip(&greedy_vars) {
3669 assert_mixed_close(
3670 selected_gradients.get(selected.as_tensor()).unwrap(),
3671 greedy_gradients.get(greedy.as_tensor()).unwrap(),
3672 )?;
3673 }
3674 Ok(())
3675 }
3676
3677 #[test]
3678 fn selected_execution_matches_frozen_greedy_forward_gradients_and_layout_trace() -> Result<()> {
3679 clear_nary_plan_cache_for_test();
3680 let device = Device::Cpu;
3681 let patterns = [
3682 EinsumAxisPattern::new(&["a", "b"], None),
3683 EinsumAxisPattern::new(&["b", "c"], None),
3684 EinsumAxisPattern::new(&["c", "d"], None),
3685 EinsumAxisPattern::new(&["d", "e"], None),
3686 ];
3687 let output = EinsumAxisPattern::new(&["a", "e"], None);
3688 let values = [
3689 Tensor::arange(0f32, (128 * 8) as f32, &device)?.reshape((128, 8))?,
3690 Tensor::arange(0f32, 8., &device)?.reshape((8, 1))?,
3691 Tensor::arange(0f32, 8., &device)?.reshape((1, 8))?,
3692 Tensor::arange(0f32, (8 * 128) as f32, &device)?.reshape((8, 128))?,
3693 ];
3694 let selected_vars = values
3695 .iter()
3696 .map(Var::from_tensor)
3697 .collect::<Result<Vec<_>>>()?;
3698 let greedy_vars = values
3699 .iter()
3700 .map(Var::from_tensor)
3701 .collect::<Result<Vec<_>>>()?;
3702 let selected_refs = selected_vars
3703 .iter()
3704 .map(|var| var.as_tensor())
3705 .collect::<Vec<_>>();
3706 let greedy_refs = greedy_vars
3707 .iter()
3708 .map(|var| var.as_tensor())
3709 .collect::<Vec<_>>();
3710 let (selected, trace) = execute_nary_einsum_for_test(
3711 &selected_refs,
3712 nary_spec(&patterns, output),
3713 NaryExecutionStrategy::Selected,
3714 )?;
3715 let (_, cached_trace) = execute_nary_einsum_for_test(
3716 &selected_refs,
3717 nary_spec(&patterns, output),
3718 NaryExecutionStrategy::Selected,
3719 )?;
3720 let (greedy, greedy_trace) = execute_nary_einsum_for_test(
3721 &greedy_refs,
3722 nary_spec(&patterns, output),
3723 NaryExecutionStrategy::StreamingGreedy,
3724 )?;
3725 assert_mixed_close(&selected, &greedy)?;
3726 assert!(!trace.used_exact);
3727 assert!(!trace.used_cached_greedy);
3728 assert!(cached_trace.used_cached_greedy);
3729 assert_eq!(cached_trace.member_sequence, trace.member_sequence);
3730 assert!(!greedy_trace.used_exact);
3731 assert_eq!(trace.member_sequence, greedy_trace.member_sequence);
3732 assert_eq!(trace.final_permutations, 1);
3733 assert!(trace.intermediates.iter().all(|step| step.canonical));
3734 assert!(
3735 trace
3736 .intermediates
3737 .iter()
3738 .all(|step| step.estimated == step.execution_graph)
3739 );
3740 assert!(
3741 trace
3742 .intermediates
3743 .iter()
3744 .all(|step| { step.output_layout == NaryLayoutEstimate::Contiguous })
3745 );
3746
3747 let selected_gradients = selected.sum_all()?.backward()?;
3748 let greedy_gradients = greedy.sum_all()?.backward()?;
3749 for (selected, greedy) in selected_vars.iter().zip(&greedy_vars) {
3750 assert_mixed_close(
3751 selected_gradients.get(selected.as_tensor()).unwrap(),
3752 greedy_gradients.get(greedy.as_tensor()).unwrap(),
3753 )?;
3754 }
3755 Ok(())
3756 }
3757}