Skip to main content

tenflowers_core/ops/binary/
implementation.rs

1//! Generic Binary Operation Implementation
2//!
3//! This module provides the core implementation of binary operations with
4//! comprehensive broadcasting support and device management.
5
6use super::core::BinaryOp;
7use crate::shape_error_taxonomy::ShapeErrorUtils;
8use crate::{Device, Result, Tensor, TensorError};
9use scirs2_core::ndarray::{ArrayD, IxDyn};
10use scirs2_core::numeric::Zero;
11
12/// Generic binary operation implementation with broadcasting
13#[allow(unused_variables)]
14pub fn binary_op<T, Op>(a: &Tensor<T>, b: &Tensor<T>, op: Op) -> Result<Tensor<T>>
15where
16    T: Clone + Default + Zero + Send + Sync + 'static + bytemuck::Pod + bytemuck::Zeroable,
17    Op: BinaryOp<T>,
18{
19    // NOTE(v0.2): Implement profiler integration
20    // #[cfg(feature = "autograd")]
21    // if let Ok(mut p) = get_profiler().lock() {
22    //     p.record_operation(&format!("binary_{}", op.name()));
23    // }
24
25    // Check device compatibility
26    if a.device() != b.device() {
27        return Err(TensorError::device_mismatch(
28            "binary_op",
29            &a.device().to_string(),
30            &b.device().to_string(),
31        ));
32    }
33
34    // Compute broadcast shape
35    let broadcast_shape = a.shape().broadcast_shape(b.shape()).ok_or_else(|| {
36        ShapeErrorUtils::broadcast_incompatible("binary_op", a.shape(), b.shape())
37    })?;
38
39    // Handle different device types
40    match (a.device(), b.device()) {
41        (Device::Cpu, Device::Cpu) => {
42            // CPU implementation
43            cpu_binary_op(a, b, op, &broadcast_shape)
44        }
45        #[cfg(feature = "gpu")]
46        (Device::Gpu(_), Device::Gpu(_)) => {
47            // GPU implementation
48            gpu_binary_op(a, b, op, &broadcast_shape)
49        }
50        #[cfg(feature = "rocm")]
51        (Device::Rocm(_), Device::Rocm(_)) => {
52            // ROCm binary operations: Fallback to CPU implementation for now
53            // Future: Implement native ROCm kernels for binary operations
54            eprintln!("Warning: ROCm binary operations using CPU fallback - native implementation pending");
55
56            // Transfer tensors to CPU, perform operation, then return on CPU
57            // (ROCm Device type doesn't have actual storage yet)
58            cpu_binary_op(a, b, op, &broadcast_shape)
59        }
60        #[cfg(any(feature = "gpu", feature = "rocm"))]
61        _ => {
62            // Device mismatch between different device types
63            Err(TensorError::device_mismatch(
64                "binary_op",
65                &a.device().to_string(),
66                &b.device().to_string(),
67            ))
68        }
69    }
70}
71
72/// CPU binary operation implementation with broadcasting
73fn cpu_binary_op<T, Op>(
74    a: &Tensor<T>,
75    b: &Tensor<T>,
76    op: Op,
77    broadcast_shape: &crate::Shape,
78) -> Result<Tensor<T>>
79where
80    T: Clone + Default + Zero + Send + Sync + 'static + bytemuck::Pod + bytemuck::Zeroable,
81    Op: BinaryOp<T>,
82{
83    use crate::tensor::TensorStorage;
84
85    // Get CPU arrays from tensors
86    let (a_array, b_array) = match (&a.storage, &b.storage) {
87        (TensorStorage::Cpu(ref a_arr), TensorStorage::Cpu(ref b_arr)) => (a_arr, b_arr),
88        #[cfg(feature = "gpu")]
89        _ => {
90            return Err(TensorError::device_mismatch(
91                "cpu_binary_op",
92                "cpu",
93                "non-cpu",
94            ))
95        }
96    };
97
98    // Create output array with broadcast shape
99    let output_dims = IxDyn(broadcast_shape.dims());
100    let mut output = ArrayD::zeros(output_dims);
101
102    // Handle broadcasting with full NumPy-style compatibility
103    if a.shape() == b.shape() && a.shape().dims() == broadcast_shape.dims() {
104        // Simple case: same shapes - direct element-wise operation
105        for ((a_val, b_val), out_val) in a_array.iter().zip(b_array.iter()).zip(output.iter_mut()) {
106            *out_val = op.apply(*a_val, *b_val);
107        }
108    } else {
109        // Advanced broadcasting case - handle different but compatible shapes
110        broadcast_operation(
111            a_array,
112            b_array,
113            &mut output,
114            &op,
115            a.shape(),
116            b.shape(),
117            broadcast_shape,
118        )?;
119    }
120
121    // Create result tensor using from_array
122    Ok(Tensor::from_array(output))
123}
124
125/// Advanced broadcasting operation for tensors with different but compatible shapes
126fn broadcast_operation<T, Op>(
127    a_array: &ArrayD<T>,
128    b_array: &ArrayD<T>,
129    output: &mut ArrayD<T>,
130    op: &Op,
131    a_shape: &crate::Shape,
132    b_shape: &crate::Shape,
133    broadcast_shape: &crate::Shape,
134) -> Result<()>
135where
136    T: Clone + Default + Zero + Send + Sync + 'static + bytemuck::Pod + bytemuck::Zeroable,
137    Op: BinaryOp<T>,
138{
139    // Handle simple scalar cases first for performance
140    if a_shape.size() == 1 {
141        // a is scalar, broadcast to b
142        if let Some(a_scalar) = a_array.iter().next() {
143            for (b_val, out_val) in b_array.iter().zip(output.iter_mut()) {
144                *out_val = op.apply(*a_scalar, *b_val);
145            }
146        }
147        return Ok(());
148    }
149
150    if b_shape.size() == 1 {
151        // b is scalar, broadcast to a
152        if let Some(b_scalar) = b_array.iter().next() {
153            for (a_val, out_val) in a_array.iter().zip(output.iter_mut()) {
154                *out_val = op.apply(*a_val, *b_scalar);
155            }
156        }
157        return Ok(());
158    }
159
160    // Full broadcasting for complex shapes
161    // Use coordinate iteration to handle broadcasting properly
162    let output_shape = broadcast_shape.dims();
163    let a_dims = a_shape.dims();
164    let b_dims = b_shape.dims();
165
166    // Calculate strides for each dimension to enable broadcasting
167    let mut a_strides = vec![0; output_shape.len()];
168    let mut b_strides = vec![0; output_shape.len()];
169
170    // Align dimensions from the right (trailing dimensions)
171    let a_offset = output_shape.len() - a_dims.len();
172    let b_offset = output_shape.len() - b_dims.len();
173
174    // Calculate actual strides for a
175    let mut a_stride_acc = 1;
176    for i in (0..a_dims.len()).rev() {
177        let out_idx = a_offset + i;
178        if a_dims[i] == 1 {
179            a_strides[out_idx] = 0; // Broadcasting: stride is 0 for singleton dimensions
180        } else {
181            a_strides[out_idx] = a_stride_acc;
182        }
183        a_stride_acc *= a_dims[i];
184    }
185
186    // Calculate actual strides for b
187    let mut b_stride_acc = 1;
188    for i in (0..b_dims.len()).rev() {
189        let out_idx = b_offset + i;
190        if b_dims[i] == 1 {
191            b_strides[out_idx] = 0; // Broadcasting: stride is 0 for singleton dimensions
192        } else {
193            b_strides[out_idx] = b_stride_acc;
194        }
195        b_stride_acc *= b_dims[i];
196    }
197
198    // Iterate through all output positions and compute indices for a and b
199    let total_elements: usize = output_shape.iter().product();
200    for linear_idx in 0..total_elements {
201        // Convert linear index to multi-dimensional coordinates
202        let mut coords = vec![0; output_shape.len()];
203        let mut remaining = linear_idx;
204        for i in (0..output_shape.len()).rev() {
205            coords[i] = remaining % output_shape[i];
206            remaining /= output_shape[i];
207        }
208
209        // Calculate corresponding indices in a and b using strides
210        let mut a_idx = 0;
211        let mut b_idx = 0;
212        for i in 0..output_shape.len() {
213            a_idx += coords[i] * a_strides[i];
214            b_idx += coords[i] * b_strides[i];
215        }
216
217        // Get values from a and b arrays using linear indexing
218        let a_val = a_array
219            .as_slice()
220            .unwrap_or_else(|| panic!("Failed to get slice from a_array"))[a_idx];
221        let b_val = b_array
222            .as_slice()
223            .unwrap_or_else(|| panic!("Failed to get slice from b_array"))[b_idx];
224
225        // Apply operation and store result
226        let result_val = op.apply(a_val, b_val);
227        output
228            .as_slice_mut()
229            .unwrap_or_else(|| panic!("Failed to get mutable slice from output"))[linear_idx] =
230            result_val;
231    }
232
233    Ok(())
234}
235
236/// GPU binary operation implementation with broadcasting
237#[cfg(feature = "gpu")]
238fn gpu_binary_op<T, Op>(
239    a: &Tensor<T>,
240    b: &Tensor<T>,
241    op: Op,
242    broadcast_shape: &crate::Shape,
243) -> Result<Tensor<T>>
244where
245    T: Clone + Default + Zero + Send + Sync + 'static + bytemuck::Pod + bytemuck::Zeroable,
246    Op: BinaryOp<T>,
247{
248    use crate::gpu::binary_ops;
249    use crate::tensor::TensorStorage;
250
251    // Get GPU buffers from tensors
252    let (a_buffer, b_buffer) = match (&a.storage, &b.storage) {
253        (TensorStorage::Gpu(ref a_buf), TensorStorage::Gpu(ref b_buf)) => (a_buf, b_buf),
254        _ => {
255            return Err(TensorError::device_mismatch(
256                "gpu_binary_op",
257                "gpu",
258                "non-gpu",
259            ))
260        }
261    };
262
263    // Map operation name to GPU operation type
264    let gpu_op = match op.name() {
265        "Add" => binary_ops::BinaryOp::Add,
266        "Sub" => binary_ops::BinaryOp::Sub,
267        "Mul" => binary_ops::BinaryOp::Mul,
268        "Div" => binary_ops::BinaryOp::Div,
269        "Pow" => binary_ops::BinaryOp::Pow,
270        "Min" => binary_ops::BinaryOp::Min,
271        "Max" => binary_ops::BinaryOp::Max,
272        _ => {
273            return Err(TensorError::invalid_argument(format!(
274                "Unsupported GPU binary operation: {}",
275                op.name()
276            )))
277        }
278    };
279
280    // Calculate output size
281    let output_len = broadcast_shape.size();
282
283    // Execute GPU operation with broadcasting if needed
284    let result_buffer = if a.shape() == b.shape() && a.shape().dims() == broadcast_shape.dims() {
285        // Simple case: same shapes - no broadcasting needed
286        binary_ops::execute_binary_op(a_buffer, b_buffer, gpu_op, output_len)?
287    } else {
288        // Broadcasting case
289        binary_ops::execute_binary_op_with_broadcasting(
290            a_buffer,
291            b_buffer,
292            gpu_op,
293            a.shape().dims(),
294            b.shape().dims(),
295            broadcast_shape.dims(),
296            output_len,
297        )?
298    };
299
300    // Create result tensor from GPU buffer
301    Ok(Tensor::from_gpu_buffer(
302        result_buffer,
303        broadcast_shape.clone(),
304    ))
305}