1use crate::prelude::SimulatorError;
7use scirs2_core::ndarray::{ArrayD, Dimension, IxDyn};
8use scirs2_core::parallel_ops::{
9 current_num_threads, IndexedParallelIterator, ParallelIterator, ThreadPool, ThreadPoolBuilder,
10};
11use scirs2_core::Complex64;
12use std::collections::{HashMap, HashSet, VecDeque};
13use std::sync::{Arc, Mutex, RwLock};
14use std::thread;
15use std::time::{Duration, Instant};
16
17use crate::error::Result;
18
19#[derive(Debug, Clone)]
21pub struct ParallelTensorConfig {
22 pub num_threads: usize,
24 pub chunk_size: usize,
26 pub enable_work_stealing: bool,
28 pub parallel_threshold_bytes: usize,
30 pub load_balancing: LoadBalancingStrategy,
32 pub numa_aware: bool,
34 pub thread_affinity: ThreadAffinityConfig,
36}
37
38impl Default for ParallelTensorConfig {
39 fn default() -> Self {
40 Self {
41 num_threads: current_num_threads(), chunk_size: 1024,
43 enable_work_stealing: true,
44 parallel_threshold_bytes: 1024 * 1024, load_balancing: LoadBalancingStrategy::DynamicWorkStealing,
46 numa_aware: true,
47 thread_affinity: ThreadAffinityConfig::default(),
48 }
49 }
50}
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54pub enum LoadBalancingStrategy {
55 RoundRobin,
57 DynamicWorkStealing,
59 NumaAware,
61 CostBased,
63 Adaptive,
65}
66
67#[derive(Debug, Clone, Default)]
69pub struct ThreadAffinityConfig {
70 pub enable_affinity: bool,
72 pub core_mapping: Vec<usize>,
74 pub numa_preferences: HashMap<usize, usize>,
76}
77
78#[derive(Debug, Clone)]
80pub struct TensorWorkUnit {
81 pub id: usize,
83 pub input_tensors: Vec<usize>,
85 pub output_tensor: usize,
87 pub contraction_indices: Vec<Vec<usize>>,
89 pub estimated_cost: f64,
91 pub memory_requirement: usize,
93 pub dependencies: HashSet<usize>,
95 pub priority: i32,
97}
98
99#[derive(Debug)]
101pub struct TensorWorkQueue {
102 pending: Mutex<VecDeque<TensorWorkUnit>>,
104 completed: RwLock<HashSet<usize>>,
106 in_progress: RwLock<HashMap<usize, Instant>>,
108 total_units: usize,
110 config: ParallelTensorConfig,
112}
113
114impl TensorWorkQueue {
115 #[must_use]
117 pub fn new(work_units: Vec<TensorWorkUnit>, config: ParallelTensorConfig) -> Self {
118 let total_units = work_units.len();
119 let mut pending = VecDeque::from(work_units);
120
121 pending.make_contiguous().sort_by(|a, b| {
123 b.priority
124 .cmp(&a.priority)
125 .then_with(|| a.dependencies.len().cmp(&b.dependencies.len()))
126 });
127
128 Self {
129 pending: Mutex::new(pending),
130 completed: RwLock::new(HashSet::new()),
131 in_progress: RwLock::new(HashMap::new()),
132 total_units,
133 config,
134 }
135 }
136
137 pub fn get_work(&self) -> Option<TensorWorkUnit> {
139 let mut pending = self
141 .pending
142 .lock()
143 .expect("pending lock should not be poisoned");
144 let completed = self
145 .completed
146 .read()
147 .expect("completed lock should not be poisoned");
148
149 for i in 0..pending.len() {
151 let work_unit = &pending[i];
152 let dependencies_satisfied = work_unit
153 .dependencies
154 .iter()
155 .all(|dep| completed.contains(dep));
156
157 if dependencies_satisfied {
158 let work_unit = pending
160 .remove(i)
161 .expect("index i is guaranteed to be within bounds");
162
163 drop(completed);
165 let mut in_progress = self
166 .in_progress
167 .write()
168 .expect("in_progress lock should not be poisoned");
169 in_progress.insert(work_unit.id, Instant::now());
170
171 return Some(work_unit);
172 }
173 }
174
175 None
176 }
177
178 pub fn complete_work(&self, work_id: usize) {
180 let mut completed = self
181 .completed
182 .write()
183 .expect("completed lock should not be poisoned");
184 completed.insert(work_id);
185
186 let mut in_progress = self
187 .in_progress
188 .write()
189 .expect("in_progress lock should not be poisoned");
190 in_progress.remove(&work_id);
191 }
192
193 pub fn is_complete(&self) -> bool {
195 let completed = self
196 .completed
197 .read()
198 .expect("completed lock should not be poisoned");
199 completed.len() == self.total_units
200 }
201
202 pub fn get_progress(&self) -> (usize, usize, usize) {
204 let completed = self
205 .completed
206 .read()
207 .expect("completed lock should not be poisoned")
208 .len();
209 let in_progress = self
210 .in_progress
211 .read()
212 .expect("in_progress lock should not be poisoned")
213 .len();
214 let pending = self
215 .pending
216 .lock()
217 .expect("pending lock should not be poisoned")
218 .len();
219 (completed, in_progress, pending)
220 }
221}
222
223pub struct ParallelTensorEngine {
225 config: ParallelTensorConfig,
227 thread_pool: ThreadPool, stats: Arc<Mutex<ParallelTensorStats>>,
231}
232
233#[derive(Debug, Clone, Default)]
235pub struct ParallelTensorStats {
236 pub total_contractions: u64,
238 pub total_computation_time: Duration,
240 pub parallel_efficiency: f64,
242 pub peak_memory_usage: usize,
244 pub thread_utilization: Vec<f64>,
246 pub load_balance_factor: f64,
248 pub cache_hit_rate: f64,
250}
251
252impl ParallelTensorEngine {
253 pub fn new(config: ParallelTensorConfig) -> Result<Self> {
255 let thread_pool = ThreadPoolBuilder::new() .num_threads(config.num_threads)
257 .build()
258 .map_err(|e| {
259 SimulatorError::InitializationFailed(format!("Thread pool creation failed: {e}"))
260 })?;
261
262 Ok(Self {
263 config,
264 thread_pool,
265 stats: Arc::new(Mutex::new(ParallelTensorStats::default())),
266 })
267 }
268
269 pub fn contract_network(
271 &self,
272 tensors: &[ArrayD<Complex64>],
273 contraction_sequence: &[ContractionPair],
274 ) -> Result<ArrayD<Complex64>> {
275 let start_time = Instant::now();
276
277 let work_units = self.create_work_units(tensors, contraction_sequence)?;
279
280 let work_queue = Arc::new(TensorWorkQueue::new(work_units, self.config.clone()));
282
283 let intermediate_results =
285 Arc::new(RwLock::new(HashMap::<usize, ArrayD<Complex64>>::new()));
286
287 {
289 let mut results = intermediate_results
290 .write()
291 .expect("intermediate_results lock should not be poisoned");
292 for (i, tensor) in tensors.iter().enumerate() {
293 results.insert(i, tensor.clone());
294 }
295 }
296
297 let final_result = self.execute_parallel_contractions(work_queue, intermediate_results)?;
299
300 let elapsed = start_time.elapsed();
302 let mut stats = self
303 .stats
304 .lock()
305 .expect("stats lock should not be poisoned");
306 stats.total_contractions += contraction_sequence.len() as u64;
307 stats.total_computation_time += elapsed;
308
309 let sequential_estimate = self.estimate_sequential_time(contraction_sequence);
311 stats.parallel_efficiency = sequential_estimate.as_secs_f64() / elapsed.as_secs_f64();
312
313 Ok(final_result)
314 }
315
316 fn create_work_units(
318 &self,
319 tensors: &[ArrayD<Complex64>],
320 contraction_sequence: &[ContractionPair],
321 ) -> Result<Vec<TensorWorkUnit>> {
322 let mut work_units: Vec<TensorWorkUnit> = Vec::new();
323 let mut next_tensor_id = tensors.len();
324
325 for (i, contraction) in contraction_sequence.iter().enumerate() {
326 let estimated_cost = self.estimate_contraction_cost(contraction, tensors)?;
327 let memory_requirement = self.estimate_memory_requirement(contraction, tensors)?;
328
329 let mut dependencies = HashSet::new();
331 for &input_id in &[contraction.tensor1_id, contraction.tensor2_id] {
332 if input_id >= tensors.len() {
333 for prev_unit in &work_units {
335 if prev_unit.output_tensor == input_id {
336 dependencies.insert(prev_unit.id);
337 break;
338 }
339 }
340 }
341 }
342
343 let work_unit = TensorWorkUnit {
344 id: i,
345 input_tensors: vec![contraction.tensor1_id, contraction.tensor2_id],
346 output_tensor: next_tensor_id,
347 contraction_indices: vec![
348 contraction.tensor1_indices.clone(),
349 contraction.tensor2_indices.clone(),
350 ],
351 estimated_cost,
352 memory_requirement,
353 dependencies,
354 priority: self.calculate_priority(estimated_cost, memory_requirement),
355 };
356
357 work_units.push(work_unit);
358 next_tensor_id += 1;
359 }
360
361 Ok(work_units)
362 }
363
364 fn execute_parallel_contractions(
366 &self,
367 work_queue: Arc<TensorWorkQueue>,
368 intermediate_results: Arc<RwLock<HashMap<usize, ArrayD<Complex64>>>>,
369 ) -> Result<ArrayD<Complex64>> {
370 let num_threads = self.config.num_threads;
371 let mut handles = Vec::new();
372
373 for thread_id in 0..num_threads {
375 let work_queue = work_queue.clone();
376 let intermediate_results = intermediate_results.clone();
377 let config = self.config.clone();
378
379 let handle = thread::spawn(move || {
380 Self::worker_thread(thread_id, work_queue, intermediate_results, config)
381 });
382 handles.push(handle);
383 }
384
385 for handle in handles {
387 handle.join().map_err(|e| {
388 SimulatorError::ComputationError(format!("Thread join failed: {e:?}"))
389 })??;
390 }
391
392 let results = intermediate_results
394 .read()
395 .expect("intermediate_results lock should not be poisoned");
396 let max_id = results.keys().max().copied().unwrap_or(0);
397 Ok(results[&max_id].clone())
398 }
399
400 fn worker_thread(
402 _thread_id: usize,
403 work_queue: Arc<TensorWorkQueue>,
404 intermediate_results: Arc<RwLock<HashMap<usize, ArrayD<Complex64>>>>,
405 _config: ParallelTensorConfig,
406 ) -> Result<()> {
407 while !work_queue.is_complete() {
408 if let Some(work_unit) = work_queue.get_work() {
409 let tensor1 = {
411 let results = intermediate_results
412 .read()
413 .expect("intermediate_results lock should not be poisoned");
414 results[&work_unit.input_tensors[0]].clone()
415 };
416
417 let tensor2 = {
418 let results = intermediate_results
419 .read()
420 .expect("intermediate_results lock should not be poisoned");
421 results[&work_unit.input_tensors[1]].clone()
422 };
423
424 let result = Self::perform_tensor_contraction(
426 &tensor1,
427 &tensor2,
428 &work_unit.contraction_indices[0],
429 &work_unit.contraction_indices[1],
430 )?;
431
432 {
434 let mut results = intermediate_results
435 .write()
436 .expect("intermediate_results lock should not be poisoned");
437 results.insert(work_unit.output_tensor, result);
438 }
439
440 work_queue.complete_work(work_unit.id);
442 } else {
443 thread::sleep(Duration::from_millis(1));
445 }
446 }
447
448 Ok(())
449 }
450
451 fn perform_tensor_contraction(
462 tensor1: &ArrayD<Complex64>,
463 tensor2: &ArrayD<Complex64>,
464 indices1: &[usize],
465 indices2: &[usize],
466 ) -> Result<ArrayD<Complex64>> {
467 let shape1 = tensor1.shape().to_vec();
468 let shape2 = tensor2.shape().to_vec();
469
470 if indices1.len() != indices2.len() {
471 return Err(SimulatorError::InvalidInput(format!(
472 "Contraction requires equal numbers of contracted axes, got {} and {}",
473 indices1.len(),
474 indices2.len()
475 )));
476 }
477 for (&a, &b) in indices1.iter().zip(indices2.iter()) {
478 if a >= shape1.len() || b >= shape2.len() {
479 return Err(SimulatorError::InvalidInput(format!(
480 "Contracted axis out of range: tensor1 axis {a} (rank {}), tensor2 axis {b} (rank {})",
481 shape1.len(),
482 shape2.len()
483 )));
484 }
485 if shape1[a] != shape2[b] {
486 return Err(SimulatorError::InvalidInput(format!(
487 "Contracted dimension mismatch: tensor1 axis {a} has size {}, tensor2 axis {b} has size {}",
488 shape1[a], shape2[b]
489 )));
490 }
491 }
492
493 let free1: Vec<usize> = (0..shape1.len())
495 .filter(|i| !indices1.contains(i))
496 .collect();
497 let free2: Vec<usize> = (0..shape2.len())
498 .filter(|i| !indices2.contains(i))
499 .collect();
500
501 let free1_dims: Vec<usize> = free1.iter().map(|&i| shape1[i]).collect();
502 let free2_dims: Vec<usize> = free2.iter().map(|&i| shape2[i]).collect();
503 let contracted_dims: Vec<usize> = indices1.iter().map(|&i| shape1[i]).collect();
504
505 let output_shape: Vec<usize> = free1_dims
506 .iter()
507 .copied()
508 .chain(free2_dims.iter().copied())
509 .collect();
510 let mut output = ArrayD::zeros(IxDyn(&output_shape));
511
512 let free1_size: usize = free1_dims.iter().product::<usize>().max(1);
513 let free2_size: usize = free2_dims.iter().product::<usize>().max(1);
514 let contracted_size: usize = contracted_dims.iter().product::<usize>().max(1);
515
516 let mut idx1 = vec![0usize; shape1.len()];
518 let mut idx2 = vec![0usize; shape2.len()];
519 let mut out_idx = vec![0usize; output_shape.len()];
520
521 for f1 in 0..free1_size {
522 let f1_idx = unravel_index(f1, &free1_dims);
523 for (pos, &axis) in free1.iter().enumerate() {
524 idx1[axis] = f1_idx[pos];
525 out_idx[pos] = f1_idx[pos];
526 }
527 for f2 in 0..free2_size {
528 let f2_idx = unravel_index(f2, &free2_dims);
529 for (pos, &axis) in free2.iter().enumerate() {
530 idx2[axis] = f2_idx[pos];
531 out_idx[free1.len() + pos] = f2_idx[pos];
532 }
533
534 let mut acc = Complex64::new(0.0, 0.0);
535 for c in 0..contracted_size {
536 let c_idx = unravel_index(c, &contracted_dims);
537 for (pos, (&a, &b)) in indices1.iter().zip(indices2.iter()).enumerate() {
538 idx1[a] = c_idx[pos];
539 idx2[b] = c_idx[pos];
540 }
541 acc += tensor1[IxDyn(&idx1)] * tensor2[IxDyn(&idx2)];
542 }
543 output[IxDyn(&out_idx)] = acc;
544 }
545 }
546
547 Ok(output)
548 }
549
550 fn estimate_contraction_cost(
552 &self,
553 contraction: &ContractionPair,
554 _tensors: &[ArrayD<Complex64>],
555 ) -> Result<f64> {
556 let cost = contraction.tensor1_indices.len() as f64
558 * contraction.tensor2_indices.len() as f64
559 * 1000.0; Ok(cost)
561 }
562
563 const fn estimate_memory_requirement(
565 &self,
566 _contraction: &ContractionPair,
567 _tensors: &[ArrayD<Complex64>],
568 ) -> Result<usize> {
569 Ok(1024 * 1024) }
572
573 fn calculate_priority(&self, cost: f64, memory: usize) -> i32 {
575 let cost_factor = (cost / 1000.0) as i32;
577 let memory_factor = (1_000_000 / (memory + 1)) as i32;
578 cost_factor + memory_factor
579 }
580
581 const fn estimate_sequential_time(&self, contraction_sequence: &[ContractionPair]) -> Duration {
583 let estimated_ops = contraction_sequence.len() as u64 * 1000; Duration::from_millis(estimated_ops)
585 }
586
587 #[must_use]
589 pub fn get_stats(&self) -> ParallelTensorStats {
590 self.stats
591 .lock()
592 .expect("stats lock should not be poisoned")
593 .clone()
594 }
595}
596
597#[derive(Debug, Clone)]
599pub struct ContractionPair {
600 pub tensor1_id: usize,
602 pub tensor2_id: usize,
604 pub tensor1_indices: Vec<usize>,
606 pub tensor2_indices: Vec<usize>,
608}
609
610pub mod strategies {
612 use super::{
613 ArrayD, Complex64, ContractionPair, LoadBalancingStrategy, NumaTopology,
614 ParallelTensorConfig, ParallelTensorEngine, Result,
615 };
616
617 pub fn work_stealing_contraction(
619 tensors: &[ArrayD<Complex64>],
620 contraction_sequence: &[ContractionPair],
621 num_threads: usize,
622 ) -> Result<ArrayD<Complex64>> {
623 let config = ParallelTensorConfig {
624 num_threads,
625 load_balancing: LoadBalancingStrategy::DynamicWorkStealing,
626 ..Default::default()
627 };
628
629 let engine = ParallelTensorEngine::new(config)?;
630 engine.contract_network(tensors, contraction_sequence)
631 }
632
633 pub fn numa_aware_contraction(
635 tensors: &[ArrayD<Complex64>],
636 contraction_sequence: &[ContractionPair],
637 numa_topology: &NumaTopology,
638 ) -> Result<ArrayD<Complex64>> {
639 let config = ParallelTensorConfig {
640 load_balancing: LoadBalancingStrategy::NumaAware,
641 numa_aware: true,
642 ..Default::default()
643 };
644
645 let engine = ParallelTensorEngine::new(config)?;
646 engine.contract_network(tensors, contraction_sequence)
647 }
648
649 pub fn adaptive_contraction(
651 tensors: &[ArrayD<Complex64>],
652 contraction_sequence: &[ContractionPair],
653 ) -> Result<ArrayD<Complex64>> {
654 let config = ParallelTensorConfig {
655 load_balancing: LoadBalancingStrategy::Adaptive,
656 enable_work_stealing: true,
657 ..Default::default()
658 };
659
660 let engine = ParallelTensorEngine::new(config)?;
661 engine.contract_network(tensors, contraction_sequence)
662 }
663}
664
665#[derive(Debug, Clone)]
667pub struct NumaTopology {
668 pub num_nodes: usize,
670 pub cores_per_node: Vec<usize>,
672 pub memory_per_node: Vec<usize>,
674}
675
676impl Default for NumaTopology {
677 fn default() -> Self {
678 let num_cores = current_num_threads(); Self {
680 num_nodes: 1,
681 cores_per_node: vec![num_cores],
682 memory_per_node: vec![8 * 1024 * 1024 * 1024], }
684 }
685}
686
687fn unravel_index(mut linear: usize, dims: &[usize]) -> Vec<usize> {
692 let mut multi = vec![0usize; dims.len()];
693 for i in (0..dims.len()).rev() {
694 let d = dims[i];
695 multi[i] = linear % d;
696 linear /= d;
697 }
698 multi
699}
700
701#[cfg(test)]
702mod tests {
703 use super::*;
704 use scirs2_core::ndarray::Array;
705
706 #[test]
707 fn test_parallel_tensor_engine() {
708 let config = ParallelTensorConfig::default();
709 let engine =
710 ParallelTensorEngine::new(config).expect("should create parallel tensor engine");
711
712 let tensor1 = Array::zeros(IxDyn(&[2, 2]));
714 let tensor2 = Array::zeros(IxDyn(&[2, 2]));
715 let tensors = vec![tensor1, tensor2];
716
717 let contraction = ContractionPair {
719 tensor1_id: 0,
720 tensor2_id: 1,
721 tensor1_indices: vec![1],
722 tensor2_indices: vec![0],
723 };
724
725 let result = engine.contract_network(&tensors, &[contraction]);
726 assert!(result.is_ok());
727 }
728
729 #[test]
730 fn test_work_queue() {
731 let work_unit = TensorWorkUnit {
732 id: 0,
733 input_tensors: vec![0, 1],
734 output_tensor: 2,
735 contraction_indices: vec![vec![0], vec![1]],
736 estimated_cost: 100.0,
737 memory_requirement: 1024,
738 dependencies: HashSet::new(),
739 priority: 1,
740 };
741
742 let config = ParallelTensorConfig::default();
743 let queue = TensorWorkQueue::new(vec![work_unit], config);
744
745 let work = queue.get_work();
746 assert!(work.is_some());
747
748 queue.complete_work(0);
749 assert!(queue.is_complete());
750 }
751
752 #[test]
753 fn test_parallel_strategies() {
754 let tensor1 = Array::ones(IxDyn(&[2, 2]));
755 let tensor2 = Array::ones(IxDyn(&[2, 2]));
756 let tensors = vec![tensor1, tensor2];
757
758 let contraction = ContractionPair {
759 tensor1_id: 0,
760 tensor2_id: 1,
761 tensor1_indices: vec![1],
762 tensor2_indices: vec![0],
763 };
764
765 let result = strategies::work_stealing_contraction(&tensors, &[contraction], 2);
766 assert!(result.is_ok());
767 }
768
769 #[test]
773 fn test_perform_tensor_contraction_matrix_product() {
774 let a = Array::from_shape_vec(
776 IxDyn(&[2, 3]),
777 vec![
778 Complex64::new(1.0, 0.0),
779 Complex64::new(2.0, 0.0),
780 Complex64::new(3.0, 0.0),
781 Complex64::new(4.0, 0.0),
782 Complex64::new(5.0, 0.0),
783 Complex64::new(6.0, 0.0),
784 ],
785 )
786 .expect("A shape");
787 let b = Array::from_shape_vec(
788 IxDyn(&[3, 2]),
789 vec![
790 Complex64::new(1.0, 0.0),
791 Complex64::new(0.0, 0.0),
792 Complex64::new(0.0, 0.0),
793 Complex64::new(1.0, 0.0),
794 Complex64::new(1.0, 0.0),
795 Complex64::new(1.0, 0.0),
796 ],
797 )
798 .expect("B shape");
799
800 let out = ParallelTensorEngine::perform_tensor_contraction(&a, &b, &[1], &[0])
801 .expect("contraction should succeed");
802 assert_eq!(out.shape(), &[2, 2]);
803 assert!((out[IxDyn(&[0, 0])] - Complex64::new(4.0, 0.0)).norm() < 1e-12);
805 assert!((out[IxDyn(&[0, 1])] - Complex64::new(5.0, 0.0)).norm() < 1e-12);
806 assert!((out[IxDyn(&[1, 0])] - Complex64::new(10.0, 0.0)).norm() < 1e-12);
807 assert!((out[IxDyn(&[1, 1])] - Complex64::new(11.0, 0.0)).norm() < 1e-12);
808 }
809
810 #[test]
812 fn test_perform_tensor_contraction_outer_product() {
813 let a = Array::from_shape_vec(
814 IxDyn(&[2]),
815 vec![Complex64::new(2.0, 0.0), Complex64::new(3.0, 0.0)],
816 )
817 .expect("A shape");
818 let b = Array::from_shape_vec(
819 IxDyn(&[2]),
820 vec![Complex64::new(5.0, 0.0), Complex64::new(7.0, 0.0)],
821 )
822 .expect("B shape");
823
824 let out = ParallelTensorEngine::perform_tensor_contraction(&a, &b, &[], &[])
825 .expect("outer product should succeed");
826 assert_eq!(out.shape(), &[2, 2]);
827 assert!((out[IxDyn(&[0, 0])] - Complex64::new(10.0, 0.0)).norm() < 1e-12);
828 assert!((out[IxDyn(&[0, 1])] - Complex64::new(14.0, 0.0)).norm() < 1e-12);
829 assert!((out[IxDyn(&[1, 0])] - Complex64::new(15.0, 0.0)).norm() < 1e-12);
830 assert!((out[IxDyn(&[1, 1])] - Complex64::new(21.0, 0.0)).norm() < 1e-12);
831 }
832}