Skip to main content

torsh_tensor/
broadcast.rs

1//! Tensor broadcasting operations with comprehensive error handling
2//!
3//! Broadcasting allows operations between tensors of different shapes by automatically
4//! expanding dimensions according to NumPy/PyTorch broadcasting rules.
5
6use 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/// Error types specific to broadcasting operations
13#[derive(Debug, Clone)]
14pub enum BroadcastError {
15    /// Shapes are not compatible for broadcasting
16    IncompatibleShapes {
17        shape1: Vec<usize>,
18        shape2: Vec<usize>,
19        reason: String,
20    },
21    /// Dimension size mismatch
22    DimensionMismatch {
23        dim: usize,
24        size1: usize,
25        size2: usize,
26    },
27    /// Shape computation overflow
28    ShapeOverflow { attempted_shape: Vec<usize> },
29    /// Memory allocation failure
30    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
80/// Broadcasting utilities for tensors
81pub struct BroadcastOps;
82
83impl BroadcastOps {
84    /// Check if two shapes are compatible for broadcasting
85    ///
86    /// Broadcasting rules (from right to left):
87    /// 1. Dimensions must be equal, or one of them is 1, or one is missing
88    /// 2. Missing dimensions are treated as 1
89    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 // Missing dimensions are treated as 1
99            };
100
101            let dim2 = if i < ndim2 {
102                shape2[ndim2 - 1 - i]
103            } else {
104                1 // Missing dimensions are treated as 1
105            };
106
107            // Check broadcasting compatibility
108            if dim1 != dim2 && dim1 != 1 && dim2 != 1 {
109                return Ok(false);
110            }
111        }
112
113        Ok(true)
114    }
115
116    /// Compute the broadcasted shape for two input shapes
117    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            // The result dimension is the maximum of the two
136            let result_dim = dim1.max(dim2);
137
138            // Check for potential overflow
139            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        // Reverse to get proper order (we computed from right to left)
152        result_shape.reverse();
153
154        // Check total size doesn't overflow
155        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    /// Compute the linear index for a tensor in a broadcasted operation
169    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        // Process from right to left (last dimension first)
189        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            // Map to original shape coordinates
194            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 // Broadcasting: use index 0 for size-1 dimensions
200                } else {
201                    broadcast_coord // Use the coordinate directly
202                }
203            } else {
204                0 // Missing dimensions are treated as 0 coordinate
205            };
206
207            // Add contribution to linear index
208            linear_index += orig_coord * stride;
209
210            // Update stride for next dimension
211            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    /// Convert flat index to multi-dimensional index
221    ///
222    /// A shape containing a zero-sized dimension addresses no elements at all,
223    /// so there is no valid flat index to decompose: the all-zero index is
224    /// returned instead of dividing by the empty extent.
225    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    /// Validate broadcasting operation parameters
243    pub fn validate_broadcast_operation(
244        shape1: &[usize],
245        shape2: &[usize],
246        operation_name: &str,
247    ) -> Result<()> {
248        // Empty shapes are allowed for scalar tensors
249        // Only check for zero dimensions in non-empty shapes
250
251        // Check for zero dimensions
252        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        // Check maximum number of dimensions
259        const MAX_DIMENSIONS: usize = 32; // Reasonable limit for memory and performance
260        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        // Check broadcasting compatibility
267        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    /// Estimate memory requirements for a broadcast operation
282    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        // Check for overflow
291        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    /// Get detailed broadcasting information for debugging
305    ///
306    /// # Errors
307    ///
308    /// Returns [`TorshError::InvalidShape`] when either shape contains a
309    /// zero-sized dimension: such a tensor holds no elements, so the expansion
310    /// factor it would be measured by is undefined (the element counts it is
311    /// divided by are zero).
312    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    /// Compute pre-computed strides for efficient broadcasting operations
336    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    /// Compute strides for a given shape (row-major/C-style)
359    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    /// Compute broadcast strides for a specific shape to match broadcast shape
372    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                    // No broadcasting needed for this dimension
391                    broadcast_strides[broadcast_dim_idx] = original_strides[orig_dim_idx];
392                } else if orig_size == 1 {
393                    // Broadcasting: stride becomes 0 to repeat the single element
394                    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                // Missing dimension in original shape, treat as size 1 with stride 0
402                broadcast_strides[broadcast_dim_idx] = 0;
403            }
404        }
405
406        Ok(broadcast_strides)
407    }
408
409    /// Detect common broadcasting patterns for optimization
410    pub fn detect_broadcast_pattern(shape1: &[usize], shape2: &[usize]) -> BroadcastPattern {
411        // Scalar broadcasting (one operand is scalar)
412        if shape1.is_empty() || shape2.is_empty() {
413            return BroadcastPattern::Scalar;
414        }
415
416        // Element-wise (same shape)
417        if shape1 == shape2 {
418            return BroadcastPattern::ElementWise;
419        }
420
421        // Matrix-vector broadcasting (2D with 1D)
422        if (shape1.len() == 2 && shape2.len() == 1) || (shape1.len() == 1 && shape2.len() == 2) {
423            return BroadcastPattern::MatrixVector;
424        }
425
426        // Vector-scalar broadcasting (one operand is 1D, but not matrix-vector case)
427        if shape1.len() == 1 || shape2.len() == 1 {
428            return BroadcastPattern::VectorScalar;
429        }
430
431        // Check for size-1 dimension patterns
432        let has_size_1_dims = shape1.contains(&1) || shape2.contains(&1);
433        if has_size_1_dims {
434            return BroadcastPattern::Size1Dimension;
435        }
436
437        // Default to general broadcasting
438        BroadcastPattern::General
439    }
440
441    /// Create optimized broadcasting preview with cost estimation
442    pub fn create_broadcast_preview(
443        shape1: &[usize],
444        shape2: &[usize],
445        element_size: usize,
446    ) -> BroadcastPreview {
447        // Check compatibility first
448        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        // Compute broadcast details
469        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    /// Estimate operation cost for different broadcasting patterns
510    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        // Estimate runtime based on memory bandwidth and complexity
542        let estimated_runtime_ms = (memory_bytes as f64 / 1e9) * runtime_factor; // Assume 1GB/s bandwidth
543
544        OperationCost {
545            computational_complexity: complexity,
546            memory_access_pattern: access_pattern,
547            cache_efficiency,
548            estimated_runtime_ms,
549        }
550    }
551}
552
553/// Broadcasting patterns for optimization
554#[derive(Debug, Clone, PartialEq)]
555pub enum BroadcastPattern {
556    /// Scalar broadcasting (one operand is scalar)
557    Scalar,
558    /// Element-wise operation (same shapes)
559    ElementWise,
560    /// Vector-scalar broadcasting
561    VectorScalar,
562    /// Matrix-vector broadcasting
563    MatrixVector,
564    /// Broadcasting with size-1 dimensions
565    Size1Dimension,
566    /// General broadcasting case
567    General,
568}
569
570/// Information about a broadcasting operation
571#[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/// Pre-computed strides for efficient broadcasting operations
582#[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/// Cache key for broadcasting operations
592#[derive(Debug, Clone, PartialEq, Eq, Hash)]
593struct BroadcastCacheKey {
594    shape1: Vec<usize>,
595    shape2: Vec<usize>,
596}
597
598/// Cached broadcasting computation results
599#[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
608/// Global broadcasting cache for repeated operations
609static BROADCAST_CACHE: std::sync::LazyLock<
610    Arc<Mutex<HashMap<BroadcastCacheKey, BroadcastCacheEntry>>>,
611> = std::sync::LazyLock::new(|| Arc::new(Mutex::new(HashMap::new())));
612
613/// Cache manager for broadcasting operations
614pub struct BroadcastCache;
615
616impl BroadcastCache {
617    /// Get cached broadcast result or compute and cache it
618    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        // Check if entry exists and is not expired
635        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                // Entry expired, remove it
648                cache.remove(&key);
649            }
650        }
651
652        // Compute fresh result
653        let mut entry = Self::compute_fresh(shape1, shape2)?;
654        entry.access_count = 1;
655        entry.last_accessed = std::time::SystemTime::now();
656
657        // Evict old entries if cache is full
658        if cache.len() >= config.max_entries {
659            Self::evict_lru(&mut cache);
660        }
661
662        // Insert new entry
663        cache.insert(key, entry.clone());
664        Ok(entry)
665    }
666
667    /// Compute fresh broadcast result without caching
668    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    /// Evict least recently used entry
683    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    /// Clear the cache
694    pub fn clear() {
695        let mut cache = BROADCAST_CACHE.lock_or_recover();
696        cache.clear();
697    }
698
699    /// Get cache statistics
700    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/// Broadcasting cache statistics
717#[derive(Debug, Clone)]
718pub struct BroadcastCacheStats {
719    pub total_entries: usize,
720    pub total_accesses: usize,
721    pub hit_rate: f64,
722}
723
724/// Configuration for broadcasting cache
725pub 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, // 5 minutes
736            enable_cache: true,
737        }
738    }
739}
740
741/// Broadcasting preview result for dry-run functionality
742#[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/// Cost estimation for broadcasting operations
755#[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/// Memory access patterns for optimization
764#[derive(Debug, Clone)]
765pub enum MemoryAccessPattern {
766    Sequential,
767    Strided { stride: usize },
768    Random,
769    Broadcast { expansion_factor: usize },
770}
771
772/// Extension trait for Shape to add broadcasting methods
773pub trait BroadcastShape {
774    /// Check if this shape is compatible for broadcasting with another shape
775    fn broadcast_compatible(&self, other: &Self) -> bool;
776
777    /// Compute the broadcasted shape with another shape
778    fn broadcast_shape(&self, other: &Self) -> Result<Shape>;
779
780    /// Check if broadcasting would be memory efficient
781    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        // Compatible shapes
810        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        // Incompatible shapes
820        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        // Basic broadcasting
829        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        // Different number of dimensions
838        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        // Test basic broadcasting index computation
850        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        // For shape [1, 3] with broadcast coordinates [1, 2]:
859        // - dimension 0: coordinate 1 -> maps to 0 (broadcast)
860        // - dimension 1: coordinate 2 -> maps to 2
861        // Linear index = 0 * 3 + 2 = 2
862        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        // Test conversion for flat index 0
870        let multi_index = BroadcastOps::flat_to_multi_index(0, &shape);
871        assert_eq!(multi_index, vec![0, 0, 0]);
872
873        // Test conversion for flat index 5
874        let multi_index = BroadcastOps::flat_to_multi_index(5, &shape);
875        assert_eq!(multi_index, vec![0, 1, 1]);
876
877        // Test conversion for flat index 23 (last index)
878        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        // Valid operation
885        assert!(BroadcastOps::validate_broadcast_operation(&[3, 4], &[1, 4], "add").is_ok());
886
887        // Invalid: incompatible shapes
888        assert!(BroadcastOps::validate_broadcast_operation(&[3, 4], &[2, 5], "add").is_err());
889
890        // Valid: scalar with non-scalar (empty shape allowed for scalars)
891        assert!(BroadcastOps::validate_broadcast_operation(&[], &[3], "add").is_ok());
892
893        // Invalid: zero dimension
894        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        // Broadcast shape should be [2, 3] = 6 elements
908        // Memory = 6 * sizeof(f32) = 6 * 4 = 24 bytes
909        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); // (3*4) / (1*4) = 3
924        assert_eq!(info.expansion_factor2, 4); // (3*4) / (3*1) = 4
925        assert!(!info.is_memory_efficient); // expansion factors > 2
926    }
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        // Test compatibility
935        assert!(shape1.broadcast_compatible(&shape2));
936        assert!(!shape1.broadcast_compatible(&shape3));
937
938        // Test broadcast shape computation
939        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        // Test incompatible shapes error
948        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        // Test validation error
956        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        // Original strides for [1, 4] should be [4, 1]
970        assert_eq!(strides.original_strides1, vec![4, 1]);
971        // Original strides for [3, 1] should be [1, 1]
972        assert_eq!(strides.original_strides2, vec![1, 1]);
973
974        // Broadcast strides for shape1 [1, 4] -> [3, 4] should be [0, 1] (dim 0 broadcasts)
975        assert_eq!(strides.broadcast_strides1, vec![0, 1]);
976        // Broadcast strides for shape2 [3, 1] -> [3, 4] should be [1, 0] (dim 1 broadcasts)
977        assert_eq!(strides.broadcast_strides2, vec![1, 0]);
978    }
979
980    #[test]
981    fn test_broadcast_pattern_detection() {
982        // Scalar pattern
983        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        // Element-wise pattern
993        assert_eq!(
994            BroadcastOps::detect_broadcast_pattern(&[3, 4], &[3, 4]),
995            BroadcastPattern::ElementWise
996        );
997
998        // Vector-scalar pattern (1D with scalar-like)
999        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        // Matrix-vector pattern (2D with 1D)
1009        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        // Size-1 dimension pattern
1019        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        // General pattern
1029        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)); // 3*4 elements
1046        assert_eq!(preview.expansion_factor1, Some(3)); // (3*4) / (1*4) = 3
1047        assert_eq!(preview.expansion_factor2, Some(4)); // (3*4) / (3*1) = 4
1048        assert!(!preview.is_memory_efficient); // factors > 2
1049        assert!(preview.error_message.is_none());
1050
1051        // Test incompatible shapes
1052        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        // Clear cache first
1060        BroadcastCache::clear();
1061
1062        let config = BroadcastCacheConfig::default();
1063        let shape1 = vec![1, 4];
1064        let shape2 = vec![3, 1];
1065
1066        // First access should compute and cache
1067        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        // Second access should hit cache
1072        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        // Verify cache statistics
1077        let stats = BroadcastCache::get_stats();
1078        assert!(stats.total_entries > 0);
1079        assert!(stats.total_accesses > 0);
1080
1081        // Test cache disabled
1082        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        // Test basic stride computation
1094        let shape = vec![2, 3, 4];
1095        let strides = BroadcastOps::compute_strides(&shape);
1096        assert_eq!(strides, vec![12, 4, 1]); // [3*4, 4, 1]
1097
1098        // Test empty shape
1099        let empty_shape = vec![];
1100        let empty_strides = BroadcastOps::compute_strides(&empty_shape);
1101        assert_eq!(empty_strides, Vec::<usize>::new());
1102
1103        // Test single dimension
1104        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); // 3*4 elements
1120        assert!(matches!(
1121            cost.memory_access_pattern,
1122            MemoryAccessPattern::Sequential
1123        ));
1124        assert!(cost.cache_efficiency > 0.8); // Element-wise should be efficient
1125        assert!(cost.estimated_runtime_ms >= 0.0);
1126    }
1127}