Skip to main content

torsh_tensor/
math_ops.rs

1//! Mathematical operations for tensors - Enhanced with SciRS2 Performance Features
2//!
3//! This module provides comprehensive mathematical operations including arithmetic,
4//! trigonometric, exponential, and logarithmic functions, along with scalar operations
5//! and **full SciRS2 backend integration** for optimal performance.
6//!
7//! # Enhanced SciRS2 Features
8//!
9//! - **SIMD Acceleration**: Vectorized operations using SciRS2 SIMD support
10//! - **Parallel Processing**: Multi-core tensor operations via SciRS2 parallel framework
11//! - **GPU Acceleration**: CUDA/Metal backend support through SciRS2 GPU abstraction
12//! - **Memory Optimization**: Lazy evaluation and memory-mapped arrays for large tensors
13//! - **Scalar arithmetic**: Operations between tensors and scalar values
14//! - **Element-wise operations**: Basic arithmetic operations between tensors
15//! - **Mathematical functions**: sqrt, exp, log, trigonometric functions
16//! - **Complex operations**: Support for complex number operations
17//! - **Broadcasting**: Automatic shape compatibility for operations
18
19use std::sync::Arc;
20use torsh_core::{
21    dtype::TensorElement,
22    error::{Result, TorshError},
23};
24
25use crate::memory_pool::global_acquire_uninit;
26
27// ✅ SciRS2 Advanced Features Integration
28// Performance acceleration through SciRS2 ecosystem
29#[cfg(feature = "simd")]
30mod simd_imports {
31    // ✅ SciRS2 Breakthrough SIMD Implementation - 14.17x Performance
32
33    // 🚀 Hyperoptimized SIMD implementations with breakthrough performance
34    // Note: Currently not using direct SIMD functions - they're available via SimdUnifiedOps trait
35    // pub use scirs2_core::simd::{ ... };
36
37    // Array types
38    pub use scirs2_core::ndarray::Array1;
39}
40
41#[cfg(feature = "simd")]
42use simd_imports::*;
43
44// Chunking and parallel processing
45#[cfg(feature = "parallel")]
46use scirs2_core::chunking::{
47    CacheAwareness, ChunkConfig, ChunkStrategy, ComputeIntensity, GpuChunkSettings, MemoryPattern,
48    NumaStrategy,
49};
50
51// Memory optimization features
52// Note: memory_efficient features require enabling the memory_efficient feature flag
53// use scirs2_core::memory_efficient::{MemoryMappedArray, LazyArray};
54
55// TODO: scirs2_core::profiling module not available yet
56// Performance profiling integration
57// #[cfg(feature = "profiling")]
58// use scirs2_core::profiling::Profiler;
59// TODO: profile_section macro not available in scirs2_core yet
60// use scirs2_core::profiling::profile_section;
61
62use crate::core_ops::{Operation, Tensor, UnaryKind};
63#[cfg(feature = "simd")]
64use crate::simd_ops_f32::BinaryF32Op;
65use crate::storage::TensorStorage;
66
67impl<T: TensorElement> Tensor<T> {
68    /// Record `result` as a differentiable element-wise unary of `self`.
69    ///
70    /// Called at the end of the public activation/transcendental wrappers so
71    /// every dispatch path (SIMD, parallel, GPU, scalar) shares one recorded
72    /// derivative. A no-op when gradients are not being tracked.
73    pub(crate) fn record_unary(&self, mut result: Self, kind: UnaryKind) -> Self {
74        if crate::should_record_grad(self.requires_grad) {
75            result.requires_grad = true;
76            result.operation = Operation::Unary {
77                input: Arc::new(self.clone()),
78                kind,
79            };
80        }
81        result
82    }
83
84    /// Record `result` as `leaky_relu(self, negative_slope)`.
85    ///
86    /// Parameterized twin of [`Tensor::record_unary`]: the slope cannot live
87    /// in [`UnaryKind`], so it goes in its own `Operation` variant (the
88    /// `MulScalar`/`DivScalar` idiom). A no-op when gradients are not tracked.
89    pub(crate) fn record_leaky_relu(&self, mut result: Self, negative_slope: T) -> Self {
90        if crate::should_record_grad(self.requires_grad) {
91            result.requires_grad = true;
92            result.operation = Operation::LeakyRelu {
93                input: Arc::new(self.clone()),
94                negative_slope,
95            };
96        }
97        result
98    }
99
100    /// Record `result` as `maximum(self, other)` (`is_maximum`) or
101    /// `minimum(self, other)`.
102    ///
103    /// The operands are stored *un-broadcast*, like [`Operation::Add`]'s, so
104    /// backward folds each gradient back to its own operand's shape.
105    pub(crate) fn record_extremum(&self, other: &Self, mut result: Self, is_maximum: bool) -> Self {
106        if crate::should_record_grad(self.requires_grad || other.requires_grad) {
107            result.requires_grad = true;
108            let lhs = Arc::new(self.clone());
109            let rhs = Arc::new(other.clone());
110            result.operation = if is_maximum {
111                Operation::Maximum { lhs, rhs }
112            } else {
113                Operation::Minimum { lhs, rhs }
114            };
115        }
116        result
117    }
118
119    /// Record `result` as `abs(self)` — but only when `self` and `result` have
120    /// the *same* element type.
121    ///
122    /// `Tensor::abs` lives on the [`torsh_core::dtype::ComplexElement`] impl
123    /// block (`complex_ops.rs`) and has signature
124    /// `Tensor<T> -> Tensor<T::Real>`. Real element types implement
125    /// `ComplexElement` with `Real = Self`, so for them the two types coincide
126    /// and `|x|` is the ordinary real absolute value with sub-gradient
127    /// `sign(x)`. For a genuinely complex `T` the output type differs, the
128    /// derivative is the Wirtinger `z / |z|` rather than `sign(x)`, and
129    /// [`Operation::Unary`] could not hold the operand anyway (it wants an
130    /// `Arc<Tensor<R>>`, not an `Arc<Tensor<T>>`).
131    ///
132    /// The `Any` downcast is what separates the two cases: it succeeds exactly
133    /// when `T == R`, so complex `abs` keeps its historical detached behaviour
134    /// and real `abs` records. It is a safe downcast — `TensorElement: 'static`
135    /// supplies the `Any` bound — not a transmute.
136    pub(crate) fn record_abs_if_real<R: TensorElement>(&self, result: Tensor<R>) -> Tensor<R> {
137        match (self as &dyn std::any::Any).downcast_ref::<Tensor<R>>() {
138            Some(real_self) => real_self.record_unary(result, UnaryKind::Abs),
139            None => result,
140        }
141    }
142}
143
144/// Element count from which a same-shape binary op dispatches to the hardware
145/// SIMD kernels of `scirs2_core::simd_ops::SimdUnifiedOps`.
146///
147/// Below this size the per-call dispatch and buffer acquisition dominate the
148/// arithmetic, so the generic path is faster.
149#[cfg(feature = "simd")]
150const SIMD_BINARY_THRESHOLD: usize = 1024;
151
152/// Element count from which the generic element-wise path fans out over
153/// `scirs2_core::parallel_ops` worker threads.
154///
155/// The same value as [`SIMD_BINARY_THRESHOLD`] on purpose: it leaves no size
156/// band in which neither the vector kernels nor the worker pool apply.
157#[cfg(feature = "parallel")]
158const PARALLEL_ELEMENTWISE_THRESHOLD: usize = 1024;
159
160/// Dispatch one of the f64 vector kernels of
161/// `scirs2_core::simd_ops::SimdUnifiedOps` into a caller-supplied buffer.
162///
163/// The op tag is shared with the f32 kernels (`BinaryF32Op`); only the element
164/// type differs. All three slices must have the same length.
165#[cfg(feature = "simd")]
166fn dispatch_f64_into(op: BinaryF32Op, a: &[f64], b: &[f64], out: &mut [f64]) {
167    use scirs2_core::simd_ops::SimdUnifiedOps;
168    match op {
169        BinaryF32Op::Add => <f64 as SimdUnifiedOps>::simd_add_into(a, b, out),
170        BinaryF32Op::Sub => <f64 as SimdUnifiedOps>::simd_sub_into(a, b, out),
171        BinaryF32Op::Mul => <f64 as SimdUnifiedOps>::simd_mul_into(a, b, out),
172        BinaryF32Op::Div => <f64 as SimdUnifiedOps>::simd_div_into(a, b, out),
173    }
174}
175
176// 🚀 Adaptive SIMD Selection System for Maximum Performance
177#[cfg(feature = "simd")]
178pub(crate) mod adaptive_simd {
179    use super::*;
180    use scirs2_core::ndarray::ArrayView1;
181
182    /// SIMD-accelerated ReLU activation function
183    /// Uses scirs2_core SIMD implementation for optimal performance
184    pub fn adaptive_simd_relu_f32(input: &ArrayView1<f32>) -> Array1<f32> {
185        scirs2_core::simd::activation::simd_relu_f32(input)
186    }
187
188    /// SIMD-accelerated sigmoid activation function
189    /// Uses scirs2_core SIMD implementation for optimal performance
190    pub fn adaptive_simd_sigmoid_f32(input: &ArrayView1<f32>) -> Array1<f32> {
191        scirs2_core::simd::transcendental::simd_sigmoid_f32(input)
192    }
193
194    // NOTE: there is deliberately no `adaptive_simd_gelu_f32` shim.
195    // `scirs2_core::simd::transcendental::simd_gelu_f32` computes a
196    // clamped-Pade approximation of `tanh`, i.e. a materially different
197    // function from the exact-tanh GELU the crate documents and
198    // differentiates (measured: up to 0.0211 absolute, 515% relative at
199    // x = -2.40). Routing large f32 tensors through it made `Tensor::gelu`
200    // discontinuous across its own `numel = 1000` dispatch threshold and
201    // inconsistent with its recorded backward, so the shim and its dispatch
202    // were removed rather than documented around. See
203    // `Tensor::gelu_forward` in math_ops_trig.rs. `relu` and `sigmoid` are
204    // unaffected: their SIMD kernels match their formulas exactly.
205}
206
207#[cfg(feature = "simd")]
208// 🚀 Intelligent Chunking System for Advanced Optimization
209#[cfg(feature = "parallel")]
210mod intelligent_chunking {
211    use super::*;
212
213    /// Tensor operation types for optimal chunking strategy selection
214    #[derive(Debug, Clone, Copy)]
215    pub enum TensorOpType {
216        /// Element-wise operations (add, mul, etc.)
217        ElementWise,
218        /// Activation functions
219        Activation,
220    }
221
222    // Note: GpuChunkSettings is now imported from scirs2_core::chunking
223
224    /// Create optimal chunking configuration based on tensor operation characteristics
225    pub fn create_optimal_chunk_config(
226        tensor_size: usize,
227        op_type: TensorOpType,
228        _device: torsh_core::device::DeviceType,
229        is_gpu_available: bool,
230    ) -> ChunkConfig {
231        match op_type {
232            TensorOpType::ElementWise => ChunkConfig {
233                strategy: if tensor_size > 100_000 {
234                    ChunkStrategy::MemoryOptimized
235                } else {
236                    ChunkStrategy::CacheOptimized
237                },
238                min_chunk_size: 64,
239                max_chunk_size: 8192,
240                prefer_work_stealing: true,
241                memory_pattern: MemoryPattern::Sequential,
242                compute_intensity: ComputeIntensity::MemoryBound,
243                enable_monitoring: false,
244                load_balance_factor: 0.1,
245                cache_awareness: CacheAwareness::L2,
246                numa_strategy: NumaStrategy::LocalPreferred,
247                gpu_settings: if is_gpu_available {
248                    Some(GpuChunkSettings::default())
249                } else {
250                    None
251                },
252            },
253
254            TensorOpType::Activation => ChunkConfig {
255                strategy: ChunkStrategy::CacheOptimized,
256                min_chunk_size: 64,
257                max_chunk_size: 4096,
258                prefer_work_stealing: true,
259                memory_pattern: MemoryPattern::Sequential,
260                compute_intensity: ComputeIntensity::ComputeIntensive,
261                enable_monitoring: false,
262                load_balance_factor: 0.1,
263                cache_awareness: CacheAwareness::L1,
264                numa_strategy: NumaStrategy::LocalPreferred,
265                gpu_settings: if is_gpu_available {
266                    Some(GpuChunkSettings {
267                        gpu_memory_ratio: 0.7,
268                        gpu_min_chunk: 2048,
269                        overlap_compute: true,
270                        gpu_bandwidth: None,      // Option<u64>
271                        transfer_bandwidth: None, // Option<u64>
272                    })
273                } else {
274                    None
275                },
276            },
277        }
278    }
279
280    /// Intelligent chunking for parallel tensor operations
281    pub fn intelligent_parallel_process<T, F, R>(
282        data: Vec<T>,
283        op_type: TensorOpType,
284        device: torsh_core::device::DeviceType,
285        operation: F,
286    ) -> Vec<R>
287    where
288        T: Send + Sync,
289        R: Send + Sync,
290        F: Fn(T) -> R + Send + Sync,
291    {
292        let is_gpu_available = matches!(
293            device,
294            torsh_core::device::DeviceType::Cuda(_)
295                | torsh_core::device::DeviceType::Metal(_)
296                | torsh_core::device::DeviceType::Wgpu(_)
297        );
298
299        let _chunk_config =
300            create_optimal_chunk_config(data.len(), op_type, device, is_gpu_available);
301
302        // ✅ SciRS2 POLICY: Use scirs2_core::parallel_ops instead of direct rayon
303        #[cfg(feature = "parallel")]
304        {
305            use scirs2_core::parallel_ops::*;
306            data.into_par_iter().map(operation).collect()
307        }
308        #[cfg(not(feature = "parallel"))]
309        {
310            data.into_iter().map(operation).collect()
311        }
312    }
313}
314
315#[cfg(feature = "parallel")]
316use intelligent_chunking::*;
317
318/// Check if two shapes can be broadcasted together
319fn can_broadcast(shape1: &[usize], shape2: &[usize]) -> bool {
320    let max_dims = shape1.len().max(shape2.len());
321
322    for i in 0..max_dims {
323        let dim1 = if i < shape1.len() {
324            shape1[shape1.len() - 1 - i]
325        } else {
326            1
327        };
328        let dim2 = if i < shape2.len() {
329            shape2[shape2.len() - 1 - i]
330        } else {
331            1
332        };
333
334        if dim1 != dim2 && dim1 != 1 && dim2 != 1 {
335            return false;
336        }
337    }
338    true
339}
340
341/// Compute the resulting shape after broadcasting
342fn compute_broadcast_shape(shape1: &[usize], shape2: &[usize]) -> Result<Vec<usize>> {
343    let max_dims = shape1.len().max(shape2.len());
344    let mut result = Vec::with_capacity(max_dims);
345
346    for i in 0..max_dims {
347        let dim1 = if i < shape1.len() {
348            shape1[shape1.len() - 1 - i]
349        } else {
350            1
351        };
352        let dim2 = if i < shape2.len() {
353            shape2[shape2.len() - 1 - i]
354        } else {
355            1
356        };
357
358        if dim1 == dim2 {
359            result.push(dim1);
360        } else if dim1 == 1 {
361            result.push(dim2);
362        } else if dim2 == 1 {
363            result.push(dim1);
364        } else {
365            return Err(TorshError::ShapeMismatch {
366                expected: shape1.to_vec(),
367                got: shape2.to_vec(),
368            });
369        }
370    }
371
372    result.reverse();
373    Ok(result)
374}
375
376/// Compute the index in the original tensor given a flat index in the broadcasted tensor
377fn compute_broadcast_index(
378    flat_idx: usize,
379    broadcast_shape: &[usize],
380    original_shape: &[usize],
381) -> usize {
382    let mut result = 0;
383    let mut remaining = flat_idx;
384
385    let dims_diff = broadcast_shape.len() - original_shape.len();
386
387    for (i, &broadcast_dim) in broadcast_shape.iter().enumerate() {
388        // Calculate stride (product of remaining dimensions)
389        let stride = broadcast_shape[i + 1..].iter().product::<usize>().max(1);
390        let coord = remaining / stride;
391        remaining %= stride;
392
393        // Validate coordinate is within broadcast dimension
394        debug_assert!(
395            coord < broadcast_dim,
396            "Coordinate {} out of bounds for dimension {} of size {}",
397            coord,
398            i,
399            broadcast_dim
400        );
401
402        if i >= dims_diff {
403            let original_dim = original_shape[i - dims_diff];
404            let adjusted_coord = if original_dim == 1 { 0 } else { coord };
405            result = result * original_dim + adjusted_coord;
406        }
407    }
408
409    result
410}
411
412impl<T: TensorElement + Copy> Tensor<T> {
413    /// Whether two tensors read through the very same storage allocation.
414    ///
415    /// Binary ops borrow both operands out of storage instead of copying them,
416    /// and `RwLock::read` is not re-entrant: taking a guard on one buffer while
417    /// already holding one on the *same* buffer risks a dead-lock. Callers use
418    /// this to fall back to a materialised copy of the second operand — which
419    /// is what makes `t.add(&t)` and `t.add(&t.clone())` safe.
420    ///
421    /// Unlike [`Tensor::shares_storage`] this covers every storage variant,
422    /// including the two SIMD ones.
423    pub(crate) fn shares_storage_buffer(&self, other: &Self) -> bool {
424        match (&self.storage, &other.storage) {
425            (TensorStorage::InMemory(a), TensorStorage::InMemory(b)) => Arc::ptr_eq(a, b),
426            (TensorStorage::MemoryMapped(a), TensorStorage::MemoryMapped(b)) => Arc::ptr_eq(a, b),
427            #[cfg(feature = "simd")]
428            (TensorStorage::Aligned(a), TensorStorage::Aligned(b)) => Arc::ptr_eq(a, b),
429            #[cfg(feature = "simd")]
430            (TensorStorage::SimdOptimized(a), TensorStorage::SimdOptimized(b)) => Arc::ptr_eq(a, b),
431            #[cfg(feature = "gpu")]
432            (TensorStorage::Device { buffer: a, .. }, TensorStorage::Device { buffer: b, .. }) => {
433                Arc::ptr_eq(a, b)
434            }
435            _ => false,
436        }
437    }
438
439    /// Address of the storage allocation this tensor reads through.
440    ///
441    /// Used only to impose a total order on lock acquisition; the value is
442    /// never dereferenced.
443    fn storage_addr(&self) -> usize {
444        match &self.storage {
445            TensorStorage::InMemory(data) => Arc::as_ptr(data) as usize,
446            TensorStorage::MemoryMapped(storage) => Arc::as_ptr(storage) as usize,
447            #[cfg(feature = "simd")]
448            TensorStorage::Aligned(data) => Arc::as_ptr(data) as usize,
449            #[cfg(feature = "simd")]
450            TensorStorage::SimdOptimized(storage) => Arc::as_ptr(storage) as usize,
451            #[cfg(feature = "gpu")]
452            TensorStorage::Device { buffer, .. } => Arc::as_ptr(buffer) as usize,
453        }
454    }
455
456    /// Run `f` against contiguous, view-ordered slices of both operands.
457    ///
458    /// Neither operand is copied while they live in different buffers: each
459    /// slice is borrowed straight out of storage for the duration of the
460    /// closure. Strided views are materialised in view order first (so the
461    /// closure can always index row-major).
462    ///
463    /// # Locking
464    /// Two operands that share one allocation take a single copy of `other`
465    /// rather than nesting guards on the same (non-re-entrant) lock, which is
466    /// what makes `t.add(&t)` and `t.add(&t.clone())` safe. Distinct buffers
467    /// are locked in ascending address order, so two threads running mirrored
468    /// operations (`a op b` and `b op a`) can never build an acquisition cycle.
469    pub(crate) fn with_operand_slices<R, F>(&self, other: &Self, f: F) -> Result<R>
470    where
471        F: FnOnce(&[T], &[T]) -> Result<R>,
472    {
473        if self.shares_storage_buffer(other) {
474            // Materialise before acquiring this tensor's guard, never inside it.
475            let other_data = other.to_vec()?;
476            return self.with_contiguous_data(|lhs| f(lhs, &other_data));
477        }
478        if self.storage_addr() <= other.storage_addr() {
479            self.with_contiguous_data(|lhs| other.with_contiguous_data(|rhs| f(lhs, rhs)))
480        } else {
481            other.with_contiguous_data(|rhs| self.with_contiguous_data(|lhs| f(lhs, rhs)))
482        }
483    }
484
485    /// Try the hardware-SIMD fast path for a same-shape binary operation.
486    ///
487    /// Returns `Ok(None)` when the operands are not eligible — different
488    /// shapes, fewer than [`SIMD_BINARY_THRESHOLD`] elements, or an element
489    /// type without a vector kernel — in which case the caller falls back to
490    /// [`Tensor::elementwise_operation`]. `f32` and `f64` both have real
491    /// AVX2/NEON kernels in `scirs2_core::simd_ops`.
492    ///
493    /// Allocation profile: zero copies of the operands and exactly one pooled
494    /// output buffer, which the kernel initialises in full.
495    #[cfg(feature = "simd")]
496    fn try_simd_binary(&self, other: &Self, op: BinaryF32Op) -> Result<Option<Self>> {
497        let n = self.numel();
498        if n < SIMD_BINARY_THRESHOLD || self.shape() != other.shape() {
499            return Ok(None);
500        }
501        let is_f32 = std::any::TypeId::of::<T>() == std::any::TypeId::of::<f32>();
502        let is_f64 = std::any::TypeId::of::<T>() == std::any::TypeId::of::<f64>();
503        if !is_f32 && !is_f64 {
504            return Ok(None);
505        }
506
507        let result_data = self.with_operand_slices(other, |a, b| {
508            if a.len() != n || b.len() != n {
509                return Err(TorshError::ShapeMismatch {
510                    expected: vec![n],
511                    got: vec![b.len()],
512                });
513            }
514            let mut buf = global_acquire_uninit::<T>(n);
515            {
516                let uninit = &mut buf.as_uninit_slice_mut()[..n];
517                // SAFETY: `MaybeUninit<T>` has the layout of `T`, and every
518                // kernel invoked below is store-only — `simd_*_into` writes all
519                // `n` outputs before anything reads them — so the buffer is
520                // fully initialised when this block ends. That is exactly why
521                // no zero-fill pass is needed here.
522                let out: &mut [T] =
523                    unsafe { std::slice::from_raw_parts_mut(uninit.as_mut_ptr().cast::<T>(), n) };
524                if is_f32 {
525                    // SAFETY: the TypeId check above confirms `T == f32`, so
526                    // reinterpreting the same-layout slices is a no-op.
527                    unsafe {
528                        op.dispatch_into(
529                            std::slice::from_raw_parts(a.as_ptr().cast::<f32>(), n),
530                            std::slice::from_raw_parts(b.as_ptr().cast::<f32>(), n),
531                            std::slice::from_raw_parts_mut(out.as_mut_ptr().cast::<f32>(), n),
532                        );
533                    }
534                } else {
535                    // SAFETY: the TypeId check above confirms `T == f64`.
536                    unsafe {
537                        dispatch_f64_into(
538                            op,
539                            std::slice::from_raw_parts(a.as_ptr().cast::<f64>(), n),
540                            std::slice::from_raw_parts(b.as_ptr().cast::<f64>(), n),
541                            std::slice::from_raw_parts_mut(out.as_mut_ptr().cast::<f64>(), n),
542                        );
543                    }
544                }
545            }
546            Ok(buf.into_vec(n))
547        })?;
548
549        let result = Self::from_data(result_data, self.shape().dims().to_vec(), self.device)?;
550        Ok(Some(result))
551    }
552
553    /// Add scalar to all elements in-place
554    pub fn add_scalar_(&mut self, scalar: T) -> Result<()>
555    where
556        T: Copy + std::ops::Add<Output = T>,
557    {
558        // Ensure data is unique (copy-on-write)
559        self.make_unique()?;
560        self.apply_(|x| x + scalar)
561    }
562
563    /// Add scalar to all elements (returns new tensor)
564    ///
565    /// Records [`Operation::AddScalar`]: `map` is forward-only, so without this
566    /// the shifted tensor would be a `requires_grad` leaf that swallows the
567    /// gradient of everything upstream of it.
568    pub fn add_scalar(&self, scalar: T) -> Result<Self>
569    where
570        T: Copy + std::ops::Add<Output = T>,
571    {
572        let mut result = self.map(|x| x + scalar)?;
573        if crate::should_record_grad(self.requires_grad) {
574            result.requires_grad = true;
575            result.operation = Operation::AddScalar {
576                input: Arc::new(self.clone()),
577                scalar,
578            };
579        }
580        Ok(result)
581    }
582
583    /// Subtract scalar from all elements in-place
584    pub fn sub_scalar_(&mut self, scalar: T) -> Result<()>
585    where
586        T: Copy + std::ops::Sub<Output = T>,
587    {
588        self.make_unique()?;
589        self.apply_(|x| x - scalar)
590    }
591
592    /// Subtract scalar from all elements (returns new tensor)
593    ///
594    /// Records [`Operation::AddScalar`] with the negated constant — the
595    /// backward rule is the same identity — so the public bound stays
596    /// `Sub<Output = T>` and no `Neg`/`Add` bound leaks into the API.
597    pub fn sub_scalar(&self, scalar: T) -> Result<Self>
598    where
599        T: Copy + std::ops::Sub<Output = T>,
600    {
601        let mut result = self.map(|x| x - scalar)?;
602        if crate::should_record_grad(self.requires_grad) {
603            result.requires_grad = true;
604            result.operation = Operation::AddScalar {
605                input: Arc::new(self.clone()),
606                scalar: <T as TensorElement>::zero() - scalar,
607            };
608        }
609        Ok(result)
610    }
611
612    /// Multiply all elements by scalar in-place
613    pub fn mul_scalar_(&mut self, scalar: T) -> Result<()>
614    where
615        T: Copy + std::ops::Mul<Output = T>,
616    {
617        // Ensure data is unique (copy-on-write)
618        self.make_unique()?;
619        self.apply_(|x| x * scalar)
620    }
621
622    /// Multiply all elements by scalar (returns new tensor)
623    ///
624    /// The result joins the autograd graph when the input requires gradients:
625    /// `d/dinput (input * s) = s`. Recording a dedicated scalar node keeps the
626    /// tape free of a full tensor of copies of the constant.
627    pub fn mul_scalar(&self, scalar: T) -> Result<Self>
628    where
629        T: Copy + std::ops::Mul<Output = T>,
630    {
631        let mut result = self.map(|x| x * scalar)?;
632        if crate::should_record_grad(self.requires_grad) {
633            result.requires_grad = true;
634            result.operation = Operation::MulScalar {
635                input: Arc::new(self.clone()),
636                scalar,
637            };
638        }
639        Ok(result)
640    }
641
642    /// Divide all elements by scalar in-place
643    pub fn div_scalar_(&mut self, scalar: T) -> Result<()>
644    where
645        T: Copy + std::ops::Div<Output = T>,
646    {
647        self.make_unique()?;
648        self.apply_(|x| x / scalar)
649    }
650
651    /// Divide all elements by scalar (returns new tensor)
652    ///
653    /// The result joins the autograd graph when the input requires gradients:
654    /// `d/dinput (input / s) = 1 / s`.
655    pub fn div_scalar(&self, scalar: T) -> Result<Self>
656    where
657        T: Copy + std::ops::Div<Output = T>,
658    {
659        let mut result = self.map(|x| x / scalar)?;
660        if crate::should_record_grad(self.requires_grad) {
661            result.requires_grad = true;
662            result.operation = Operation::DivScalar {
663                input: Arc::new(self.clone()),
664                scalar,
665            };
666        }
667        Ok(result)
668    }
669
670    /// Element-wise addition with another tensor (supports broadcasting)
671    pub fn add(&self, other: &Self) -> Result<Self>
672    where
673        T: std::ops::Add<Output = T>,
674    {
675        // Handle broadcasting for different shapes
676        if self.shape() != other.shape() {
677            return self.broadcast_add(other);
678        }
679
680        // Hardware SIMD fast path (f32/f64, ≥ SIMD_BINARY_THRESHOLD elements),
681        // reading both operands in place and writing one pooled output buffer.
682        #[cfg(feature = "simd")]
683        if let Some(mut result) = self.try_simd_binary(other, BinaryF32Op::Add)? {
684            if crate::should_record_grad(self.requires_grad || other.requires_grad) {
685                result.requires_grad = true;
686                result.operation = Operation::Add {
687                    lhs: Arc::new(self.clone()),
688                    rhs: Arc::new(other.clone()),
689                };
690            }
691            return Ok(result);
692        }
693
694        // Same shape - use optimized elementwise operation
695        let mut result = self.elementwise_operation(other, |a, b| a + b)?;
696
697        // Preserve gradient tracking
698        if crate::should_record_grad(self.requires_grad || other.requires_grad) {
699            result.requires_grad = true;
700            result.operation = Operation::Add {
701                lhs: Arc::new(self.clone()),
702                rhs: Arc::new(other.clone()),
703            };
704        }
705
706        Ok(result)
707    }
708
709    /// Broadcasting-aware addition for different shapes
710    fn broadcast_add(&self, other: &Self) -> Result<Self>
711    where
712        T: std::ops::Add<Output = T>,
713    {
714        // Simple broadcasting implementation for common cases
715        let self_shape_binding = self.shape();
716        let other_shape_binding = other.shape();
717        let self_shape = self_shape_binding.dims();
718        let other_shape = other_shape_binding.dims();
719
720        // Check if broadcasting is possible
721        if !can_broadcast(self_shape, other_shape) {
722            return Err(TorshError::ShapeMismatch {
723                expected: self_shape.to_vec(),
724                got: other_shape.to_vec(),
725            });
726        }
727
728        // Compute the broadcasted shape
729        let broadcast_shape = compute_broadcast_shape(self_shape, other_shape)?;
730
731        // Perform broadcasting addition, reading both operands in place.
732        let total_elems: usize = broadcast_shape.iter().product();
733        let result_data = self.with_operand_slices(other, |self_data, other_data| {
734            let mut buf = global_acquire_uninit::<T>(total_elems);
735            let uninit = &mut buf.as_uninit_slice_mut()[..total_elems];
736            let mut count = 0;
737
738            for i in 0..total_elems {
739                let self_idx = compute_broadcast_index(i, &broadcast_shape, self_shape);
740                let other_idx = compute_broadcast_index(i, &broadcast_shape, other_shape);
741
742                let self_val = *self_data
743                    .get(self_idx)
744                    .ok_or_else(|| TorshError::IndexError {
745                        index: self_idx,
746                        size: self_data.len(),
747                    })?;
748                let other_val =
749                    *other_data
750                        .get(other_idx)
751                        .ok_or_else(|| TorshError::IndexError {
752                            index: other_idx,
753                            size: other_data.len(),
754                        })?;
755                uninit[count].write(self_val + other_val);
756                count += 1;
757            }
758
759            Ok(buf.into_vec(count))
760        })?;
761        let mut result = Self::from_data(result_data, broadcast_shape, self.device)?;
762
763        // Preserve gradient tracking
764        if crate::should_record_grad(self.requires_grad || other.requires_grad) {
765            result.requires_grad = true;
766            result.operation = Operation::Add {
767                lhs: Arc::new(self.clone()),
768                rhs: Arc::new(other.clone()),
769            };
770        }
771
772        Ok(result)
773    }
774
775    /// Element-wise subtraction with another tensor
776    pub fn sub(&self, other: &Self) -> Result<Self>
777    where
778        T: std::ops::Sub<Output = T>,
779    {
780        // Hardware SIMD fast path (f32/f64, ≥ SIMD_BINARY_THRESHOLD elements),
781        // reading both operands in place and writing one pooled output buffer.
782        #[cfg(feature = "simd")]
783        if let Some(mut result) = self.try_simd_binary(other, BinaryF32Op::Sub)? {
784            if crate::should_record_grad(self.requires_grad || other.requires_grad) {
785                result.requires_grad = true;
786                result.operation = crate::Operation::Sub {
787                    lhs: Arc::new(self.clone()),
788                    rhs: Arc::new(other.clone()),
789                };
790            }
791            return Ok(result);
792        }
793
794        let mut result = self.elementwise_operation(other, |a, b| a - b)?;
795
796        // Propagate requires_grad and record operation for autograd
797        if crate::should_record_grad(self.requires_grad || other.requires_grad) {
798            result.requires_grad = true;
799            result.operation = crate::Operation::Sub {
800                lhs: Arc::new(self.clone()),
801                rhs: Arc::new(other.clone()),
802            };
803        }
804
805        Ok(result)
806    }
807
808    /// Element-wise multiplication with another tensor (supports broadcasting).
809    ///
810    /// When either operand requires gradients the result records
811    /// `Operation::Mul` with the *un-broadcast* operands, so backward can apply
812    /// `d/dlhs = rhs`, `d/drhs = lhs` and fold each gradient back to its
813    /// operand's own shape.
814    pub fn mul(&self, other: &Self) -> Result<Self>
815    where
816        T: std::ops::Mul<Output = T>,
817    {
818        // Hardware SIMD fast path (f32/f64, ≥ SIMD_BINARY_THRESHOLD elements),
819        // reading both operands in place and writing one pooled output buffer.
820        #[cfg(feature = "simd")]
821        if let Some(mut result) = self.try_simd_binary(other, BinaryF32Op::Mul)? {
822            if crate::should_record_grad(self.requires_grad || other.requires_grad) {
823                result.requires_grad = true;
824                result.operation = crate::Operation::Mul {
825                    lhs: Arc::new(self.clone()),
826                    rhs: Arc::new(other.clone()),
827                };
828            }
829            return Ok(result);
830        }
831
832        let mut result = self.elementwise_operation(other, |a, b| a * b)?;
833
834        // Propagate requires_grad and record the operation for autograd
835        if crate::should_record_grad(self.requires_grad || other.requires_grad) {
836            result.requires_grad = true;
837            result.operation = crate::Operation::Mul {
838                lhs: Arc::new(self.clone()),
839                rhs: Arc::new(other.clone()),
840            };
841        }
842
843        Ok(result)
844    }
845
846    /// Element-wise division with another tensor (supports broadcasting).
847    ///
848    /// When either operand requires gradients the result records
849    /// `Operation::Div` with the *un-broadcast* operands, so backward can apply
850    /// `d/dlhs = 1 / rhs`, `d/drhs = -lhs / rhs²` and fold each gradient back to
851    /// its operand's own shape.
852    pub fn div(&self, other: &Self) -> Result<Self>
853    where
854        T: std::ops::Div<Output = T>,
855    {
856        // Hardware SIMD fast path (f32/f64, ≥ SIMD_BINARY_THRESHOLD elements),
857        // reading both operands in place and writing one pooled output buffer.
858        #[cfg(feature = "simd")]
859        if let Some(mut result) = self.try_simd_binary(other, BinaryF32Op::Div)? {
860            if crate::should_record_grad(self.requires_grad || other.requires_grad) {
861                result.requires_grad = true;
862                result.operation = crate::Operation::Div {
863                    lhs: Arc::new(self.clone()),
864                    rhs: Arc::new(other.clone()),
865                };
866            }
867            return Ok(result);
868        }
869
870        let mut result = self.elementwise_operation(other, |a, b| a / b)?;
871
872        // Propagate requires_grad and record the operation for autograd
873        if crate::should_record_grad(self.requires_grad || other.requires_grad) {
874            result.requires_grad = true;
875            result.operation = crate::Operation::Div {
876                lhs: Arc::new(self.clone()),
877                rhs: Arc::new(other.clone()),
878            };
879        }
880
881        Ok(result)
882    }
883
884    /// Handle broadcasting binary operations
885    fn broadcast_binary_op<F>(&self, other: &Self, op: F) -> Result<Self>
886    where
887        F: Fn(T, T) -> T + Send + Sync,
888    {
889        use crate::broadcast::BroadcastOps;
890
891        let self_shape_binding = self.shape();
892        let self_shape = self_shape_binding.dims();
893        let other_shape_binding = other.shape();
894        let other_shape = other_shape_binding.dims();
895
896        // Compute the broadcast result shape
897        let broadcast_shape = BroadcastOps::compute_broadcast_shape(self_shape, other_shape)?;
898
899        let total_elements = broadcast_shape.iter().product::<usize>();
900        let result_data = self.with_operand_slices(other, |self_data, other_data| {
901            let mut buf = global_acquire_uninit::<T>(total_elements);
902            let uninit = &mut buf.as_uninit_slice_mut()[..total_elements];
903            let mut count = 0;
904
905            // Generate all possible indices for the broadcast shape
906            let mut indices = vec![0; broadcast_shape.len()];
907            for _ in 0..total_elements {
908                // Map broadcast indices to original tensor indices
909                let self_idx =
910                    self.compute_broadcast_index(&indices, self_shape, &broadcast_shape)?;
911                let other_idx =
912                    other.compute_broadcast_index(&indices, other_shape, &broadcast_shape)?;
913
914                let lhs = *self_data
915                    .get(self_idx)
916                    .ok_or_else(|| TorshError::IndexError {
917                        index: self_idx,
918                        size: self_data.len(),
919                    })?;
920                let rhs = *other_data
921                    .get(other_idx)
922                    .ok_or_else(|| TorshError::IndexError {
923                        index: other_idx,
924                        size: other_data.len(),
925                    })?;
926                uninit[count].write(op(lhs, rhs));
927                count += 1;
928
929                // Increment indices (like an odometer)
930                Self::increment_indices(&mut indices, &broadcast_shape);
931            }
932
933            Ok(buf.into_vec(count))
934        })?;
935        Self::from_data(result_data, broadcast_shape, self.device)
936    }
937
938    /// Helper function to increment multi-dimensional indices
939    fn increment_indices(indices: &mut [usize], shape: &[usize]) {
940        for i in (0..indices.len()).rev() {
941            indices[i] += 1;
942            if indices[i] < shape[i] {
943                break;
944            }
945            indices[i] = 0;
946        }
947    }
948
949    /// Compute flat index from broadcast indices for this tensor
950    fn compute_broadcast_index(
951        &self,
952        broadcast_indices: &[usize],
953        original_shape: &[usize],
954        broadcast_shape: &[usize],
955    ) -> Result<usize> {
956        let ndim_diff = broadcast_shape.len() - original_shape.len();
957        let mut flat_index = 0;
958        let mut stride = 1;
959
960        for i in (0..original_shape.len()).rev() {
961            let broadcast_idx = broadcast_indices[ndim_diff + i];
962            let original_size = original_shape[i];
963
964            // Handle broadcasting: if original size is 1, use index 0
965            let actual_idx = if original_size == 1 { 0 } else { broadcast_idx };
966
967            flat_index += actual_idx * stride;
968            stride *= original_size;
969        }
970
971        Ok(flat_index)
972    }
973
974    /// Generic element-wise binary operation over two same-shape tensors.
975    ///
976    /// Both operands are read in place (no defensive copies) and the result is
977    /// written into a single pooled buffer. Tensors with at least
978    /// [`PARALLEL_ELEMENTWISE_THRESHOLD`] elements are split into
979    /// cache-sized chunks across `scirs2_core::parallel_ops` workers; smaller
980    /// ones run a single vectorisable pass.
981    ///
982    /// Element types with a hardware kernel (`f32`, `f64`) reach this function
983    /// only below the SIMD threshold — see [`Tensor::try_simd_binary`].
984    fn elementwise_operation<F>(&self, other: &Self, op: F) -> Result<Self>
985    where
986        F: Fn(T, T) -> T + Send + Sync,
987    {
988        // Handle broadcasting if shapes don't match
989        if self.shape() != other.shape() {
990            return self.broadcast_binary_op(other, op);
991        }
992
993        let n = self.numel();
994        let device = self.device;
995        let result_data = self.with_operand_slices(other, |a, b| {
996            if a.len() != n || b.len() != n {
997                return Err(TorshError::ShapeMismatch {
998                    expected: vec![n],
999                    got: vec![b.len()],
1000                });
1001            }
1002
1003            let mut buf = global_acquire_uninit::<T>(n);
1004            {
1005                let uninit = &mut buf.as_uninit_slice_mut()[..n];
1006
1007                // Operation-aware chunking: the chunk config that used to be
1008                // computed and discarded now actually sizes the work units.
1009                #[cfg(feature = "parallel")]
1010                {
1011                    if n >= PARALLEL_ELEMENTWISE_THRESHOLD {
1012                        use scirs2_core::parallel_ops::*;
1013                        let config = create_optimal_chunk_config(
1014                            n,
1015                            TensorOpType::ElementWise,
1016                            device,
1017                            false,
1018                        );
1019                        let chunk = config.max_chunk_size.max(config.min_chunk_size).max(1);
1020                        uninit
1021                            .par_chunks_mut(chunk)
1022                            .zip(a.par_chunks(chunk))
1023                            .zip(b.par_chunks(chunk))
1024                            .for_each(|((out_chunk, a_chunk), b_chunk)| {
1025                                for (slot, (&x, &y)) in
1026                                    out_chunk.iter_mut().zip(a_chunk.iter().zip(b_chunk.iter()))
1027                                {
1028                                    slot.write(op(x, y));
1029                                }
1030                            });
1031                        return Ok(buf.into_vec(n));
1032                    }
1033                }
1034
1035                for (slot, (&x, &y)) in uninit.iter_mut().zip(a.iter().zip(b.iter())) {
1036                    slot.write(op(x, y));
1037                }
1038            }
1039            Ok(buf.into_vec(n))
1040        })?;
1041
1042        Self::from_data(result_data, self.shape().dims().to_vec(), device)
1043    }
1044}
1045
1046// ─────────────────────────────────────────────────────────────────────────────
1047// Block C – In-place tensor × tensor arithmetic with f32 SIMD fast paths
1048// ─────────────────────────────────────────────────────────────────────────────
1049
1050impl<T: TensorElement + Copy> Tensor<T> {
1051    /// Element-wise in-place addition: `self += other`.
1052    ///
1053    /// Supports broadcasting `other` into `self`'s shape (e.g. adding a `[C]`
1054    /// bias to a `[B, C]` activation). If broadcasting would force `self` to
1055    /// grow, an error is returned — in-place ops cannot resize the destination.
1056    /// For f32 tensors with ≥ 1024 elements and matching shapes, this routes
1057    /// through the SIMD-backed `simd_ops_f32::add_assign_f32`. No new tensor
1058    /// data buffer is ever allocated.
1059    ///
1060    /// # Errors
1061    /// - `requires_grad` is true (autograd cannot be tracked through in-place mutations)
1062    /// - shapes are not broadcast-compatible
1063    /// - broadcasting would require `self` to grow
1064    pub fn add_(&mut self, other: &Self) -> Result<&mut Self>
1065    where
1066        T: std::ops::Add<Output = T>,
1067    {
1068        self.inplace_binary_op(
1069            other,
1070            "add_",
1071            crate::simd_ops_f32::add_assign_f32,
1072            |a, b| a + b,
1073        )
1074    }
1075
1076    /// Element-wise in-place subtraction: `self -= other` (with broadcast of `other` into `self`).
1077    ///
1078    /// For f32 tensors with ≥ 1024 elements and matching shapes, this routes
1079    /// through `simd_ops_f32::sub_assign_f32`.
1080    pub fn sub_(&mut self, other: &Self) -> Result<&mut Self>
1081    where
1082        T: std::ops::Sub<Output = T>,
1083    {
1084        self.inplace_binary_op(
1085            other,
1086            "sub_",
1087            crate::simd_ops_f32::sub_assign_f32,
1088            |a, b| a - b,
1089        )
1090    }
1091
1092    /// Element-wise in-place multiplication: `self *= other` (with broadcast of `other` into `self`).
1093    ///
1094    /// For f32 tensors with ≥ 1024 elements and matching shapes, this routes
1095    /// through `simd_ops_f32::mul_assign_f32`.
1096    pub fn mul_(&mut self, other: &Self) -> Result<&mut Self>
1097    where
1098        T: std::ops::Mul<Output = T>,
1099    {
1100        self.inplace_binary_op(
1101            other,
1102            "mul_",
1103            crate::simd_ops_f32::mul_assign_f32,
1104            |a, b| a * b,
1105        )
1106    }
1107
1108    /// Element-wise in-place division: `self /= other` (with broadcast of `other` into `self`).
1109    ///
1110    /// For f32 tensors with ≥ 1024 elements and matching shapes, this routes
1111    /// through `simd_ops_f32::div_assign_f32`.
1112    pub fn div_(&mut self, other: &Self) -> Result<&mut Self>
1113    where
1114        T: std::ops::Div<Output = T>,
1115    {
1116        self.inplace_binary_op(
1117            other,
1118            "div_",
1119            crate::simd_ops_f32::div_assign_f32,
1120            |a, b| a / b,
1121        )
1122    }
1123
1124    /// Shared in-place binary-op driver: validates shapes (with broadcast-into-self),
1125    /// then dispatches to the f32 SIMD fast path when applicable, otherwise to a
1126    /// single-locked slice loop. Never allocates a new data buffer.
1127    fn inplace_binary_op<S, F>(
1128        &mut self,
1129        other: &Self,
1130        op_name: &str,
1131        simd_f32: S,
1132        scalar_op: F,
1133    ) -> Result<&mut Self>
1134    where
1135        S: Fn(&mut [f32], &[f32]),
1136        F: Fn(T, T) -> T,
1137    {
1138        if self.requires_grad {
1139            return Err(TorshError::InvalidArgument(format!(
1140                "In-place operation `{op_name}` on tensor that requires grad is not allowed"
1141            )));
1142        }
1143
1144        // f32 SIMD fast path: identical shapes, large enough to amortise dispatch.
1145        if std::any::TypeId::of::<T>() == std::any::TypeId::of::<f32>()
1146            && self.shape() == other.shape()
1147            && self.numel() >= 1024
1148        {
1149            self.make_unique()?;
1150            let other_data = other.data()?;
1151            self.storage.with_slice_mut(|out_t: &mut [T]| {
1152                // Safety: TypeId guard above confirms T == f32, so reinterpreting
1153                // &mut [T] / &[T] as &mut [f32] / &[f32] is a no-op.
1154                let out_f32: &mut [f32] = unsafe {
1155                    std::slice::from_raw_parts_mut(out_t.as_mut_ptr() as *mut f32, out_t.len())
1156                };
1157                let rhs_f32: &[f32] = unsafe {
1158                    std::slice::from_raw_parts(other_data.as_ptr() as *const f32, other_data.len())
1159                };
1160                simd_f32(out_f32, rhs_f32);
1161                Ok(())
1162            })?;
1163            return Ok(self);
1164        }
1165
1166        // Shape validation + broadcast-into-self path.
1167        let self_dims_vec = self.shape().dims().to_vec();
1168        let other_dims_vec = other.shape().dims().to_vec();
1169        let other_data = other.data()?;
1170
1171        if self_dims_vec == other_dims_vec {
1172            // Equal shapes, generic-T scalar pass.
1173            self.make_unique()?;
1174            self.storage.with_slice_mut(|slice: &mut [T]| {
1175                debug_assert_eq!(slice.len(), other_data.len());
1176                for (dst, src) in slice.iter_mut().zip(other_data.iter()) {
1177                    *dst = scalar_op(*dst, *src);
1178                }
1179                Ok(())
1180            })?;
1181            return Ok(self);
1182        }
1183
1184        // Broadcast path: must be broadcast-compatible AND self's shape must
1185        // already equal the broadcast shape (cannot grow self in place).
1186        use crate::broadcast::BroadcastOps;
1187        let broadcast_shape =
1188            BroadcastOps::compute_broadcast_shape(&self_dims_vec, &other_dims_vec)?;
1189        if broadcast_shape != self_dims_vec {
1190            return Err(TorshError::ShapeMismatch {
1191                expected: self_dims_vec,
1192                got: broadcast_shape,
1193            });
1194        }
1195
1196        self.make_unique()?;
1197        let total = self_dims_vec.iter().product::<usize>();
1198        let self_dims = self_dims_vec.as_slice();
1199        let other_dims = other_dims_vec.as_slice();
1200        self.storage.with_slice_mut(|slice: &mut [T]| {
1201            for flat_idx in 0..total {
1202                let multi_index = BroadcastOps::flat_to_multi_index(flat_idx, self_dims);
1203                let other_idx =
1204                    BroadcastOps::compute_broadcast_index(&multi_index, other_dims, self_dims)?;
1205                slice[flat_idx] = scalar_op(slice[flat_idx], other_data[other_idx]);
1206            }
1207            Ok(())
1208        })?;
1209
1210        Ok(self)
1211    }
1212}
1213
1214// Internal operations for autograd (general implementations)
1215impl<T: TensorElement + Copy> Tensor<T> {
1216    /// Add operation (used by autograd backward pass)
1217    pub fn add_op(&self, other: &Self) -> Result<Self>
1218    where
1219        T: std::ops::Add<Output = T>,
1220    {
1221        self.add(other)
1222    }
1223
1224    /// Multiply operation (used by autograd backward pass)
1225    pub fn mul_op(&self, other: &Self) -> Result<Self>
1226    where
1227        T: std::ops::Mul<Output = T>,
1228    {
1229        self.mul(other)
1230    }
1231
1232    /// Sigmoid activation function with SIMD optimization
1233    pub fn sigmoid(&self) -> Result<Self>
1234    where
1235        T: torsh_core::dtype::FloatElement,
1236    {
1237        let result = self.sigmoid_forward()?;
1238        Ok(self.record_unary(result, UnaryKind::Sigmoid))
1239    }
1240
1241    /// Forward-only sigmoid (all dispatch paths); autograd is recorded by the
1242    /// public [`Tensor::sigmoid`] wrapper.
1243    fn sigmoid_forward(&self) -> Result<Self>
1244    where
1245        T: torsh_core::dtype::FloatElement,
1246    {
1247        // GPU fast path: f32 CUDA tensors dispatch to oxicuda's ComputeBackend.
1248        // Declines to None (CPU fallback) unless a GPU backend is active.
1249        #[cfg(feature = "gpu")]
1250        if let Some(result) =
1251            crate::gpu_dispatch::try_unary_f32(self, crate::gpu_dispatch::UnaryOp::Sigmoid)
1252        {
1253            return Ok(result);
1254        }
1255
1256        // ✅ SciRS2 SIMD Optimization - Vectorized sigmoid for f32 tensors
1257        #[cfg(feature = "simd")]
1258        {
1259            if std::any::TypeId::of::<T>() == std::any::TypeId::of::<f32>() && self.numel() > 1000 {
1260                return self.simd_sigmoid_f32();
1261            }
1262        }
1263
1264        // ✅ SciRS2 Parallel Processing - Use parallel computation for medium tensors
1265        #[cfg(feature = "parallel")]
1266        {
1267            if self.numel() > 100 {
1268                let one = <T as scirs2_core::numeric::One>::one();
1269                return self.parallel_map(|x| {
1270                    // sigmoid(x) = 1 / (1 + exp(-x))
1271                    one / (one + (-x).exp())
1272                });
1273            }
1274        }
1275
1276        // Fallback to sequential tensor operations
1277        let one = <T as scirs2_core::numeric::One>::one();
1278        let neg_self = self.neg()?;
1279        let exp_neg = neg_self.exp()?;
1280        let one_plus_exp = exp_neg.add_scalar(one)?;
1281        let ones = Self::ones(self.shape().dims(), self.device)?;
1282        ones.div(&one_plus_exp)
1283    }
1284
1285    /// SIMD-optimized sigmoid activation function for f32 tensors
1286    #[cfg(feature = "simd")]
1287    fn simd_sigmoid_f32(&self) -> Result<Self> {
1288        self.simd_activation_f32(adaptive_simd::adaptive_simd_sigmoid_f32)
1289    }
1290
1291    /// Shared driver for the f32 SIMD activations.
1292    ///
1293    /// Reads the operand straight out of storage instead of copying it with
1294    /// `data()`, and reinterprets the kernel's result buffer as `Vec<T>` in a
1295    /// single step rather than transmuting element by element. Only the one
1296    /// buffer produced by the kernel is allocated.
1297    ///
1298    /// # Safety contract
1299    /// Callers must have established `T == f32` (the `TypeId` guards in
1300    /// [`Tensor::relu`] and [`Tensor::sigmoid`] do so).
1301    #[cfg(feature = "simd")]
1302    fn simd_activation_f32<K>(&self, kernel: K) -> Result<Self>
1303    where
1304        K: Fn(&scirs2_core::ndarray::ArrayView1<f32>) -> Array1<f32>,
1305    {
1306        use scirs2_core::ndarray::ArrayView1;
1307
1308        let numel = self.numel();
1309        let result_data: Vec<T> = self.with_contiguous_data(|data| {
1310            // SAFETY: the caller's TypeId guard confirms `T == f32`, so the
1311            // slice can be reinterpreted without copying.
1312            let data_f32: &[f32] =
1313                unsafe { std::slice::from_raw_parts(data.as_ptr().cast::<f32>(), numel) };
1314            let result_array = kernel(&ArrayView1::from(data_f32));
1315            let (values, _offset) = result_array.into_raw_vec_and_offset();
1316            // SAFETY: `T == f32`, so the buffer is already a valid `Vec<T>`;
1317            // reinterpreting it whole avoids a per-element `transmute_copy`
1318            // and a second allocation.
1319            Ok(unsafe {
1320                let mut values = std::mem::ManuallyDrop::new(values);
1321                Vec::from_raw_parts(
1322                    values.as_mut_ptr().cast::<T>(),
1323                    values.len(),
1324                    values.capacity(),
1325                )
1326            })
1327        })?;
1328
1329        Self::from_data(result_data, self.shape().dims().to_vec(), self.device)
1330    }
1331
1332    /// ReLU activation function (Rectified Linear Unit) with SIMD optimization
1333    pub fn relu(&self) -> Result<Self>
1334    where
1335        T: std::cmp::PartialOrd + scirs2_core::numeric::Zero,
1336    {
1337        let result = self.relu_forward()?;
1338        Ok(self.record_unary(result, UnaryKind::Relu))
1339    }
1340
1341    /// Forward-only ReLU (all dispatch paths); autograd is recorded by the
1342    /// public [`Tensor::relu`] wrapper.
1343    fn relu_forward(&self) -> Result<Self>
1344    where
1345        T: std::cmp::PartialOrd + scirs2_core::numeric::Zero,
1346    {
1347        // GPU fast path: f32 CUDA tensors dispatch to oxicuda's ComputeBackend.
1348        // Declines to None (CPU fallback) unless a GPU backend is active.
1349        #[cfg(feature = "gpu")]
1350        if let Some(result) =
1351            crate::gpu_dispatch::try_unary_f32(self, crate::gpu_dispatch::UnaryOp::Relu)
1352        {
1353            return Ok(result);
1354        }
1355
1356        let zero = <T as scirs2_core::numeric::Zero>::zero();
1357
1358        // ✅ SciRS2 SIMD Optimization - Vectorized ReLU for f32 tensors
1359        #[cfg(feature = "simd")]
1360        {
1361            if std::any::TypeId::of::<T>() == std::any::TypeId::of::<f32>() && self.numel() > 1000 {
1362                return self.simd_relu_f32();
1363            }
1364        }
1365
1366        // ✅ SciRS2 Parallel Processing - Use parallel map for medium tensors
1367        #[cfg(feature = "parallel")]
1368        {
1369            if self.numel() > 100 {
1370                return self.parallel_map(|x| if x > zero { x } else { zero });
1371            }
1372        }
1373
1374        // Fallback to sequential processing
1375        self.map(|x| if x > zero { x } else { zero })
1376    }
1377
1378    /// SIMD-optimized ReLU activation function for f32 tensors
1379    #[cfg(feature = "simd")]
1380    fn simd_relu_f32(&self) -> Result<Self> {
1381        self.simd_activation_f32(adaptive_simd::adaptive_simd_relu_f32)
1382    }
1383
1384    /// SciRS2 Intelligent parallel map operation for medium-sized tensors with operation-aware chunking
1385    #[cfg(feature = "parallel")]
1386    fn parallel_map<F>(&self, op: F) -> Result<Self>
1387    where
1388        F: Fn(T) -> T + Send + Sync,
1389    {
1390        let data = self.data()?;
1391
1392        // Use intelligent chunking system - activation functions get specialized chunking strategy
1393        let result_data = intelligent_parallel_process(
1394            data.iter().copied().collect::<Vec<_>>(),
1395            TensorOpType::Activation, // Most parallel_map calls are for activation functions
1396            self.device.clone(),
1397            op,
1398        );
1399
1400        Self::from_data(result_data, self.shape().dims().to_vec(), self.device)
1401    }
1402
1403    /// Element-wise minimum with another tensor (supports broadcasting).
1404    ///
1405    /// Records [`Operation::Minimum`] with the *un-broadcast* operands, so
1406    /// backward sends each element's gradient to whichever operand was smaller
1407    /// (splitting `0.5 / 0.5` on an exact tie) and folds it back to that
1408    /// operand's own shape.
1409    pub fn minimum(&self, other: &Self) -> Result<Self>
1410    where
1411        T: std::cmp::PartialOrd,
1412    {
1413        let result = self.elementwise_operation(other, |a, b| if a < b { a } else { b })?;
1414        Ok(self.record_extremum(other, result, false))
1415    }
1416
1417    /// Element-wise maximum with another tensor (supports broadcasting).
1418    ///
1419    /// Records [`Operation::Maximum`] with the *un-broadcast* operands. This is
1420    /// what makes `relu(x) == x.maximum(&zeros)` differentiable, which is the
1421    /// form `torsh_nn::functional::relu` uses.
1422    pub fn maximum(&self, other: &Self) -> Result<Self>
1423    where
1424        T: std::cmp::PartialOrd,
1425    {
1426        let result = self.elementwise_operation(other, |a, b| if a > b { a } else { b })?;
1427        Ok(self.record_extremum(other, result, true))
1428    }
1429
1430    /// Clamp tensor values between `min` and `max` bounds.
1431    ///
1432    /// Records [`Operation::ClampBounds`]: the gradient passes wherever the
1433    /// element was left alone (`min <= x <= max`, inclusive at both bounds, as
1434    /// in ATen's `clamp_backward`) and is zero wherever the clamp moved it.
1435    pub fn clamp(&self, min: T, max: T) -> Result<Self>
1436    where
1437        T: std::cmp::PartialOrd + Copy,
1438    {
1439        self.clamp_bounds(Some(min), Some(max))
1440    }
1441
1442    /// Clamp tensor values from below only — PyTorch's `tensor.clamp_min(min)`.
1443    ///
1444    /// Gradient passes wherever `x >= min` (inclusive) and is zero below it.
1445    pub fn clamp_min(&self, min: T) -> Result<Self>
1446    where
1447        T: std::cmp::PartialOrd + Copy,
1448    {
1449        self.clamp_bounds(Some(min), None)
1450    }
1451
1452    /// Clamp tensor values from above only — PyTorch's `tensor.clamp_max(max)`.
1453    ///
1454    /// Gradient passes wherever `x <= max` (inclusive) and is zero above it.
1455    pub fn clamp_max(&self, max: T) -> Result<Self>
1456    where
1457        T: std::cmp::PartialOrd + Copy,
1458    {
1459        self.clamp_bounds(None, Some(max))
1460    }
1461
1462    /// Shared implementation of [`Tensor::clamp`], [`Tensor::clamp_min`] and
1463    /// [`Tensor::clamp_max`]: apply whichever bounds were supplied, then record
1464    /// them so backward can reproduce the same predicate.
1465    fn clamp_bounds(&self, min: Option<T>, max: Option<T>) -> Result<Self>
1466    where
1467        T: std::cmp::PartialOrd + Copy,
1468    {
1469        let data = self.to_vec()?;
1470        // Exactly the original `if x < min { min } else if x > max { max }
1471        // else { x }` ladder, with an absent bound skipping its arm. Order
1472        // matters and is preserved: for the degenerate `min > max` the lower
1473        // bound still wins, as it did before.
1474        let clamped_data: Vec<T> = data
1475            .iter()
1476            .map(|&x| match (min, max) {
1477                (Some(lower), _) if x < lower => lower,
1478                (_, Some(upper)) if x > upper => upper,
1479                _ => x,
1480            })
1481            .collect();
1482
1483        let mut result = Self::from_data(
1484            clamped_data,
1485            self.shape().dims().to_vec(),
1486            self.device.clone(),
1487        )?;
1488
1489        if crate::should_record_grad(self.requires_grad) {
1490            result.requires_grad = true;
1491            result.operation = Operation::ClampBounds {
1492                input: Arc::new(self.clone()),
1493                min,
1494                max,
1495            };
1496        }
1497
1498        Ok(result)
1499    }
1500
1501    // ✅ clamp_ moved to in-place operations section below for PyTorch compatibility
1502
1503    /// Dot product with another tensor (for 1D tensors)
1504    pub fn dot(&self, other: &Self) -> Result<Self>
1505    where
1506        T: std::ops::Mul<Output = T> + std::ops::Add<Output = T> + scirs2_core::numeric::Zero,
1507    {
1508        // For now, implement as element-wise multiply then sum
1509        let elementwise = self.mul(other)?;
1510        elementwise.sum()
1511    }
1512}
1513
1514// SciRS2 backend integration for optimized operations
1515impl<T: TensorElement + Copy + scirs2_core::numeric::FromPrimitive> Tensor<T> {
1516    /// Use SciRS2 backend for optimized tensor addition
1517    pub fn add_scirs2(&self, other: &Self) -> Result<Self>
1518    where
1519        T: std::ops::Add<Output = T> + scirs2_core::numeric::Float,
1520    {
1521        // TODO: Integrate with actual SciRS2 backend
1522        // For now, fall back to basic implementation
1523        self.add(other)
1524    }
1525
1526    /// Use SciRS2 backend for optimized tensor multiplication
1527    pub fn mul_scirs2(&self, other: &Self) -> Result<Self>
1528    where
1529        T: std::ops::Mul<Output = T> + scirs2_core::numeric::Float,
1530    {
1531        // TODO: Integrate with actual SciRS2 backend
1532        // For now, fall back to basic implementation
1533        self.mul(other)
1534    }
1535
1536    /// Use SciRS2 backend for optimized tensor subtraction
1537    pub fn sub_scirs2(&self, other: &Self) -> Result<Self>
1538    where
1539        T: std::ops::Sub<Output = T> + scirs2_core::numeric::Float,
1540    {
1541        // TODO: Integrate with actual SciRS2 backend
1542        // For now, fall back to basic implementation
1543        self.sub(other)
1544    }
1545
1546    /// Use SciRS2 backend for optimized tensor division
1547    pub fn div_scirs2(&self, other: &Self) -> Result<Self>
1548    where
1549        T: std::ops::Div<Output = T> + scirs2_core::numeric::Float,
1550    {
1551        // TODO: Integrate with actual SciRS2 backend
1552        // For now, fall back to basic implementation
1553        self.div(other)
1554    }
1555}
1556
1557// Operator overloads for convenient syntax
1558impl<T: TensorElement + Copy> std::ops::Add for &Tensor<T>
1559where
1560    T: std::ops::Add<Output = T>,
1561{
1562    type Output = Tensor<T>;
1563
1564    fn add(self, rhs: Self) -> Self::Output {
1565        self.add(rhs).expect("tensor addition should succeed")
1566    }
1567}
1568
1569impl<T: TensorElement + Copy> std::ops::Sub for &Tensor<T>
1570where
1571    T: std::ops::Sub<Output = T>,
1572{
1573    type Output = Tensor<T>;
1574
1575    fn sub(self, rhs: Self) -> Self::Output {
1576        self.sub(rhs).expect("tensor subtraction should succeed")
1577    }
1578}
1579
1580impl<T: TensorElement + Copy> std::ops::Mul for &Tensor<T>
1581where
1582    T: std::ops::Mul<Output = T>,
1583{
1584    type Output = Tensor<T>;
1585
1586    fn mul(self, rhs: Self) -> Self::Output {
1587        self.mul(rhs).expect("tensor multiplication should succeed")
1588    }
1589}
1590
1591impl<T: TensorElement + Copy> std::ops::Div for &Tensor<T>
1592where
1593    T: std::ops::Div<Output = T>,
1594{
1595    type Output = Tensor<T>;
1596
1597    fn div(self, rhs: Self) -> Self::Output {
1598        self.div(rhs).expect("tensor division should succeed")
1599    }
1600}
1601
1602// Negation operator
1603impl<T: TensorElement + Copy> std::ops::Neg for &Tensor<T>
1604where
1605    T: std::ops::Neg<Output = T>,
1606{
1607    type Output = Tensor<T>;
1608
1609    fn neg(self) -> Self::Output {
1610        let mut result = self.map(|x| -x).expect("negation map should succeed");
1611        // `map` is forward-only, so the operator has to record its own node;
1612        // negation is `MulScalar(-1)`, whose backward arm already exists.
1613        if crate::should_record_grad(self.requires_grad()) {
1614            result.requires_grad = true;
1615            result.operation = Operation::MulScalar {
1616                input: Arc::new(self.clone()),
1617                scalar: -<T as TensorElement>::one(),
1618            };
1619        }
1620        result
1621    }
1622}
1623
1624// ✅ SciRS2 ADVANCED PERFORMANCE FEATURES
1625// High-performance implementations leveraging SciRS2 ecosystem
1626
1627impl<T: TensorElement + Copy + scirs2_core::numeric::Float> Tensor<T> {
1628    /// Element-wise addition with SIMD acceleration (SciRS2)
1629    ///
1630    /// Uses real hardware SIMD instructions (AVX2/NEON) via `scirs2_core::simd_ops::SimdUnifiedOps`
1631    #[cfg(feature = "simd")]
1632    pub fn add_simd(&self, other: &Self) -> Result<Self>
1633    where
1634        T: scirs2_core::simd_ops::SimdUnifiedOps,
1635    {
1636        use scirs2_core::ndarray::Array1;
1637
1638        // Shape check
1639        if self.shape().dims() != other.shape().dims() {
1640            return Err(TorshError::ShapeMismatch {
1641                expected: self.shape().dims().to_vec(),
1642                got: other.shape().dims().to_vec(),
1643            });
1644        }
1645
1646        // Get data as vectors
1647        let data_a = self.to_vec()?;
1648        let data_b = other.to_vec()?;
1649
1650        // Create ndarray arrays
1651        let arr_a = Array1::from_vec(data_a);
1652        let arr_b = Array1::from_vec(data_b);
1653
1654        // Use REAL SIMD operation (not Rayon!)
1655        let result_arr = T::simd_add(&arr_a.view(), &arr_b.view());
1656
1657        // Convert back to Tensor
1658        Tensor::from_vec(result_arr.to_vec(), self.shape().dims())
1659    }
1660
1661    /// Element-wise multiplication with SIMD acceleration (SciRS2)
1662    ///
1663    /// Uses real hardware SIMD instructions (AVX2/NEON) via `scirs2_core::simd_ops::SimdUnifiedOps`
1664    #[cfg(feature = "simd")]
1665    pub fn mul_simd(&self, other: &Self) -> Result<Self>
1666    where
1667        T: scirs2_core::simd_ops::SimdUnifiedOps,
1668    {
1669        use scirs2_core::ndarray::Array1;
1670
1671        // Shape check
1672        if self.shape().dims() != other.shape().dims() {
1673            return Err(TorshError::ShapeMismatch {
1674                expected: self.shape().dims().to_vec(),
1675                got: other.shape().dims().to_vec(),
1676            });
1677        }
1678
1679        // Get data as vectors
1680        let data_a = self.to_vec()?;
1681        let data_b = other.to_vec()?;
1682
1683        // Create ndarray arrays
1684        let arr_a = Array1::from_vec(data_a);
1685        let arr_b = Array1::from_vec(data_b);
1686
1687        // Use REAL SIMD operation (not Rayon!)
1688        let result_arr = T::simd_mul(&arr_a.view(), &arr_b.view());
1689
1690        // Convert back to Tensor
1691        Tensor::from_vec(result_arr.to_vec(), self.shape().dims())
1692    }
1693
1694    /// Dot product with SIMD acceleration (SciRS2)
1695    ///
1696    /// Uses real hardware SIMD instructions (AVX2/NEON) via `scirs2_core::simd_ops::SimdUnifiedOps`
1697    ///
1698    /// Returns a scalar value (sum of element-wise products)
1699    #[cfg(feature = "simd")]
1700    pub fn dot_simd(&self, other: &Self) -> Result<T>
1701    where
1702        T: scirs2_core::simd_ops::SimdUnifiedOps,
1703    {
1704        use scirs2_core::ndarray::Array1;
1705
1706        // Shape check
1707        if self.shape().dims() != other.shape().dims() {
1708            return Err(TorshError::ShapeMismatch {
1709                expected: self.shape().dims().to_vec(),
1710                got: other.shape().dims().to_vec(),
1711            });
1712        }
1713
1714        // Get data as vectors
1715        let data_a = self.to_vec()?;
1716        let data_b = other.to_vec()?;
1717
1718        // Create ndarray arrays
1719        let arr_a = Array1::from_vec(data_a);
1720        let arr_b = Array1::from_vec(data_b);
1721
1722        // Use REAL SIMD dot product (not Rayon!)
1723        Ok(T::simd_dot(&arr_a.view(), &arr_b.view()))
1724    }
1725
1726    /// Memory-efficient reduction using SciRS2 intelligent chunking and lazy evaluation
1727    pub fn reduce_memory_efficient<F>(&self, func: F) -> Result<T>
1728    where
1729        F: Fn(T, T) -> T + Send + Sync,
1730    {
1731        #[cfg(feature = "profiling")]
1732        {
1733            // let _profile = profile_section!("tensor_reduce_memory_efficient");
1734        }
1735
1736        // Use simple reduction for now to get basic functionality working
1737        // Regular reduction for smaller tensors
1738        let data = self.to_vec()?;
1739        Ok(data
1740            .into_iter()
1741            .reduce(func)
1742            .unwrap_or_else(|| <T as scirs2_core::numeric::Zero>::zero()))
1743    }
1744}
1745
1746// ✅ In-place activation functions for PyTorch compatibility
1747impl<T: TensorElement + Copy + std::ops::Mul<Output = T>> Tensor<T> {
1748    /// In-place ReLU activation: self = max(0, self)
1749    ///
1750    /// # PyTorch Compatibility
1751    /// Equivalent to PyTorch's `tensor.relu_()`
1752    ///
1753    /// # Errors
1754    /// - Returns error if `requires_grad` is true
1755    pub fn relu_(&mut self) -> Result<&mut Self>
1756    where
1757        T: std::cmp::PartialOrd + scirs2_core::numeric::Zero,
1758    {
1759        if self.requires_grad {
1760            return Err(TorshError::InvalidArgument(
1761                "In-place operation on tensor that requires grad is not allowed".to_string(),
1762            ));
1763        }
1764
1765        // f32 SIMD fast path with PyTorch NaN-passthrough semantics
1766        if std::any::TypeId::of::<T>() == std::any::TypeId::of::<f32>() && self.numel() >= 1024 {
1767            self.make_unique()?;
1768            self.storage.with_slice_mut(|out_t: &mut [T]| {
1769                // Safety: TypeId confirmed T == f32.
1770                let out_f32: &mut [f32] = unsafe {
1771                    std::slice::from_raw_parts_mut(out_t.as_mut_ptr() as *mut f32, out_t.len())
1772                };
1773                crate::simd_ops_f32::relu_assign_f32(out_f32);
1774                Ok(())
1775            })?;
1776            return Ok(self);
1777        }
1778
1779        // Generic path: one copy-on-write promotion, then a single locked pass
1780        // over the buffer (`apply_`) instead of a lock pair per element.
1781        let zero = <T as scirs2_core::numeric::Zero>::zero();
1782        self.apply_(|x| if x < zero { zero } else { x })?;
1783
1784        Ok(self)
1785    }
1786
1787    /// In-place sigmoid activation: self = 1 / (1 + exp(-self))
1788    ///
1789    /// # PyTorch Compatibility
1790    /// Equivalent to PyTorch's `tensor.sigmoid_()`
1791    pub fn sigmoid_(&mut self) -> Result<&mut Self>
1792    where
1793        T: torsh_core::dtype::FloatElement,
1794    {
1795        if self.requires_grad {
1796            return Err(TorshError::InvalidArgument(
1797                "In-place operation on tensor that requires grad is not allowed".to_string(),
1798            ));
1799        }
1800
1801        // One copy-on-write promotion, then a single locked pass.
1802        let one = <T as scirs2_core::numeric::One>::one();
1803        self.apply_(|x| one / (one + (-x).exp()))?;
1804
1805        Ok(self)
1806    }
1807
1808    /// In-place tanh activation: self = tanh(self)
1809    ///
1810    /// # PyTorch Compatibility
1811    /// Equivalent to PyTorch's `tensor.tanh_()`
1812    pub fn tanh_(&mut self) -> Result<&mut Self>
1813    where
1814        T: torsh_core::dtype::FloatElement,
1815    {
1816        if self.requires_grad {
1817            return Err(TorshError::InvalidArgument(
1818                "In-place operation on tensor that requires grad is not allowed".to_string(),
1819            ));
1820        }
1821
1822        // One copy-on-write promotion, then a single locked pass.
1823        self.apply_(|x| x.tanh())?;
1824
1825        Ok(self)
1826    }
1827
1828    /// In-place GELU activation
1829    ///
1830    /// # PyTorch Compatibility
1831    /// Equivalent to PyTorch's `tensor.gelu_()`
1832    pub fn gelu_(&mut self) -> Result<&mut Self>
1833    where
1834        T: torsh_core::dtype::FloatElement,
1835    {
1836        if self.requires_grad {
1837            return Err(TorshError::InvalidArgument(
1838                "In-place operation on tensor that requires grad is not allowed".to_string(),
1839            ));
1840        }
1841
1842        // Constants of the tanh GELU approximation. `T::from` is fallible for
1843        // exotic element types, so it is reported rather than unwrapped.
1844        let constant = |value: f64| -> Result<T> {
1845            T::from(value).ok_or_else(|| {
1846                TorshError::InvalidArgument(format!(
1847                    "gelu_: element type cannot represent the constant {value}"
1848                ))
1849            })
1850        };
1851        let pi = constant(std::f64::consts::PI)?;
1852        let two = constant(2.0)?;
1853        let sqrt_2_over_pi = (two / pi).sqrt();
1854        let point_044715 = constant(0.044715)?;
1855        let one = <T as scirs2_core::numeric::One>::one();
1856        let half = constant(0.5)?;
1857
1858        // One copy-on-write promotion, then a single locked pass.
1859        self.apply_(|x| {
1860            let x_cubed = x * x * x;
1861            let tanh_input = sqrt_2_over_pi * (x + point_044715 * x_cubed);
1862            half * x * (one + tanh_input.tanh())
1863        })?;
1864
1865        Ok(self)
1866    }
1867
1868    /// In-place leaky ReLU activation
1869    ///
1870    /// # PyTorch Compatibility
1871    /// Equivalent to PyTorch's `tensor.leaky_relu_(negative_slope)`
1872    pub fn leaky_relu_(&mut self, negative_slope: T) -> Result<&mut Self>
1873    where
1874        T: std::cmp::PartialOrd + scirs2_core::numeric::Zero,
1875    {
1876        if self.requires_grad {
1877            return Err(TorshError::InvalidArgument(
1878                "In-place operation on tensor that requires grad is not allowed".to_string(),
1879            ));
1880        }
1881
1882        // f32 SIMD fast path with PyTorch NaN-passthrough semantics
1883        if std::any::TypeId::of::<T>() == std::any::TypeId::of::<f32>() && self.numel() >= 1024 {
1884            // Safety: TypeId confirmed T == f32; copy the scalar via transmute_copy.
1885            let slope_f32: f32 = unsafe { std::mem::transmute_copy::<T, f32>(&negative_slope) };
1886            self.make_unique()?;
1887            self.storage.with_slice_mut(|out_t: &mut [T]| {
1888                let out_f32: &mut [f32] = unsafe {
1889                    std::slice::from_raw_parts_mut(out_t.as_mut_ptr() as *mut f32, out_t.len())
1890                };
1891                crate::simd_ops_f32::leaky_relu_assign_f32(out_f32, slope_f32);
1892                Ok(())
1893            })?;
1894            return Ok(self);
1895        }
1896
1897        // One copy-on-write promotion, then a single locked pass.
1898        let zero = <T as scirs2_core::numeric::Zero>::zero();
1899        self.apply_(|x| if x < zero { negative_slope * x } else { x })?;
1900
1901        Ok(self)
1902    }
1903
1904    /// In-place clamp operation: self = clamp(self, min, max)
1905    ///
1906    /// # PyTorch Compatibility
1907    /// Equivalent to PyTorch's `tensor.clamp_(min, max)`
1908    pub fn clamp_(&mut self, min: T, max: T) -> Result<&mut Self>
1909    where
1910        T: std::cmp::PartialOrd,
1911    {
1912        if self.requires_grad {
1913            return Err(TorshError::InvalidArgument(
1914                "In-place operation on tensor that requires grad is not allowed".to_string(),
1915            ));
1916        }
1917
1918        // f32 SIMD fast path with PyTorch NaN-passthrough semantics
1919        if std::any::TypeId::of::<T>() == std::any::TypeId::of::<f32>() && self.numel() >= 1024 {
1920            // Safety: TypeId confirmed T == f32; copy scalars via transmute_copy.
1921            let min_f32: f32 = unsafe { std::mem::transmute_copy::<T, f32>(&min) };
1922            let max_f32: f32 = unsafe { std::mem::transmute_copy::<T, f32>(&max) };
1923            self.make_unique()?;
1924            self.storage.with_slice_mut(|out_t: &mut [T]| {
1925                let out_f32: &mut [f32] = unsafe {
1926                    std::slice::from_raw_parts_mut(out_t.as_mut_ptr() as *mut f32, out_t.len())
1927                };
1928                crate::simd_ops_f32::clamp_assign_f32(out_f32, min_f32, max_f32);
1929                Ok(())
1930            })?;
1931            return Ok(self);
1932        }
1933
1934        // One copy-on-write promotion, then a single locked pass.
1935        self.apply_(|x| {
1936            if x < min {
1937                min
1938            } else if x > max {
1939                max
1940            } else {
1941                x
1942            }
1943        })?;
1944
1945        Ok(self)
1946    }
1947}
1948
1949#[path = "math_ops_trig.rs"]
1950mod math_ops_trig;
1951
1952#[cfg(test)]
1953#[path = "math_ops_tests.rs"]
1954mod tests;