Skip to main content

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}