1use std::{
4 iter::zip,
5 time::{Duration, Instant},
6};
7
8use itertools::Itertools;
9use ordered_float::NotNan;
10use rand::{rngs::StdRng, seq::IteratorRandom, Rng, SeedableRng};
11use rayon::iter::{IndexedParallelIterator, IntoParallelRefMutIterator, ParallelIterator};
12use rustc_hash::FxHashSet;
13
14use crate::{
15 contractionpath::{
16 communication_schemes::CommunicationScheme,
17 contraction_cost::{compute_memory_requirements, contract_size_tensors_bytes},
18 paths::{
19 cotengrust::{Cotengrust, OptMethod},
20 ContractionPathResult, Pathfinder,
21 },
22 repartitioning::compute_solution,
23 SimplePath,
24 },
25 tensornetwork::{
26 partitioning::partition_tensor_network,
27 tensor::{CompositeTensor, LeafTensor},
28 },
29};
30
31type ScoreType = NotNan<f64>;
32
33const PROCESSING_THREADS: usize = 48;
36
37pub trait OptModel: Sync + Send {
39 type SolutionType: Clone + Sync + Send;
41
42 fn generate_trial_solution<R: Rng>(
44 &self,
45 current_solution: Self::SolutionType,
46 rng: &mut R,
47 ) -> Self::SolutionType;
48
49 fn evaluate<R: Rng>(&self, solution: &Self::SolutionType, rng: &mut R) -> ScoreType;
51}
52
53#[derive(Clone, Copy)]
55pub struct SimulatedAnnealingOptimizer {
56 n_trials: usize,
58 max_time: Duration,
60 n_steps: usize,
62 restart_iter: usize,
65 initial_temperature: f64,
67 final_temperature: f64,
69}
70
71#[inline]
75fn linear_interpolation(start: f64, end: f64, t: f64) -> f64 {
76 (end - start).mul_add(t, start)
77}
78
79impl SimulatedAnnealingOptimizer {
80 #[allow(clippy::too_many_arguments)]
86 fn optimize_with_temperature<M, R>(
87 &self,
88 model: &M,
89 initial_solution: M::SolutionType,
90 rng: &mut R,
91 ) -> (M::SolutionType, ScoreType)
92 where
93 M: OptModel,
94 R: Rng,
95 {
96 let mut current_score = model.evaluate(&initial_solution, rng);
97 let mut current_solution = initial_solution;
98 let mut best_solution = current_solution.clone();
99 let mut best_score = current_score;
100 let mut last_improvement = 0;
101 let steps_per_thread = self.n_steps.div_ceil(self.n_trials);
102
103 let log_start = self.initial_temperature.log2();
104 let log_end = self.final_temperature.log2();
105 let total_seconds = self.max_time.as_secs_f64();
106 let mut temperature = self.initial_temperature;
107 let mut rngs = (0..self.n_trials)
108 .map(|_| StdRng::seed_from_u64(rng.random()))
109 .collect_vec();
110 let end_time = Instant::now() + self.max_time;
111 loop {
112 let (_, trial_solution, trial_score) = rngs
114 .par_iter_mut()
115 .enumerate()
116 .map(|(index, rng)| {
117 let mut trial_score = current_score;
118 let mut trial_solution = current_solution.clone();
119 for _ in 0..steps_per_thread {
120 let solution = model.generate_trial_solution(trial_solution.clone(), rng);
121 let score = model.evaluate(&solution, rng);
122
123 let diff = (score / trial_score).log2();
124 let acceptance_probability = (-diff / temperature).exp();
125 let random_value = rng.random();
126
127 if acceptance_probability >= random_value {
128 trial_solution = solution;
129 trial_score = score;
130 }
131 }
132 (index, trial_solution, trial_score)
133 })
134 .min_by_key(|(index, _, score)| (*score, *index))
135 .unwrap();
136
137 current_score = trial_score;
138 current_solution = trial_solution;
139
140 if current_score < best_score {
142 best_solution = current_solution.clone();
143 best_score = current_score;
144 last_improvement = 0;
145 }
146
147 last_improvement += 1;
148
149 if last_improvement == self.restart_iter {
151 current_solution = best_solution.clone();
152 current_score = best_score;
153 }
154
155 let now = Instant::now();
157 if now > end_time {
158 break;
160 }
161 let remaining_time = (end_time - now).as_secs_f64();
162 let progress = 1.0 - remaining_time / total_seconds;
163 temperature = linear_interpolation(log_start, log_end, progress).exp2();
164 }
165
166 (best_solution, best_score)
167 }
168}
169
170fn evaluate_partitioning<R>(
172 tensor: &CompositeTensor,
173 partitioning: &[usize],
174 communication_scheme: CommunicationScheme,
175 memory_limit: Option<f64>,
176 rng: &mut R,
177) -> NotNan<f64>
178where
179 R: Rng,
180{
181 let (partitioned_tn, path, parallel_cost, _) =
183 compute_solution(tensor, partitioning, communication_scheme, Some(rng));
184
185 if let Some(limit) = memory_limit {
187 let mem = compute_memory_requirements(
189 partitioned_tn.tensors(),
190 &path,
191 contract_size_tensors_bytes,
192 );
193
194 if mem > limit {
195 return NotNan::new(f64::INFINITY).unwrap();
196 }
197 }
198 NotNan::new(parallel_cost).unwrap()
199}
200
201pub struct NaivePartitioningModel<'a> {
203 pub tensor: &'a CompositeTensor,
204 pub num_partitions: usize,
205 pub communication_scheme: CommunicationScheme,
206 pub memory_limit: Option<f64>,
207}
208
209impl OptModel for NaivePartitioningModel<'_> {
210 type SolutionType = Vec<usize>;
211
212 fn generate_trial_solution<R: Rng>(
213 &self,
214 mut current_solution: Self::SolutionType,
215 rng: &mut R,
216 ) -> Self::SolutionType {
217 let tensor_index = rng.random_range(0..current_solution.len());
218 let current_partition = current_solution[tensor_index];
219 let new_partition = loop {
220 let b = rng.random_range(0..self.num_partitions);
221 if b != current_partition {
222 break b;
223 }
224 };
225 current_solution[tensor_index] = new_partition;
226 current_solution
227 }
228
229 fn evaluate<R: Rng>(&self, solution: &Self::SolutionType, rng: &mut R) -> ScoreType {
230 evaluate_partitioning(
231 self.tensor,
232 solution,
233 self.communication_scheme,
234 self.memory_limit,
235 rng,
236 )
237 }
238}
239
240pub struct NaiveIntermediatePartitioningModel<'a> {
242 pub tensor: &'a CompositeTensor,
243 pub num_partitions: usize,
244 pub communication_scheme: CommunicationScheme,
245 pub memory_limit: Option<f64>,
246}
247
248impl OptModel for NaiveIntermediatePartitioningModel<'_> {
249 type SolutionType = (Vec<usize>, Vec<SimplePath>);
250
251 fn generate_trial_solution<R: Rng>(
252 &self,
253 current_solution: Self::SolutionType,
254 rng: &mut R,
255 ) -> Self::SolutionType {
256 let (mut partitioning, mut contraction_paths) = current_solution;
257
258 let source_partition = contraction_paths
260 .iter()
261 .enumerate()
262 .filter_map(|(contraction_id, contraction)| {
263 if contraction.len() >= 3 {
264 Some(contraction_id)
265 } else {
266 None
267 }
268 })
269 .choose(rng);
270
271 let Some(source_partition) = source_partition else {
272 return (partitioning, contraction_paths);
274 };
275
276 let pair_index = rng.random_range(0..contraction_paths[source_partition].len() - 1);
278 let (i, j) = contraction_paths[source_partition][pair_index];
279 let mut tensor_leaves = FxHashSet::from_iter([i, j]);
280
281 for (i, j) in contraction_paths[source_partition]
283 .iter()
284 .take(pair_index)
285 .rev()
286 {
287 if tensor_leaves.contains(i) {
288 tensor_leaves.insert(*j);
289 }
290 }
291
292 let mut shifted_indices = Vec::with_capacity(tensor_leaves.len());
293 for (partition_tensor_index, (i, _partition)) in partitioning
294 .iter()
295 .enumerate()
296 .filter(|(_, partition)| *partition == &source_partition)
297 .enumerate()
298 {
299 if tensor_leaves.contains(&partition_tensor_index) {
300 shifted_indices.push(i);
301 }
302 }
303
304 let target_partition = loop {
306 let b = rng.random_range(0..self.num_partitions);
307 if b != source_partition {
308 break b;
309 }
310 };
311
312 for index in shifted_indices {
314 partitioning[index] = target_partition;
315 }
316
317 let mut from_tensor = CompositeTensor::default();
319 let mut to_tensor = CompositeTensor::default();
320 for (partition_index, tensor) in zip(&partitioning, self.tensor.tensors()) {
321 if *partition_index == source_partition {
322 from_tensor.push_tensor(tensor.clone());
323 } else if *partition_index == target_partition {
324 to_tensor.push_tensor(tensor.clone());
325 }
326 }
327
328 let mut from_opt = Cotengrust::new(OptMethod::Greedy);
329 let result = from_opt.find_path(&from_tensor);
330 let from_path = result.replace_path();
331 contraction_paths[source_partition] = from_path.into_simple();
332
333 let mut to_opt = Cotengrust::new(OptMethod::Greedy);
334 let result = to_opt.find_path(&to_tensor);
335 let to_path = result.replace_path();
336 contraction_paths[target_partition] = to_path.into_simple();
337
338 (partitioning, contraction_paths)
339 }
340
341 fn evaluate<R: Rng>(&self, solution: &Self::SolutionType, rng: &mut R) -> ScoreType {
342 evaluate_partitioning(
343 self.tensor,
344 &solution.0,
345 self.communication_scheme,
346 self.memory_limit,
347 rng,
348 )
349 }
350}
351
352pub struct LeafPartitioningModel<'a> {
355 pub tensor: &'a CompositeTensor,
356 pub communication_scheme: CommunicationScheme,
357 pub memory_limit: Option<f64>,
358}
359
360impl OptModel for LeafPartitioningModel<'_> {
361 type SolutionType = (Vec<usize>, Vec<LeafTensor>);
362
363 fn generate_trial_solution<R: Rng>(
364 &self,
365 current_solution: Self::SolutionType,
366 rng: &mut R,
367 ) -> Self::SolutionType {
368 let (mut partitioning, mut partition_tensors) = current_solution;
369 let tensor_index = rng.random_range(0..partitioning.len());
370 let shifted_tensor = self.tensor.tensor(tensor_index).as_leaf().unwrap();
371 let source_partition = partitioning[tensor_index];
372
373 let (new_partition, _) = partition_tensors
374 .iter()
375 .enumerate()
376 .filter_map(|(i, partition_tensor)| {
377 if i != source_partition {
378 Some((
379 i,
380 (shifted_tensor ^ partition_tensor).size() - partition_tensor.size(),
381 ))
382 } else {
383 None
385 }
386 })
387 .min_by(|a, b| a.1.total_cmp(&b.1))
388 .unwrap();
389
390 partitioning[tensor_index] = new_partition;
391 partition_tensors[source_partition] ^= shifted_tensor;
392 partition_tensors[new_partition] ^= shifted_tensor;
393 (partitioning, partition_tensors)
394 }
395
396 fn evaluate<R: Rng>(&self, solution: &Self::SolutionType, rng: &mut R) -> ScoreType {
397 evaluate_partitioning(
398 self.tensor,
399 &solution.0,
400 self.communication_scheme,
401 self.memory_limit,
402 rng,
403 )
404 }
405}
406
407pub struct IntermediatePartitioningModel<'a> {
410 pub tensor: &'a CompositeTensor,
411 pub communication_scheme: CommunicationScheme,
412 pub memory_limit: Option<f64>,
413}
414
415impl IntermediatePartitioningModel<'_> {
416 pub fn compute_initial_solution(
417 &self,
418 initial_partitioning: &[usize],
419 initial_contraction_paths: Option<Vec<SimplePath>>,
420 ) -> <IntermediatePartitioningModel<'_> as OptModel>::SolutionType {
421 let partitioned_tn = partition_tensor_network(self.tensor.clone(), initial_partitioning);
422
423 let partition_tensors = partitioned_tn
425 .tensors()
426 .iter()
427 .map(|t| t.as_composite().unwrap().external_tensor())
428 .collect_vec();
429
430 let contraction_paths = initial_contraction_paths.unwrap_or_else(|| {
431 partitioned_tn
433 .tensors()
434 .iter()
435 .map(|t| {
436 let mut opt = Cotengrust::new(OptMethod::Greedy);
438 let result = opt.find_path(t.as_composite().unwrap());
439 let path = result.replace_path();
440 path.into_simple()
441 })
442 .collect()
443 });
444
445 (
447 initial_partitioning.to_vec(),
448 partition_tensors,
449 contraction_paths,
450 )
451 }
452}
453
454impl OptModel for IntermediatePartitioningModel<'_> {
455 type SolutionType = (Vec<usize>, Vec<LeafTensor>, Vec<SimplePath>);
456
457 fn generate_trial_solution<R: Rng>(
458 &self,
459 current_solution: Self::SolutionType,
460 rng: &mut R,
461 ) -> Self::SolutionType {
462 let (mut partitioning, mut partition_tensors, mut contraction_paths) = current_solution;
463
464 let source_partition = contraction_paths
466 .iter()
467 .enumerate()
468 .filter_map(|(contraction_id, contraction)| {
469 if contraction.len() >= 3 {
470 Some(contraction_id)
471 } else {
472 None
473 }
474 })
475 .choose(rng);
476
477 let Some(source_partition) = source_partition else {
478 return (partitioning, partition_tensors, contraction_paths);
480 };
481
482 let pair_index = rng.random_range(0..contraction_paths[source_partition].len() - 1);
484 let (i, j) = contraction_paths[source_partition][pair_index];
485 let mut tensor_leaves = FxHashSet::from_iter([i, j]);
486
487 for (i, j) in contraction_paths[source_partition]
489 .iter()
490 .take(pair_index)
491 .rev()
492 {
493 if tensor_leaves.contains(i) {
494 tensor_leaves.insert(*j);
495 }
496 }
497
498 let mut shifted_tensor = LeafTensor::default();
499 let mut shifted_indices = Vec::with_capacity(tensor_leaves.len());
500 for (partition_tensor_index, (i, _partition)) in partitioning
501 .iter()
502 .enumerate()
503 .filter(|(_, partition)| *partition == &source_partition)
504 .enumerate()
505 {
506 if tensor_leaves.contains(&partition_tensor_index) {
507 shifted_tensor ^= self.tensor.tensor(i).as_leaf().unwrap();
508 shifted_indices.push(i);
509 }
510 }
511
512 let (target_partition, _) = partition_tensors
515 .iter()
516 .enumerate()
517 .filter_map(|(i, partition_tensor)| {
518 if i != source_partition {
519 Some((
520 i,
521 (&shifted_tensor ^ partition_tensor).size() - partition_tensor.size(),
522 ))
523 } else {
524 None
526 }
527 })
528 .min_by(|a, b| a.1.total_cmp(&b.1))
529 .unwrap();
530
531 for index in shifted_indices {
533 partitioning[index] = target_partition;
534 }
535
536 partition_tensors[source_partition] ^= &shifted_tensor;
538 partition_tensors[target_partition] ^= &shifted_tensor;
539
540 let mut from_tensor = CompositeTensor::default();
542 let mut to_tensor = CompositeTensor::default();
543 for (partition_index, tensor) in zip(&partitioning, self.tensor.tensors()) {
544 if *partition_index == source_partition {
545 from_tensor.push_tensor(tensor.clone());
546 } else if *partition_index == target_partition {
547 to_tensor.push_tensor(tensor.clone());
548 }
549 }
550
551 let mut from_opt = Cotengrust::new(OptMethod::Greedy);
552 let result = from_opt.find_path(&from_tensor);
553 let from_path = result.replace_path();
554 contraction_paths[source_partition] = from_path.into_simple();
555
556 let mut to_opt = Cotengrust::new(OptMethod::Greedy);
557 let result = to_opt.find_path(&to_tensor);
558 let to_path = result.replace_path();
559 contraction_paths[target_partition] = to_path.into_simple();
560
561 (partitioning, partition_tensors, contraction_paths)
562 }
563
564 fn evaluate<R: Rng>(&self, solution: &Self::SolutionType, rng: &mut R) -> ScoreType {
565 evaluate_partitioning(
566 self.tensor,
567 &solution.0,
568 self.communication_scheme,
569 self.memory_limit,
570 rng,
571 )
572 }
573}
574
575pub fn balance_partitions<R, M>(
577 model: M,
578 initial_solution: M::SolutionType,
579 rng: &mut R,
580 max_time: Duration,
581) -> (M::SolutionType, ScoreType)
582where
583 R: Rng,
584 M: OptModel,
585{
586 let optimizer = SimulatedAnnealingOptimizer {
587 n_trials: PROCESSING_THREADS,
588 max_time,
589 n_steps: PROCESSING_THREADS * 10,
590 restart_iter: 50,
591 initial_temperature: 2.0,
592 final_temperature: 0.05,
593 };
594 optimizer.optimize_with_temperature(&model, initial_solution, rng)
595}
596
597#[cfg(test)]
598mod tests {
599 use approx::assert_abs_diff_eq;
600
601 use super::*;
602
603 #[test]
604 fn simple_linear_interpolation() {
605 assert_abs_diff_eq!(linear_interpolation(0., 6., 0.5), 3.0);
606 assert_abs_diff_eq!(linear_interpolation(-1.0, 4.0, 0.2), 0.0);
607 assert_abs_diff_eq!(linear_interpolation(-7.0, -6.0, 0.0), -7.0);
608 assert_abs_diff_eq!(linear_interpolation(3.0, 5.0, 1.0), 5.0);
609 }
610
611 #[test]
612 fn small_leaf_partitioning() {
613 let t1 = LeafTensor::new_from_const(vec![0, 1], 2);
614 let t2 = LeafTensor::new_from_const(vec![2, 3], 2);
615 let t3 = LeafTensor::new_from_const(vec![0, 1, 4], 2);
616 let t4 = LeafTensor::new_from_const(vec![2, 3, 4], 2);
617 let tn = CompositeTensor::new(vec![t1.clone(), t2.clone(), t3.clone(), t4.clone()]);
618 let tn1 = CompositeTensor::new(vec![t1, t2]);
619 let tn2 = CompositeTensor::new(vec![t3, t4]);
620 let initial_partitioning = vec![0, 0, 1, 1];
621 let initial_partitions = vec![tn1.external_tensor(), tn2.external_tensor()];
622 let mut rng = StdRng::seed_from_u64(42);
623
624 let ((partitioning, _partitions), _) = balance_partitions(
625 LeafPartitioningModel {
626 tensor: &tn,
627 communication_scheme: CommunicationScheme::Greedy,
628 memory_limit: None,
629 },
630 (initial_partitioning, initial_partitions),
631 &mut rng,
632 Duration::from_secs(2),
633 );
634 let ref_partitioning = if partitioning[0] == 0 {
636 [0, 1, 0, 1]
637 } else {
638 [1, 0, 1, 0]
639 };
640 assert_eq!(partitioning, ref_partitioning);
641 }
642}