1use std::collections::BTreeMap;
2use crate::{ComputeType, Error, Mode, OperandDescriptor, OperationDescriptor, Plan, Result, TensorDescriptor};
3use crate::plan::merge_extent;
4use ruda_core::tensor::DType;
5use ruda_core::device::Device;
6use ruda_kernel::{dsl::Runtime, tensor::RudaTensor};
7
8#[derive(Clone, Debug)]
9enum Token { Label(Mode), Ellipsis }
10
11fn tokens(text: &str) -> Result<Vec<Token>> {
12 let mut result = Vec::new();
13 let bytes = text.as_bytes();
14 let mut index = 0;
15 let mut ellipsis = false;
16 while index < bytes.len() {
17 if bytes[index].is_ascii_alphabetic() {
18 result.push(Token::Label(bytes[index] as Mode));
19 index += 1;
20 } else if bytes[index..].starts_with(b"...") && !ellipsis {
21 result.push(Token::Ellipsis);
22 ellipsis = true;
23 index += 3;
24 } else {
25 return Err(Error::InvalidExpression("use ASCII letters and at most one ellipsis per operand".into()));
26 }
27 }
28 Ok(result)
29}
30
31fn expand(tokens: &[Token], ellipsis_modes: &[Mode]) -> Vec<Mode> {
32 let mut modes = Vec::new();
33 for token in tokens {
34 match token {
35 Token::Label(mode) => modes.push(*mode),
36 Token::Ellipsis => modes.extend_from_slice(ellipsis_modes),
37 }
38 }
39 modes
40}
41
42pub(crate) fn output_dtype(inputs: &[OperandDescriptor]) -> DType {
43 let first = inputs[0].tensor().dtype();
44 if inputs.iter().all(|input| input.tensor().dtype() == first) { return first; }
45 if inputs.iter().any(|input| input.tensor().dtype() == DType::F64) { DType::F64 } else { DType::F32 }
46}
47
48pub(crate) fn infer_output(inputs: &[OperandDescriptor], modes: &[Mode], dtype: DType) -> Result<TensorDescriptor> {
49 let mut extents = BTreeMap::new();
50 for input in inputs {
51 for (&mode, &extent) in input.modes().iter().zip(input.tensor().extents()) {
52 merge_extent(&mut extents, mode, extent)?;
53 }
54 }
55 let shape = modes.iter().map(|mode| extents.get(mode).copied().ok_or_else(||
56 Error::InvalidOperation(format!("output mode {mode} is absent from all inputs"))))
57 .collect::<Result<Vec<_>>>()?;
58 TensorDescriptor::contiguous(&shape, dtype)
59}
60
61#[derive(Clone, Debug)]
63pub struct EinsumPlan {
64 expression: String,
65 plan: Plan,
66 path: Option<crate::path::ContractionPath>,
67}
68
69impl EinsumPlan {
70 pub fn new(expression: &str, inputs: &[TensorDescriptor]) -> Result<Self> {
71 Self::build(expression, inputs, None)
72 }
73
74 pub fn with_options(
75 expression: &str, inputs: &[TensorDescriptor], output_dtype: DType, compute: ComputeType,
76 ) -> Result<Self> {
77 Self::build(expression, inputs, Some((output_dtype, compute)))
78 }
79
80 fn build(expression: &str, inputs: &[TensorDescriptor], options: Option<(DType, ComputeType)>) -> Result<Self> {
81 if inputs.is_empty() {
82 return Err(Error::InvalidExpression("at least one tensor is required".into()));
83 }
84 let expression: String = expression.chars().filter(|c| !c.is_ascii_whitespace()).collect();
85 let mut sides = expression.split("->");
86 let lhs = sides.next().unwrap_or_default();
87 let rhs = sides.next();
88 if sides.next().is_some() {
89 return Err(Error::InvalidExpression("more than one output arrow".into()));
90 }
91 let input_text: Vec<_> = lhs.split(',').collect();
92 if input_text.len() != inputs.len() {
93 return Err(Error::InputCount { expected: input_text.len(), actual: inputs.len() });
94 }
95 let token_lists: Vec<_> = input_text.iter().map(|text| tokens(text)).collect::<Result<_>>()?;
96 let mut widths = Vec::new();
97 let mut counts = BTreeMap::<Mode, usize>::new();
98 for (tokens, descriptor) in token_lists.iter().zip(inputs) {
99 let explicit = tokens.iter().filter(|token| matches!(token, Token::Label(_))).count();
100 let has_ellipsis = tokens.iter().any(|token| matches!(token, Token::Ellipsis));
101 if explicit > descriptor.rank() || (!has_ellipsis && explicit != descriptor.rank()) {
102 return Err(Error::InvalidExpression("operand labels do not match its tensor rank".into()));
103 }
104 widths.push(descriptor.rank() - explicit);
105 for token in tokens {
106 if let Token::Label(mode) = token { *counts.entry(*mode).or_default() += 1; }
107 }
108 }
109 let width = widths.iter().copied().max().unwrap_or(0);
110 let ellipsis_modes = (0..width).map(|axis| {
111 let axis = i32::try_from(axis).map_err(|_| Error::Overflow)?;
112 i32::MIN.checked_add(axis).ok_or(Error::Overflow)
113 }).collect::<Result<Vec<_>>>()?;
114 let mut operands = Vec::new();
115 for ((tokens, descriptor), local_width) in token_lists.iter().zip(inputs).zip(widths) {
116 let modes = expand(tokens, &ellipsis_modes[width - local_width..]);
117 operands.push(OperandDescriptor::new(descriptor.clone(), &modes)?);
118 }
119 let output_modes = if let Some(rhs) = rhs {
120 expand(&tokens(rhs)?, &ellipsis_modes)
121 } else {
122 let mut modes = ellipsis_modes;
123 modes.extend(counts.iter().filter_map(|(&mode, &count)| (count == 1).then_some(mode)));
124 modes
125 };
126 let (dtype, compute) = options.unwrap_or_else(||
127 (output_dtype(&operands), ComputeType::for_operands(&operands)));
128 let output = infer_output(&operands, &output_modes, dtype)?;
129 let operation = OperationDescriptor::sum_product(operands, None, output, &output_modes, compute)?;
130 Ok(Self { expression, plan: Plan::new(operation)?, path: None })
131 }
132
133 pub fn expression(&self) -> &str { &self.expression }
134 pub fn plan(&self) -> &Plan { &self.plan }
135 pub fn output(&self) -> &TensorDescriptor { self.plan.operation().output() }
136
137 pub fn optimize(mut self) -> Result<Self> {
141 if self.plan.operation().inputs().len() > 1 {
142 self.path = Some(crate::path::ContractionPath::new(self.plan.operation())?);
143 }
144 Ok(self)
145 }
146
147 pub fn planned_scalar_products(&self) -> Option<u128> {
148 self.path.as_ref().map(|path| path.products)
149 }
150
151 pub fn execute<R: Runtime>(&self, inputs: &[&RudaTensor<R>]) -> Result<RudaTensor<R>> {
152 if let Some(path) = &self.path {
153 self.plan.validate_inputs(inputs, &[1.0])?;
154 path.execute(inputs)
155 } else {
156 self.plan.execute(inputs, &[1.0])
157 }
158 }
159
160 pub fn execute_into<R: Runtime>(&self, inputs: &[&RudaTensor<R>], output: RudaTensor<R>) -> Result<RudaTensor<R>> {
161 if let Some(path) = &self.path {
162 self.plan.validate_inputs(inputs, &[1.0])?;
163 if !self.output().matches(&output) { return Err(Error::OutputMismatch); }
164 self.output().check_buffer(&output)?;
165 if inputs[0].device.to_id() != output.device.to_id() { return Err(Error::DeviceMismatch); }
166 if !output.can_mut() { return Err(Error::SharedOutput); }
167 path.execute_into(inputs, output)
168 } else {
169 self.plan.execute_into(inputs, output, &[1.0])
170 }
171 }
172}
173
174pub fn einsum<R: Runtime>(expression: &str, inputs: &[&RudaTensor<R>]) -> Result<RudaTensor<R>> {
179 let descriptors = inputs.iter().map(|input| TensorDescriptor::from_tensor(*input)).collect::<Result<Vec<_>>>()?;
180 EinsumPlan::new(expression, &descriptors)?.execute(inputs)
181}
182
183pub fn einsum_with_options<R: Runtime>(
184 expression: &str, inputs: &[&RudaTensor<R>], output_dtype: DType, compute: ComputeType,
185) -> Result<RudaTensor<R>> {
186 let descriptors = inputs.iter().map(|input| TensorDescriptor::from_tensor(*input)).collect::<Result<Vec<_>>>()?;
187 EinsumPlan::with_options(expression, &descriptors, output_dtype, compute)?.execute(inputs)
188}