1use std::collections::BinaryHeap;
2
3use itertools::Itertools;
4use rustc_hash::FxHashMap;
5
6use crate::{
7 contractionpath::{
8 candidates::Candidate,
9 contraction_cost::{contract_cost_tensors, contract_size_tensors},
10 paths::{BasicContractionPathResult, ContractionPathResult, CostType, Pathfinder},
11 ssa_ordering, ContractionPath,
12 },
13 tensornetwork::tensor::{CompositeTensor, LeafTensor},
14 utils::traits::HashMapInsertNew,
15};
16
17pub struct BranchBound {
19 nbranch: Option<usize>,
20 cutoff_flops_factor: f64,
21 minimize: CostType,
22 best_flops: f64,
23 best_size: f64,
24 best_path: ContractionPath,
25 best_progress: FxHashMap<usize, f64>,
26 result_cache: FxHashMap<(usize, usize), usize>,
27 flop_cache: FxHashMap<usize, f64>,
28 size_cache: FxHashMap<usize, f64>,
29 tensor_cache: FxHashMap<usize, LeafTensor>,
30}
31
32impl BranchBound {
33 pub fn new(nbranch: Option<usize>, cutoff_flops_factor: f64, minimize: CostType) -> Self {
34 Self {
35 nbranch,
36 cutoff_flops_factor,
37 minimize,
38 best_flops: f64::INFINITY,
39 best_size: f64::INFINITY,
40 best_path: ContractionPath::default(),
41 best_progress: FxHashMap::default(),
42 result_cache: FxHashMap::default(),
43 flop_cache: FxHashMap::default(),
44 size_cache: FxHashMap::default(),
45 tensor_cache: FxHashMap::default(),
46 }
47 }
48
49 fn assess_candidate(
50 &mut self,
51 mut i: usize,
52 mut j: usize,
53 flops: f64,
54 size: f64,
55 remaining_len: usize,
56 ) -> Option<Candidate> {
57 let flops_12: f64;
58 let size_12: f64;
59 let k12: usize;
60 let k12_tensor: LeafTensor;
61 let mut current_flops = flops;
62 let mut current_size = size;
63 if self.tensor_cache[&j].size() > self.tensor_cache[&i].size() {
65 (i, j) = (j, i);
66 }
67
68 if self.result_cache.contains_key(&(i, j)) {
69 k12 = self.result_cache[&(i, j)];
70 flops_12 = self.flop_cache[&k12];
71 size_12 = self.size_cache[&k12];
72 } else {
73 k12 = self.tensor_cache.len();
74 flops_12 = contract_cost_tensors(&self.tensor_cache[&i], &self.tensor_cache[&j]);
75 size_12 = contract_size_tensors(&self.tensor_cache[&i], &self.tensor_cache[&j]);
76 k12_tensor = &self.tensor_cache[&i] ^ &self.tensor_cache[&j];
77
78 self.result_cache.entry((i, j)).or_insert_with(|| k12);
79 self.flop_cache.entry(k12).or_insert_with(|| flops_12);
80 self.size_cache.entry(k12).or_insert_with(|| size_12);
81 self.tensor_cache.insert_new(k12, k12_tensor);
82 }
83 current_flops += flops_12;
84 current_size = current_size.max(size_12);
85
86 if current_flops > self.best_flops && current_size > self.best_size {
87 return None;
88 }
89 let best_flops = *self
90 .best_progress
91 .entry(remaining_len)
92 .or_insert(current_flops);
93
94 if current_flops < best_flops {
95 self.best_progress.insert(remaining_len, current_flops);
96 } else if current_flops > self.cutoff_flops_factor * best_flops {
97 return None;
98 }
99
100 Some(Candidate {
101 flop_cost: current_flops,
102 size_cost: current_size,
103 parent_ids: (i, j),
104 child_id: k12,
105 })
106 }
107
108 fn branch_iterate(
113 &mut self,
114 tensor: &CompositeTensor,
115 path: &[(usize, usize, usize)],
116 remaining: &[usize],
117 flops: f64,
118 size: f64,
119 ) {
120 if remaining.len() == 1 {
121 match self.minimize {
122 CostType::Flops => {
123 if self.best_flops > flops {
124 self.best_flops = flops;
125 self.best_size = size;
126 self.best_path = ssa_ordering(path, tensor.tensors().len());
127 }
128 }
129 CostType::Size => {
130 if self.best_size > size {
131 self.best_flops = flops;
132 self.best_size = size;
133 self.best_path = ssa_ordering(path, tensor.tensors().len());
134 }
135 }
136 }
137 return;
138 }
139
140 let mut candidates = BinaryHeap::<Candidate>::new();
141 for i in remaining.iter().copied().combinations(2) {
142 let candidate = self.assess_candidate(i[0], i[1], flops, size, remaining.len());
143 if let Some(new_candidate) = candidate {
144 candidates.push(new_candidate);
145 }
146 }
147 let mut new_remaining;
148 let mut new_path: Vec<(usize, usize, usize)>;
149 let mut bi = 0;
150 while self.nbranch.is_none() || bi < self.nbranch.unwrap() {
151 bi += 1;
152 let Some(Candidate {
153 flop_cost,
154 size_cost,
155 parent_ids,
156 child_id,
157 }) = candidates.pop()
158 else {
159 break;
160 };
161 new_remaining = remaining.to_vec();
162 new_remaining.retain(|e| *e != parent_ids.0 && *e != parent_ids.1);
163 new_remaining.push(child_id);
164 new_path = path.to_vec();
165 new_path.push((parent_ids.0, parent_ids.1, child_id));
166 self.branch_iterate(tensor, &new_path, &new_remaining, flop_cost, size_cost);
167 }
168 }
169}
170
171impl Pathfinder for BranchBound {
172 type Result = BasicContractionPathResult;
173
174 fn find_path(&mut self, tensor: &CompositeTensor) -> BasicContractionPathResult {
175 let tensors = tensor.tensors().clone();
176 self.flop_cache.clear();
177 self.size_cache.clear();
178 self.result_cache.clear();
179 self.tensor_cache.clear();
180 let mut nested_paths = FxHashMap::default();
181 for (index, mut tensor) in tensors.into_iter().enumerate() {
183 if let Some(composite) = tensor.as_composite() {
185 let mut bb =
186 BranchBound::new(self.nbranch, self.cutoff_flops_factor, self.minimize);
187 let result = bb.find_path(composite);
188 nested_paths.insert(index, result.ssa_path().clone());
189 tensor = composite.external_tensor().into();
190 }
191 let tensor = tensor.into_leaf().unwrap();
192 self.size_cache
193 .entry(index)
194 .or_insert_with(|| tensor.size());
195
196 self.tensor_cache.insert_new(index, tensor);
197 }
198 let remaining = (0..tensor.tensors().len()).collect_vec();
199 self.branch_iterate(tensor, &[], &remaining, 0f64, 0f64);
200 let best_path = ContractionPath {
201 nested: nested_paths,
202 toplevel: std::mem::take(&mut self.best_path).into_simple(),
203 };
204 BasicContractionPathResult {
205 ssa_path: best_path,
206 flops: self.best_flops,
207 size: self.best_size,
208 }
209 }
210}
211
212#[cfg(test)]
213mod tests {
214 use super::*;
215
216 use rustc_hash::FxHashMap;
217
218 use crate::contractionpath::paths::{CostType, Pathfinder};
219 use crate::path;
220
221 fn setup_simple() -> CompositeTensor {
222 let bond_dims =
223 FxHashMap::from_iter([(0, 5), (1, 2), (2, 6), (3, 8), (4, 1), (5, 3), (6, 4)]);
224 CompositeTensor::new(vec![
225 LeafTensor::new_from_map(vec![4, 3, 2], &bond_dims),
226 LeafTensor::new_from_map(vec![0, 1, 3, 2], &bond_dims),
227 LeafTensor::new_from_map(vec![4, 5, 6], &bond_dims),
228 ])
229 }
230
231 fn setup_complex() -> CompositeTensor {
232 let bond_dims = FxHashMap::from_iter([
233 (0, 27),
234 (1, 18),
235 (2, 12),
236 (3, 15),
237 (4, 5),
238 (5, 3),
239 (6, 18),
240 (7, 22),
241 (8, 45),
242 (9, 65),
243 (10, 5),
244 (11, 17),
245 ]);
246 CompositeTensor::new(vec![
247 LeafTensor::new_from_map(vec![4, 3, 2], &bond_dims),
248 LeafTensor::new_from_map(vec![0, 1, 3, 2], &bond_dims),
249 LeafTensor::new_from_map(vec![4, 5, 6], &bond_dims),
250 LeafTensor::new_from_map(vec![6, 8, 9], &bond_dims),
251 LeafTensor::new_from_map(vec![10, 8, 9], &bond_dims),
252 LeafTensor::new_from_map(vec![5, 1, 0], &bond_dims),
253 ])
254 }
255
256 #[test]
257 fn test_contract_order_simple() {
258 let tn = setup_simple();
259 let mut opt = BranchBound::new(None, 20., CostType::Flops);
260 let result = opt.find_path(&tn);
261
262 assert_eq!(
263 result,
264 BasicContractionPathResult {
265 ssa_path: path![(1, 0), (2, 3)],
266 flops: 4540.,
267 size: 538.
268 }
269 );
270 }
271
272 #[test]
273 fn test_contract_order_complex() {
274 let tn = setup_complex();
275 let mut opt = BranchBound::new(None, 20., CostType::Flops);
276 let result = opt.find_path(&tn);
277
278 assert_eq!(
279 result,
280 BasicContractionPathResult {
281 ssa_path: path![(1, 5), (0, 6), (2, 7), (3, 8), (4, 9)],
282 flops: 2654474.,
283 size: 89478.
284 }
285 );
286 }
287}