1use std::collections::HashMap;
7use std::sync::{Arc, Mutex};
8use torsh_core::error::{Result, TorshError};
9use torsh_core::sync::MutexExt;
10use torsh_core::Shape;
11
12#[derive(Debug, Clone)]
14pub enum BroadcastError {
15 IncompatibleShapes {
17 shape1: Vec<usize>,
18 shape2: Vec<usize>,
19 reason: String,
20 },
21 DimensionMismatch {
23 dim: usize,
24 size1: usize,
25 size2: usize,
26 },
27 ShapeOverflow { attempted_shape: Vec<usize> },
29 MemoryError {
31 required_size: usize,
32 available_size: Option<usize>,
33 },
34}
35
36impl std::fmt::Display for BroadcastError {
37 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
38 match self {
39 BroadcastError::IncompatibleShapes {
40 shape1,
41 shape2,
42 reason,
43 } => {
44 write!(
45 f,
46 "Cannot broadcast shapes {shape1:?} and {shape2:?}: {reason}"
47 )
48 }
49 BroadcastError::DimensionMismatch { dim, size1, size2 } => {
50 write!(
51 f,
52 "Dimension {dim} mismatch: {size1} vs {size2} (neither is 1)"
53 )
54 }
55 BroadcastError::ShapeOverflow { attempted_shape } => {
56 write!(
57 f,
58 "Broadcast shape {attempted_shape:?} would overflow memory limits"
59 )
60 }
61 BroadcastError::MemoryError {
62 required_size,
63 available_size,
64 } => {
65 if let Some(available) = available_size {
66 write!(
67 f,
68 "Insufficient memory for broadcast: need {required_size} bytes, have {available} bytes"
69 )
70 } else {
71 write!(f, "Memory allocation failed for {required_size} bytes")
72 }
73 }
74 }
75 }
76}
77
78impl std::error::Error for BroadcastError {}
79
80pub struct BroadcastOps;
82
83impl BroadcastOps {
84 pub fn are_shapes_compatible(shape1: &[usize], shape2: &[usize]) -> Result<bool> {
90 let ndim1 = shape1.len();
91 let ndim2 = shape2.len();
92 let max_ndim = ndim1.max(ndim2);
93
94 for i in 0..max_ndim {
95 let dim1 = if i < ndim1 {
96 shape1[ndim1 - 1 - i]
97 } else {
98 1 };
100
101 let dim2 = if i < ndim2 {
102 shape2[ndim2 - 1 - i]
103 } else {
104 1 };
106
107 if dim1 != dim2 && dim1 != 1 && dim2 != 1 {
109 return Ok(false);
110 }
111 }
112
113 Ok(true)
114 }
115
116 pub fn compute_broadcast_shape(shape1: &[usize], shape2: &[usize]) -> Result<Vec<usize>> {
118 if !Self::are_shapes_compatible(shape1, shape2)? {
119 return Err(TorshError::BroadcastError {
120 shape1: shape1.to_vec(),
121 shape2: shape2.to_vec(),
122 });
123 }
124
125 let ndim1 = shape1.len();
126 let ndim2 = shape2.len();
127 let max_ndim = ndim1.max(ndim2);
128 let mut result_shape = Vec::with_capacity(max_ndim);
129
130 for i in 0..max_ndim {
131 let dim1 = if i < ndim1 { shape1[ndim1 - 1 - i] } else { 1 };
132
133 let dim2 = if i < ndim2 { shape2[ndim2 - 1 - i] } else { 1 };
134
135 let result_dim = dim1.max(dim2);
137
138 if result_dim > usize::MAX / 2 {
140 return Err(TorshError::InvalidArgument(
141 BroadcastError::ShapeOverflow {
142 attempted_shape: result_shape.clone(),
143 }
144 .to_string(),
145 ));
146 }
147
148 result_shape.push(result_dim);
149 }
150
151 result_shape.reverse();
153
154 let total_elements = result_shape.iter().product::<usize>();
156 if total_elements > isize::MAX as usize {
157 return Err(TorshError::InvalidArgument(
158 BroadcastError::ShapeOverflow {
159 attempted_shape: result_shape,
160 }
161 .to_string(),
162 ));
163 }
164
165 Ok(result_shape)
166 }
167
168 pub fn compute_broadcast_index(
170 multi_index: &[usize],
171 original_shape: &[usize],
172 broadcast_shape: &[usize],
173 ) -> Result<usize> {
174 let orig_ndim = original_shape.len();
175 let broadcast_ndim = broadcast_shape.len();
176
177 if multi_index.len() != broadcast_ndim {
178 return Err(TorshError::InvalidArgument(format!(
179 "Multi-index length {} doesn't match broadcast shape dimensions {}",
180 multi_index.len(),
181 broadcast_ndim
182 )));
183 }
184
185 let mut linear_index = 0;
186 let mut stride = 1;
187
188 for i in 0..broadcast_ndim {
190 let broadcast_dim_idx = broadcast_ndim - 1 - i;
191 let broadcast_coord = multi_index[broadcast_dim_idx];
192
193 let orig_coord = if i < orig_ndim {
195 let orig_dim_idx = orig_ndim - 1 - i;
196 let orig_dim_size = original_shape[orig_dim_idx];
197
198 if orig_dim_size == 1 {
199 0 } else {
201 broadcast_coord }
203 } else {
204 0 };
206
207 linear_index += orig_coord * stride;
209
210 if i < orig_ndim {
212 let orig_dim_idx = orig_ndim - 1 - i;
213 stride *= original_shape[orig_dim_idx];
214 }
215 }
216
217 Ok(linear_index)
218 }
219
220 pub fn flat_to_multi_index(flat_index: usize, shape: &[usize]) -> Vec<usize> {
226 if shape.contains(&0) {
227 return vec![0; shape.len()];
228 }
229
230 let mut multi_index = Vec::with_capacity(shape.len());
231 let mut remaining = flat_index;
232
233 for &dim_size in shape.iter().rev() {
234 multi_index.push(remaining % dim_size);
235 remaining /= dim_size;
236 }
237
238 multi_index.reverse();
239 multi_index
240 }
241
242 pub fn validate_broadcast_operation(
244 shape1: &[usize],
245 shape2: &[usize],
246 operation_name: &str,
247 ) -> Result<()> {
248 if shape1.contains(&0) || shape2.contains(&0) {
253 return Err(TorshError::InvalidArgument(format!(
254 "Cannot perform {operation_name} operation on tensors with zero-sized dimensions"
255 )));
256 }
257
258 const MAX_DIMENSIONS: usize = 32; if shape1.len() > MAX_DIMENSIONS || shape2.len() > MAX_DIMENSIONS {
261 return Err(TorshError::InvalidArgument(format!(
262 "Too many dimensions for {operation_name} operation (max: {MAX_DIMENSIONS})"
263 )));
264 }
265
266 if !Self::are_shapes_compatible(shape1, shape2)? {
268 return Err(TorshError::InvalidArgument(
269 BroadcastError::IncompatibleShapes {
270 shape1: shape1.to_vec(),
271 shape2: shape2.to_vec(),
272 reason: format!("Shapes not compatible for {operation_name} operation"),
273 }
274 .to_string(),
275 ));
276 }
277
278 Ok(())
279 }
280
281 pub fn estimate_broadcast_memory(
283 shape1: &[usize],
284 shape2: &[usize],
285 element_size: usize,
286 ) -> Result<usize> {
287 let broadcast_shape = Self::compute_broadcast_shape(shape1, shape2)?;
288 let num_elements = broadcast_shape.iter().product::<usize>();
289
290 let memory_required = num_elements.checked_mul(element_size).ok_or_else(|| {
292 TorshError::InvalidArgument(
293 BroadcastError::MemoryError {
294 required_size: usize::MAX,
295 available_size: None,
296 }
297 .to_string(),
298 )
299 })?;
300
301 Ok(memory_required)
302 }
303
304 pub fn get_broadcast_info(shape1: &[usize], shape2: &[usize]) -> Result<BroadcastInfo> {
313 if shape1.contains(&0) || shape2.contains(&0) {
314 return Err(TorshError::InvalidShape(format!(
315 "Cannot describe broadcasting for zero-sized shapes {shape1:?} and {shape2:?}"
316 )));
317 }
318
319 let broadcast_shape = Self::compute_broadcast_shape(shape1, shape2)?;
320 let expansion_factor1 =
321 broadcast_shape.iter().product::<usize>() / shape1.iter().product::<usize>();
322 let expansion_factor2 =
323 broadcast_shape.iter().product::<usize>() / shape2.iter().product::<usize>();
324
325 Ok(BroadcastInfo {
326 original_shape1: shape1.to_vec(),
327 original_shape2: shape2.to_vec(),
328 broadcast_shape,
329 expansion_factor1,
330 expansion_factor2,
331 is_memory_efficient: expansion_factor1 <= 2 && expansion_factor2 <= 2,
332 })
333 }
334
335 pub fn compute_broadcast_strides(
337 shape1: &[usize],
338 shape2: &[usize],
339 broadcast_shape: &[usize],
340 ) -> Result<BroadcastStrides> {
341 let original_strides1 = Self::compute_strides(shape1);
342 let original_strides2 = Self::compute_strides(shape2);
343
344 let broadcast_strides1 =
345 Self::compute_broadcast_strides_for_shape(shape1, broadcast_shape, &original_strides1)?;
346 let broadcast_strides2 =
347 Self::compute_broadcast_strides_for_shape(shape2, broadcast_shape, &original_strides2)?;
348
349 Ok(BroadcastStrides {
350 original_strides1,
351 original_strides2,
352 broadcast_strides1,
353 broadcast_strides2,
354 broadcast_shape: broadcast_shape.to_vec(),
355 })
356 }
357
358 fn compute_strides(shape: &[usize]) -> Vec<usize> {
360 if shape.is_empty() {
361 return Vec::new();
362 }
363
364 let mut strides = vec![1; shape.len()];
365 for i in (0..shape.len().saturating_sub(1)).rev() {
366 strides[i] = strides[i + 1] * shape[i + 1];
367 }
368 strides
369 }
370
371 fn compute_broadcast_strides_for_shape(
373 original_shape: &[usize],
374 broadcast_shape: &[usize],
375 original_strides: &[usize],
376 ) -> Result<Vec<usize>> {
377 let orig_ndim = original_shape.len();
378 let broadcast_ndim = broadcast_shape.len();
379 let mut broadcast_strides = vec![0; broadcast_ndim];
380
381 for i in 0..broadcast_ndim {
382 let broadcast_dim_idx = broadcast_ndim - 1 - i;
383
384 if i < orig_ndim {
385 let orig_dim_idx = orig_ndim - 1 - i;
386 let orig_size = original_shape[orig_dim_idx];
387 let broadcast_size = broadcast_shape[broadcast_dim_idx];
388
389 if orig_size == broadcast_size {
390 broadcast_strides[broadcast_dim_idx] = original_strides[orig_dim_idx];
392 } else if orig_size == 1 {
393 broadcast_strides[broadcast_dim_idx] = 0;
395 } else {
396 return Err(TorshError::InvalidArgument(format!(
397 "Cannot broadcast dimension {orig_dim_idx}: original size {orig_size}, broadcast size {broadcast_size}"
398 )));
399 }
400 } else {
401 broadcast_strides[broadcast_dim_idx] = 0;
403 }
404 }
405
406 Ok(broadcast_strides)
407 }
408
409 pub fn detect_broadcast_pattern(shape1: &[usize], shape2: &[usize]) -> BroadcastPattern {
411 if shape1.is_empty() || shape2.is_empty() {
413 return BroadcastPattern::Scalar;
414 }
415
416 if shape1 == shape2 {
418 return BroadcastPattern::ElementWise;
419 }
420
421 if (shape1.len() == 2 && shape2.len() == 1) || (shape1.len() == 1 && shape2.len() == 2) {
423 return BroadcastPattern::MatrixVector;
424 }
425
426 if shape1.len() == 1 || shape2.len() == 1 {
428 return BroadcastPattern::VectorScalar;
429 }
430
431 let has_size_1_dims = shape1.contains(&1) || shape2.contains(&1);
433 if has_size_1_dims {
434 return BroadcastPattern::Size1Dimension;
435 }
436
437 BroadcastPattern::General
439 }
440
441 pub fn create_broadcast_preview(
443 shape1: &[usize],
444 shape2: &[usize],
445 element_size: usize,
446 ) -> BroadcastPreview {
447 let compatible = Self::are_shapes_compatible(shape1, shape2).unwrap_or_default();
449
450 if !compatible {
451 return BroadcastPreview {
452 success: false,
453 broadcast_shape: None,
454 memory_required: None,
455 expansion_factor1: None,
456 expansion_factor2: None,
457 is_memory_efficient: false,
458 operation_cost: OperationCost {
459 computational_complexity: 0,
460 memory_access_pattern: MemoryAccessPattern::Sequential,
461 cache_efficiency: 0.0,
462 estimated_runtime_ms: 0.0,
463 },
464 error_message: Some("Shapes are not compatible for broadcasting".to_string()),
465 };
466 }
467
468 let broadcast_shape = match Self::compute_broadcast_shape(shape1, shape2) {
470 Ok(shape) => shape,
471 Err(e) => {
472 return BroadcastPreview {
473 success: false,
474 broadcast_shape: None,
475 memory_required: None,
476 expansion_factor1: None,
477 expansion_factor2: None,
478 is_memory_efficient: false,
479 operation_cost: OperationCost {
480 computational_complexity: 0,
481 memory_access_pattern: MemoryAccessPattern::Sequential,
482 cache_efficiency: 0.0,
483 estimated_runtime_ms: 0.0,
484 },
485 error_message: Some(format!("Error computing broadcast shape: {e}")),
486 };
487 }
488 };
489
490 let memory_required = Self::estimate_broadcast_memory(shape1, shape2, element_size).ok();
491
492 let info = Self::get_broadcast_info(shape1, shape2)
493 .expect("broadcast info should be available after shape validation");
494 let pattern = Self::detect_broadcast_pattern(shape1, shape2);
495 let cost = Self::estimate_operation_cost(&pattern, &broadcast_shape, element_size);
496
497 BroadcastPreview {
498 success: true,
499 broadcast_shape: Some(broadcast_shape),
500 memory_required,
501 expansion_factor1: Some(info.expansion_factor1),
502 expansion_factor2: Some(info.expansion_factor2),
503 is_memory_efficient: info.is_memory_efficient,
504 operation_cost: cost,
505 error_message: None,
506 }
507 }
508
509 fn estimate_operation_cost(
511 pattern: &BroadcastPattern,
512 broadcast_shape: &[usize],
513 element_size: usize,
514 ) -> OperationCost {
515 let num_elements = broadcast_shape.iter().product::<usize>();
516 let memory_bytes = num_elements * element_size;
517
518 let (complexity, access_pattern, cache_efficiency, runtime_factor) = match pattern {
519 BroadcastPattern::Scalar => (num_elements, MemoryAccessPattern::Sequential, 0.95, 1.0),
520 BroadcastPattern::ElementWise => {
521 (num_elements, MemoryAccessPattern::Sequential, 0.9, 1.0)
522 }
523 BroadcastPattern::VectorScalar => {
524 (num_elements, MemoryAccessPattern::Sequential, 0.8, 1.2)
525 }
526 BroadcastPattern::MatrixVector => {
527 let stride = broadcast_shape.last().unwrap_or(&1);
528 (
529 num_elements,
530 MemoryAccessPattern::Strided { stride: *stride },
531 0.7,
532 1.5,
533 )
534 }
535 BroadcastPattern::Size1Dimension => {
536 (num_elements, MemoryAccessPattern::Random, 0.6, 2.0)
537 }
538 BroadcastPattern::General => (num_elements, MemoryAccessPattern::Random, 0.5, 2.5),
539 };
540
541 let estimated_runtime_ms = (memory_bytes as f64 / 1e9) * runtime_factor; OperationCost {
545 computational_complexity: complexity,
546 memory_access_pattern: access_pattern,
547 cache_efficiency,
548 estimated_runtime_ms,
549 }
550 }
551}
552
553#[derive(Debug, Clone, PartialEq)]
555pub enum BroadcastPattern {
556 Scalar,
558 ElementWise,
560 VectorScalar,
562 MatrixVector,
564 Size1Dimension,
566 General,
568}
569
570#[derive(Debug, Clone)]
572pub struct BroadcastInfo {
573 pub original_shape1: Vec<usize>,
574 pub original_shape2: Vec<usize>,
575 pub broadcast_shape: Vec<usize>,
576 pub expansion_factor1: usize,
577 pub expansion_factor2: usize,
578 pub is_memory_efficient: bool,
579}
580
581#[derive(Debug, Clone, PartialEq, Eq, Hash)]
583pub struct BroadcastStrides {
584 pub original_strides1: Vec<usize>,
585 pub original_strides2: Vec<usize>,
586 pub broadcast_strides1: Vec<usize>,
587 pub broadcast_strides2: Vec<usize>,
588 pub broadcast_shape: Vec<usize>,
589}
590
591#[derive(Debug, Clone, PartialEq, Eq, Hash)]
593struct BroadcastCacheKey {
594 shape1: Vec<usize>,
595 shape2: Vec<usize>,
596}
597
598#[derive(Debug, Clone)]
600pub struct BroadcastCacheEntry {
601 pub broadcast_shape: Vec<usize>,
602 pub strides: BroadcastStrides,
603 pub info: BroadcastInfo,
604 access_count: usize,
605 last_accessed: std::time::SystemTime,
606}
607
608static BROADCAST_CACHE: std::sync::LazyLock<
610 Arc<Mutex<HashMap<BroadcastCacheKey, BroadcastCacheEntry>>>,
611> = std::sync::LazyLock::new(|| Arc::new(Mutex::new(HashMap::new())));
612
613pub struct BroadcastCache;
615
616impl BroadcastCache {
617 pub fn get_or_compute(
619 shape1: &[usize],
620 shape2: &[usize],
621 config: &BroadcastCacheConfig,
622 ) -> Result<BroadcastCacheEntry> {
623 if !config.enable_cache {
624 return Self::compute_fresh(shape1, shape2);
625 }
626
627 let key = BroadcastCacheKey {
628 shape1: shape1.to_vec(),
629 shape2: shape2.to_vec(),
630 };
631
632 let mut cache = BROADCAST_CACHE.lock_or_recover();
633
634 if let Some(entry) = cache.get_mut(&key) {
636 let now = std::time::SystemTime::now();
637 let age = now
638 .duration_since(entry.last_accessed)
639 .unwrap_or_default()
640 .as_secs();
641
642 if age < config.ttl_seconds {
643 entry.access_count += 1;
644 entry.last_accessed = now;
645 return Ok(entry.clone());
646 } else {
647 cache.remove(&key);
649 }
650 }
651
652 let mut entry = Self::compute_fresh(shape1, shape2)?;
654 entry.access_count = 1;
655 entry.last_accessed = std::time::SystemTime::now();
656
657 if cache.len() >= config.max_entries {
659 Self::evict_lru(&mut cache);
660 }
661
662 cache.insert(key, entry.clone());
664 Ok(entry)
665 }
666
667 fn compute_fresh(shape1: &[usize], shape2: &[usize]) -> Result<BroadcastCacheEntry> {
669 let broadcast_shape = BroadcastOps::compute_broadcast_shape(shape1, shape2)?;
670 let strides = BroadcastOps::compute_broadcast_strides(shape1, shape2, &broadcast_shape)?;
671 let info = BroadcastOps::get_broadcast_info(shape1, shape2)?;
672
673 Ok(BroadcastCacheEntry {
674 broadcast_shape,
675 strides,
676 info,
677 access_count: 0,
678 last_accessed: std::time::SystemTime::now(),
679 })
680 }
681
682 fn evict_lru(cache: &mut HashMap<BroadcastCacheKey, BroadcastCacheEntry>) {
684 if let Some((oldest_key, _)) = cache
685 .iter()
686 .min_by_key(|(_, entry)| entry.last_accessed)
687 .map(|(k, v)| (k.clone(), v.clone()))
688 {
689 cache.remove(&oldest_key);
690 }
691 }
692
693 pub fn clear() {
695 let mut cache = BROADCAST_CACHE.lock_or_recover();
696 cache.clear();
697 }
698
699 pub fn get_stats() -> BroadcastCacheStats {
701 let cache = BROADCAST_CACHE.lock_or_recover();
702 let total_accesses: usize = cache.values().map(|entry| entry.access_count).sum();
703
704 BroadcastCacheStats {
705 total_entries: cache.len(),
706 total_accesses,
707 hit_rate: if total_accesses > 0 {
708 cache.len() as f64 / total_accesses as f64
709 } else {
710 0.0
711 },
712 }
713 }
714}
715
716#[derive(Debug, Clone)]
718pub struct BroadcastCacheStats {
719 pub total_entries: usize,
720 pub total_accesses: usize,
721 pub hit_rate: f64,
722}
723
724pub struct BroadcastCacheConfig {
726 pub max_entries: usize,
727 pub ttl_seconds: u64,
728 pub enable_cache: bool,
729}
730
731impl Default for BroadcastCacheConfig {
732 fn default() -> Self {
733 Self {
734 max_entries: 1000,
735 ttl_seconds: 300, enable_cache: true,
737 }
738 }
739}
740
741#[derive(Debug, Clone)]
743pub struct BroadcastPreview {
744 pub success: bool,
745 pub broadcast_shape: Option<Vec<usize>>,
746 pub memory_required: Option<usize>,
747 pub expansion_factor1: Option<usize>,
748 pub expansion_factor2: Option<usize>,
749 pub is_memory_efficient: bool,
750 pub operation_cost: OperationCost,
751 pub error_message: Option<String>,
752}
753
754#[derive(Debug, Clone)]
756pub struct OperationCost {
757 pub computational_complexity: usize,
758 pub memory_access_pattern: MemoryAccessPattern,
759 pub cache_efficiency: f64,
760 pub estimated_runtime_ms: f64,
761}
762
763#[derive(Debug, Clone)]
765pub enum MemoryAccessPattern {
766 Sequential,
767 Strided { stride: usize },
768 Random,
769 Broadcast { expansion_factor: usize },
770}
771
772pub trait BroadcastShape {
774 fn broadcast_compatible(&self, other: &Self) -> bool;
776
777 fn broadcast_shape(&self, other: &Self) -> Result<Shape>;
779
780 fn is_broadcast_efficient(&self, other: &Self) -> bool;
782}
783
784impl BroadcastShape for Shape {
785 fn broadcast_compatible(&self, other: &Self) -> bool {
786 BroadcastOps::are_shapes_compatible(self.dims(), other.dims()).unwrap_or(false)
787 }
788
789 fn broadcast_shape(&self, other: &Self) -> Result<Shape> {
790 let result_dims = BroadcastOps::compute_broadcast_shape(self.dims(), other.dims())?;
791 Ok(Shape::new(result_dims))
792 }
793
794 fn is_broadcast_efficient(&self, other: &Self) -> bool {
795 if let Ok(info) = BroadcastOps::get_broadcast_info(self.dims(), other.dims()) {
796 info.is_memory_efficient
797 } else {
798 false
799 }
800 }
801}
802
803#[cfg(test)]
804mod tests {
805 use super::*;
806
807 #[test]
808 fn test_broadcast_compatibility() {
809 assert!(BroadcastOps::are_shapes_compatible(&[3, 4], &[1, 4])
811 .expect("shape compatibility check should succeed"));
812 assert!(BroadcastOps::are_shapes_compatible(&[3, 1], &[3, 4])
813 .expect("shape compatibility check should succeed"));
814 assert!(BroadcastOps::are_shapes_compatible(&[1], &[3, 4])
815 .expect("shape compatibility check should succeed"));
816 assert!(BroadcastOps::are_shapes_compatible(&[], &[3])
817 .expect("shape compatibility check should succeed"));
818
819 assert!(!BroadcastOps::are_shapes_compatible(&[3, 4], &[2, 4])
821 .expect("shape compatibility check should succeed"));
822 assert!(!BroadcastOps::are_shapes_compatible(&[3, 2], &[4, 3])
823 .expect("shape compatibility check should succeed"));
824 }
825
826 #[test]
827 fn test_broadcast_shape_computation() {
828 let result = BroadcastOps::compute_broadcast_shape(&[3, 4], &[1, 4])
830 .expect("broadcast should succeed");
831 assert_eq!(result, vec![3, 4]);
832
833 let result = BroadcastOps::compute_broadcast_shape(&[3, 1], &[3, 4])
834 .expect("broadcast should succeed");
835 assert_eq!(result, vec![3, 4]);
836
837 let result =
839 BroadcastOps::compute_broadcast_shape(&[1], &[3, 4]).expect("broadcast should succeed");
840 assert_eq!(result, vec![3, 4]);
841
842 let result =
843 BroadcastOps::compute_broadcast_shape(&[], &[3]).expect("broadcast should succeed");
844 assert_eq!(result, vec![3]);
845 }
846
847 #[test]
848 fn test_broadcast_index_computation() {
849 let multi_index = vec![1, 2];
851 let original_shape = vec![1, 3];
852 let broadcast_shape = vec![2, 3];
853
854 let linear_index =
855 BroadcastOps::compute_broadcast_index(&multi_index, &original_shape, &broadcast_shape)
856 .expect("broadcast should succeed");
857
858 assert_eq!(linear_index, 2);
863 }
864
865 #[test]
866 fn test_flat_to_multi_index() {
867 let shape = vec![2, 3, 4];
868
869 let multi_index = BroadcastOps::flat_to_multi_index(0, &shape);
871 assert_eq!(multi_index, vec![0, 0, 0]);
872
873 let multi_index = BroadcastOps::flat_to_multi_index(5, &shape);
875 assert_eq!(multi_index, vec![0, 1, 1]);
876
877 let multi_index = BroadcastOps::flat_to_multi_index(23, &shape);
879 assert_eq!(multi_index, vec![1, 2, 3]);
880 }
881
882 #[test]
883 fn test_validation() {
884 assert!(BroadcastOps::validate_broadcast_operation(&[3, 4], &[1, 4], "add").is_ok());
886
887 assert!(BroadcastOps::validate_broadcast_operation(&[3, 4], &[2, 5], "add").is_err());
889
890 assert!(BroadcastOps::validate_broadcast_operation(&[], &[3], "add").is_ok());
892
893 assert!(BroadcastOps::validate_broadcast_operation(&[3, 0], &[3, 1], "add").is_err());
895 }
896
897 #[test]
898 fn test_memory_estimation() {
899 let shape1 = vec![2, 3];
900 let shape2 = vec![1, 3];
901 let element_size = std::mem::size_of::<f32>();
902
903 let memory_required =
904 BroadcastOps::estimate_broadcast_memory(&shape1, &shape2, element_size)
905 .expect("broadcast memory estimation should succeed");
906
907 assert_eq!(memory_required, 6 * element_size);
910 }
911
912 #[test]
913 fn test_broadcast_info() {
914 let shape1 = vec![1, 4];
915 let shape2 = vec![3, 1];
916
917 let info = BroadcastOps::get_broadcast_info(&shape1, &shape2)
918 .expect("broadcast info should succeed");
919
920 assert_eq!(info.original_shape1, vec![1, 4]);
921 assert_eq!(info.original_shape2, vec![3, 1]);
922 assert_eq!(info.broadcast_shape, vec![3, 4]);
923 assert_eq!(info.expansion_factor1, 3); assert_eq!(info.expansion_factor2, 4); assert!(!info.is_memory_efficient); }
927
928 #[test]
929 fn test_shape_trait_extension() {
930 let shape1 = Shape::new(vec![3, 4]);
931 let shape2 = Shape::new(vec![1, 4]);
932 let shape3 = Shape::new(vec![2, 5]);
933
934 assert!(shape1.broadcast_compatible(&shape2));
936 assert!(!shape1.broadcast_compatible(&shape3));
937
938 let broadcast_result = shape1
940 .broadcast_shape(&shape2)
941 .expect("broadcast_shape should succeed");
942 assert_eq!(broadcast_result.dims(), &[3, 4]);
943 }
944
945 #[test]
946 fn test_error_messages() {
947 let result = BroadcastOps::compute_broadcast_shape(&[3, 4], &[2, 5]);
949 assert!(result.is_err());
950 if let Err(TorshError::InvalidArgument(err)) = result {
951 let msg = err.to_string();
952 assert!(msg.contains("Cannot broadcast"));
953 }
954
955 let result = BroadcastOps::validate_broadcast_operation(&[3, 0], &[3, 1], "multiply");
957 assert!(result.is_err());
958 }
959
960 #[test]
961 fn test_broadcast_strides() {
962 let shape1 = vec![1, 4];
963 let shape2 = vec![3, 1];
964 let broadcast_shape = vec![3, 4];
965
966 let strides = BroadcastOps::compute_broadcast_strides(&shape1, &shape2, &broadcast_shape)
967 .expect("broadcast should succeed");
968
969 assert_eq!(strides.original_strides1, vec![4, 1]);
971 assert_eq!(strides.original_strides2, vec![1, 1]);
973
974 assert_eq!(strides.broadcast_strides1, vec![0, 1]);
976 assert_eq!(strides.broadcast_strides2, vec![1, 0]);
978 }
979
980 #[test]
981 fn test_broadcast_pattern_detection() {
982 assert_eq!(
984 BroadcastOps::detect_broadcast_pattern(&[], &[3, 4]),
985 BroadcastPattern::Scalar
986 );
987 assert_eq!(
988 BroadcastOps::detect_broadcast_pattern(&[3, 4], &[]),
989 BroadcastPattern::Scalar
990 );
991
992 assert_eq!(
994 BroadcastOps::detect_broadcast_pattern(&[3, 4], &[3, 4]),
995 BroadcastPattern::ElementWise
996 );
997
998 assert_eq!(
1000 BroadcastOps::detect_broadcast_pattern(&[1], &[4]),
1001 BroadcastPattern::VectorScalar
1002 );
1003 assert_eq!(
1004 BroadcastOps::detect_broadcast_pattern(&[4], &[1]),
1005 BroadcastPattern::VectorScalar
1006 );
1007
1008 assert_eq!(
1010 BroadcastOps::detect_broadcast_pattern(&[3, 4], &[4]),
1011 BroadcastPattern::MatrixVector
1012 );
1013 assert_eq!(
1014 BroadcastOps::detect_broadcast_pattern(&[4], &[3, 4]),
1015 BroadcastPattern::MatrixVector
1016 );
1017
1018 assert_eq!(
1020 BroadcastOps::detect_broadcast_pattern(&[1, 4], &[3, 4]),
1021 BroadcastPattern::Size1Dimension
1022 );
1023 assert_eq!(
1024 BroadcastOps::detect_broadcast_pattern(&[3, 1], &[3, 4]),
1025 BroadcastPattern::Size1Dimension
1026 );
1027
1028 assert_eq!(
1030 BroadcastOps::detect_broadcast_pattern(&[2, 3, 4], &[5, 2, 3, 4]),
1031 BroadcastPattern::General
1032 );
1033 }
1034
1035 #[test]
1036 fn test_broadcast_preview() {
1037 let shape1 = vec![1, 4];
1038 let shape2 = vec![3, 1];
1039 let element_size = std::mem::size_of::<f32>();
1040
1041 let preview = BroadcastOps::create_broadcast_preview(&shape1, &shape2, element_size);
1042
1043 assert!(preview.success);
1044 assert_eq!(preview.broadcast_shape, Some(vec![3, 4]));
1045 assert_eq!(preview.memory_required, Some(12 * element_size)); assert_eq!(preview.expansion_factor1, Some(3)); assert_eq!(preview.expansion_factor2, Some(4)); assert!(!preview.is_memory_efficient); assert!(preview.error_message.is_none());
1050
1051 let preview_fail = BroadcastOps::create_broadcast_preview(&[3, 4], &[2, 5], element_size);
1053 assert!(!preview_fail.success);
1054 assert!(preview_fail.error_message.is_some());
1055 }
1056
1057 #[test]
1058 fn test_broadcast_cache() {
1059 BroadcastCache::clear();
1061
1062 let config = BroadcastCacheConfig::default();
1063 let shape1 = vec![1, 4];
1064 let shape2 = vec![3, 1];
1065
1066 let entry1 = BroadcastCache::get_or_compute(&shape1, &shape2, &config)
1068 .expect("broadcast cache computation should succeed");
1069 assert_eq!(entry1.broadcast_shape, vec![3, 4]);
1070
1071 let entry2 = BroadcastCache::get_or_compute(&shape1, &shape2, &config)
1073 .expect("broadcast cache computation should succeed");
1074 assert_eq!(entry2.broadcast_shape, vec![3, 4]);
1075
1076 let stats = BroadcastCache::get_stats();
1078 assert!(stats.total_entries > 0);
1079 assert!(stats.total_accesses > 0);
1080
1081 let config_no_cache = BroadcastCacheConfig {
1083 enable_cache: false,
1084 ..Default::default()
1085 };
1086 let entry3 = BroadcastCache::get_or_compute(&shape1, &shape2, &config_no_cache)
1087 .expect("broadcast cache computation should succeed");
1088 assert_eq!(entry3.broadcast_shape, vec![3, 4]);
1089 }
1090
1091 #[test]
1092 fn test_stride_computation() {
1093 let shape = vec![2, 3, 4];
1095 let strides = BroadcastOps::compute_strides(&shape);
1096 assert_eq!(strides, vec![12, 4, 1]); let empty_shape = vec![];
1100 let empty_strides = BroadcastOps::compute_strides(&empty_shape);
1101 assert_eq!(empty_strides, Vec::<usize>::new());
1102
1103 let single_shape = vec![5];
1105 let single_strides = BroadcastOps::compute_strides(&single_shape);
1106 assert_eq!(single_strides, vec![1]);
1107 }
1108
1109 #[test]
1110 fn test_operation_cost_estimation() {
1111 let shape1 = vec![3, 4];
1112 let shape2 = vec![3, 4];
1113 let element_size = std::mem::size_of::<f32>();
1114
1115 let preview = BroadcastOps::create_broadcast_preview(&shape1, &shape2, element_size);
1116 assert!(preview.success);
1117
1118 let cost = &preview.operation_cost;
1119 assert_eq!(cost.computational_complexity, 12); assert!(matches!(
1121 cost.memory_access_pattern,
1122 MemoryAccessPattern::Sequential
1123 ));
1124 assert!(cost.cache_efficiency > 0.8); assert!(cost.estimated_runtime_ms >= 0.0);
1126 }
1127}