ndarray_einsum_beta/optimizers.rs
1// Copyright 2019 Jared Samet
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Methods to produce a `ContractionOrder`, specifying what order in which to perform pairwise contractions between tensors
16//! in order to perform the full contraction.
17use crate::SizedContraction;
18use std::collections::HashSet;
19
20#[cfg(feature = "serde")]
21use serde::{Deserialize, Serialize};
22
23/// Either an input operand or an intermediate result
24#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
25#[derive(Debug, Clone)]
26pub enum OperandNumber {
27 Input(usize),
28 IntermediateResult(usize),
29}
30
31/// Which two tensors to contract
32#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
33#[derive(Debug, Clone)]
34pub struct OperandNumPair {
35 pub lhs: OperandNumber,
36 pub rhs: OperandNumber,
37}
38
39/// A single pairwise contraction between two input operands, an input operand and an intermediate
40/// result, or two intermediate results.
41#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
42#[derive(Debug, Clone)]
43pub struct Pair {
44 /// The contraction to be performed
45 pub sized_contraction: SizedContraction,
46
47 /// Which two tensors to contract
48 pub operand_nums: OperandNumPair,
49}
50
51/// The order in which to contract pairs of tensors and the specific contractions to be performed between the pairs.
52///
53/// Either a singleton contraction, in the case of a single input operand, or a list of pair contractions,
54/// given two or more input operands
55#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
56#[derive(Debug, Clone)]
57pub enum ContractionOrder {
58 /// If there's only one input operand, this is simply a clone of the original SizedContraction
59 Singleton(SizedContraction),
60
61 /// If there are two or more input operands, this is a vector of pairwise contractions between
62 /// input operands and/or intermediate results from prior contractions.
63 Pairs(Vec<Pair>),
64}
65
66/// Strategy for optimizing the contraction. The only currently supported options are "Naive" and "Reverse".
67///
68/// TODO: Figure out whether this should be done with traits
69#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
70#[derive(Debug)]
71pub enum OptimizationMethod {
72 /// Contracts each pair of tensors in the order given in the input and uses the intermediate
73 /// result as the LHS of the next contraction.
74 Naive,
75
76 /// Contracts each pair of tensors in the reverse of the order given in the input and uses the
77 /// intermediate result as the LHS of the next contraction. Only implemented to help test
78 /// that this is actually functioning properly.
79 Reverse,
80
81 /// (Not yet supported) Something like [this](https://optimized-einsum.readthedocs.io/en/latest/greedy_path.html)
82 Greedy,
83
84 /// (Not yet supported) Something like [this](https://optimized-einsum.readthedocs.io/en/latest/optimal_path.html)
85 Optimal,
86
87 /// (Not yet supported) Something like [this](https://optimized-einsum.readthedocs.io/en/latest/branching_path.html)
88 Branch,
89}
90
91/// Returns a set of all the indices in any of the remaining operands or in the output
92fn get_remaining_indices(operand_indices: &[Vec<char>], output_indices: &[char]) -> HashSet<char> {
93 let mut result: HashSet<char> = HashSet::new();
94 for &c in operand_indices.iter().flat_map(|s| s.iter()) {
95 result.insert(c);
96 }
97 for &c in output_indices.iter() {
98 result.insert(c);
99 }
100 result
101}
102
103/// Returns a set of all the indices in the LHS or the RHS
104fn get_existing_indices(lhs_indices: &[char], rhs_indices: &[char]) -> HashSet<char> {
105 let mut result: HashSet<char> = lhs_indices.iter().cloned().collect();
106 for &c in rhs_indices.iter() {
107 result.insert(c);
108 }
109 result
110}
111
112/// Returns a permuted version of `sized_contraction`, specified by `tensor_order`
113fn generate_permuted_contraction(
114 sized_contraction: &SizedContraction,
115 tensor_order: &[usize],
116) -> SizedContraction {
117 // Reorder the operands of the SizedContraction and clone everything else
118 assert_eq!(
119 sized_contraction.contraction.operand_indices.len(),
120 tensor_order.len()
121 );
122 let mut new_operand_indices = Vec::new();
123 for &i in tensor_order {
124 new_operand_indices.push(sized_contraction.contraction.operand_indices[i].clone());
125 }
126 sized_contraction
127 .subset(
128 &new_operand_indices,
129 &sized_contraction.contraction.output_indices,
130 )
131 .unwrap()
132}
133
134/// Generates a mini-contraction corresponding to `lhs_operand_indices`,`rhs_operand_indices`->`output_indices`
135fn generate_sized_contraction_pair(
136 lhs_operand_indices: &[char],
137 rhs_operand_indices: &[char],
138 output_indices: &[char],
139 orig_contraction: &SizedContraction,
140) -> SizedContraction {
141 orig_contraction
142 .subset(
143 &[lhs_operand_indices.to_vec(), rhs_operand_indices.to_vec()],
144 output_indices,
145 )
146 .unwrap()
147}
148
149/// Generate the actual path consisting of all the mini-contractions. Currently always
150/// contracts two input operands and then repeatedly uses the result as the LHS of the
151/// next pairwise contraction.
152fn generate_path(sized_contraction: &SizedContraction, tensor_order: &[usize]) -> ContractionOrder {
153 // Generate the actual path consisting of all the mini-contractions.
154 //
155 // TODO: Take a &[OperandNumPair] instead of &[usize]
156 // and Keep track of the intermediate results
157
158 // Make a reordered full SizedContraction in the order specified by the called
159 let permuted_contraction = generate_permuted_contraction(sized_contraction, tensor_order);
160
161 match permuted_contraction.contraction.operand_indices.len() {
162 1 => {
163 // If there's only one input tensor, make a single-step path consisting of a
164 // singleton contraction (operand_nums = None).
165 ContractionOrder::Singleton(permuted_contraction.clone())
166 }
167 2 => {
168 // If there's exactly two input tensors, make a single-step path consisting
169 // of a pair contraction (operand_nums = Some(OperandNumPair)).
170 let sc = generate_sized_contraction_pair(
171 &permuted_contraction.contraction.operand_indices[0],
172 &permuted_contraction.contraction.operand_indices[1],
173 &permuted_contraction.contraction.output_indices,
174 &permuted_contraction,
175 );
176 let operand_num_pair = OperandNumPair {
177 lhs: OperandNumber::Input(tensor_order[0]),
178 rhs: OperandNumber::Input(tensor_order[1]),
179 };
180 let only_step = Pair {
181 sized_contraction: sc,
182 operand_nums: operand_num_pair,
183 };
184 ContractionOrder::Pairs(vec![only_step])
185 }
186 _ => {
187 // If there's three or more input tensors, we have some work to do.
188
189 let mut steps = Vec::new();
190 // In the main body of the loop, output_indices will contain the result of the prior pair
191 // contraction. Initialize it to the elements of the first LHS tensor so that we can
192 // clone it on the first go-around as well as all the later ones.
193 let mut output_indices = permuted_contraction.contraction.operand_indices[0].clone();
194
195 for idx_of_lhs in 0..(permuted_contraction.contraction.operand_indices.len() - 1) {
196 // lhs_indices is either the first tensor (on the first iteration of the loop)
197 // or the output from the previous step.
198 let lhs_indices = output_indices.clone();
199
200 // rhs_indices is always the next tensor.
201 let idx_of_rhs = idx_of_lhs + 1;
202 let rhs_indices = &permuted_contraction.contraction.operand_indices[idx_of_rhs];
203
204 // existing_indices and remaining_indices are only needed to figure out
205 // what output_indices will be for this step.
206 //
207 // existing_indices consists of the indices in either the LHS or the RHS tensor
208 // for this step.
209 //
210 // remaining_indices consists of the indices in all the elements after the RHS
211 // tensor or in the outputs.
212 //
213 // The output indices we want is the intersection of the two (unless this is
214 // the RHS is the last operand, in which case it's just the output indices).
215 //
216 // For example, say the string is "ij,jk,kl,lm->im".
217 // First iteration:
218 // lhs = [i,j]
219 // rhs = [j,k]
220 // existing = {i,j,k}
221 // remaining = {k,l,m,i} (the i is used in the final output so we need to
222 // keep it around)
223 // output = {i,k}
224 // Mini-contraction: ij,jk->ik
225 // Second iteration:
226 // lhs = [i,k]
227 // rhs = [k,l]
228 // existing = {i,k,l}
229 // remaining = {l,m,i}
230 // output = {i,l}
231 // Mini-contraction: ik,kl->il
232 // Third (and final) iteration:
233 // lhs = [i,l]
234 // rhs = [l,m]
235 // (Short-circuit) output = {i,m}
236 // Mini-contraction: il,lm->im
237 output_indices =
238 if idx_of_rhs == (permuted_contraction.contraction.operand_indices.len() - 1) {
239 // Used up all the operands; just return output
240 permuted_contraction.contraction.output_indices.clone()
241 } else {
242 let existing_indices = get_existing_indices(&lhs_indices, rhs_indices);
243 let remaining_indices = get_remaining_indices(
244 &permuted_contraction.contraction.operand_indices[(idx_of_rhs + 1)..],
245 &permuted_contraction.contraction.output_indices,
246 );
247 existing_indices
248 .intersection(&remaining_indices)
249 .cloned()
250 .collect()
251 };
252
253 // Phew, now make the mini-contraction.
254 let sc = generate_sized_contraction_pair(
255 &lhs_indices,
256 &rhs_indices,
257 &output_indices,
258 &permuted_contraction,
259 );
260
261 let operand_nums = if idx_of_lhs == 0 {
262 OperandNumPair {
263 lhs: OperandNumber::Input(tensor_order[idx_of_lhs]), // tensor_order[0]
264 rhs: OperandNumber::Input(tensor_order[idx_of_rhs]), // tensor_order[1]
265 }
266 } else {
267 OperandNumPair {
268 lhs: OperandNumber::IntermediateResult(idx_of_lhs - 1),
269 rhs: OperandNumber::Input(tensor_order[idx_of_rhs]),
270 }
271 };
272 steps.push(Pair {
273 sized_contraction: sc,
274 operand_nums,
275 });
276 }
277
278 ContractionOrder::Pairs(steps)
279 }
280 }
281}
282
283/// Contracts the first two operands, then contracts the result with the third operand, etc.
284fn naive_order(sized_contraction: &SizedContraction) -> Vec<usize> {
285 (0..sized_contraction.contraction.operand_indices.len()).collect()
286}
287
288/// Contracts the last two operands, then contracts the result with the third-to-last operand, etc.
289fn reverse_order(sized_contraction: &SizedContraction) -> Vec<usize> {
290 (0..sized_contraction.contraction.operand_indices.len())
291 .rev()
292 .collect()
293}
294
295// TODO: Maybe this should take a function pointer from &SizedContraction -> Vec<usize>?
296/// Given a `SizedContraction` and an optimization strategy, returns an order in which to
297/// perform pairwise contractions in order to produce the final result
298pub fn generate_optimized_order(
299 sized_contraction: &SizedContraction,
300 strategy: OptimizationMethod,
301) -> ContractionOrder {
302 let tensor_order = match strategy {
303 OptimizationMethod::Naive => naive_order(sized_contraction),
304 OptimizationMethod::Reverse => reverse_order(sized_contraction),
305 _ => panic!("Unsupported optimization method"),
306 };
307 generate_path(sized_contraction, &tensor_order)
308}