Skip to main content

tenflowers_core/ops/
async_binary.rs

1use crate::ops::binary::{AddOp, BinaryOp, DivOp, MulOp, PReLUOp, PowOp, SubOp};
2#[cfg(feature = "gpu")]
3use crate::ops::hybrid_scheduler::{HybridWorkScheduler, WorkItem, WorkType};
4
5// Re-export WorkPriority for public API
6#[cfg(feature = "gpu")]
7pub use crate::ops::hybrid_scheduler::WorkPriority;
8#[cfg(test)]
9use crate::tensor::TensorStorage;
10/// Async binary operations with CPU-GPU overlap using hybrid scheduler
11use crate::{Device, Result, Tensor, TensorError};
12
13#[cfg(not(feature = "gpu"))]
14#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
15pub enum WorkPriority {
16    Low = 0,
17    Normal = 1,
18    High = 2,
19    Critical = 3,
20}
21
22use crate::device::async_execution::AsyncExecutor;
23use std::future::Future;
24use std::pin::Pin;
25use std::sync::Arc;
26use std::task::{Context, Poll};
27
28#[cfg(feature = "gpu")]
29use crate::gpu::multi_stream_executor::MultiStreamGpuExecutor;
30
31/// Async binary operation future
32pub struct AsyncBinaryOpFuture<T> {
33    inner: Pin<Box<dyn Future<Output = Result<Tensor<T>>> + Send>>,
34}
35
36impl<T> Future for AsyncBinaryOpFuture<T> {
37    type Output = Result<Tensor<T>>;
38
39    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
40        self.inner.as_mut().poll(cx)
41    }
42}
43
44/// Async binary operation executor with CPU-GPU overlap
45pub struct AsyncBinaryOperationExecutor {
46    #[cfg(feature = "gpu")]
47    hybrid_scheduler: Option<Arc<HybridWorkScheduler>>,
48    #[cfg(not(feature = "gpu"))]
49    cpu_executor: Arc<AsyncExecutor>,
50}
51
52impl AsyncBinaryOperationExecutor {
53    /// Create a new async binary operation executor.
54    ///
55    /// When a usable GPU device is available, work is scheduled through the
56    /// CPU-GPU hybrid scheduler. When no GPU device can be created (headless /
57    /// sandboxed environments where `request_device` fails), this degrades
58    /// honestly to a CPU-only executor rather than erroring — `execute_async`
59    /// already runs a real CPU path when no scheduler is present. This mirrors
60    /// the fallback long used by [`global_async_executor`].
61    #[cfg(feature = "gpu")]
62    pub fn new(device_id: usize) -> Result<Self> {
63        match crate::device::context::get_gpu_context(device_id) {
64            Ok(gpu_ctx) => {
65                // Create CPU executor
66                let cpu_executor = Arc::new(AsyncExecutor::new(Device::Cpu));
67
68                // Create GPU executor
69                let gpu_executor = Arc::new(MultiStreamGpuExecutor::new(
70                    gpu_ctx.device.clone(),
71                    gpu_ctx.queue.clone(),
72                ));
73
74                // Create hybrid scheduler
75                let hybrid_scheduler =
76                    Arc::new(HybridWorkScheduler::new(cpu_executor, gpu_executor));
77
78                Ok(Self {
79                    hybrid_scheduler: Some(hybrid_scheduler),
80                })
81            }
82            // No usable GPU device: honest CPU-only fallback.
83            Err(_) => Ok(Self {
84                hybrid_scheduler: None,
85            }),
86        }
87    }
88
89    /// Create a new async binary operation executor (CPU-only)
90    #[cfg(not(feature = "gpu"))]
91    pub fn new(_device_id: usize) -> Result<Self> {
92        let cpu_executor = Arc::new(AsyncExecutor::new(Device::Cpu));
93        Ok(Self { cpu_executor })
94    }
95
96    /// Execute an async binary operation with CPU-GPU overlap
97    pub fn execute_async<T, Op>(
98        &self,
99        a: &Tensor<T>,
100        b: &Tensor<T>,
101        op: Op,
102    ) -> AsyncBinaryOpFuture<T>
103    where
104        T: Clone
105            + Default
106            + scirs2_core::num_traits::Zero
107            + Send
108            + Sync
109            + 'static
110            + bytemuck::Pod
111            + bytemuck::Zeroable,
112        Op: BinaryOp<T> + Send + Sync + 'static,
113    {
114        #[cfg(feature = "gpu")]
115        {
116            if let Some(ref scheduler) = self.hybrid_scheduler {
117                // Create work item for the operation
118                let input_size = a.shape().elements() + b.shape().elements();
119                let dtype = std::any::type_name::<T>();
120
121                let work_item = scheduler.create_binary_op_work(
122                    op.name(),
123                    input_size,
124                    dtype,
125                    crate::ops::hybrid_scheduler::WorkPriority::Normal,
126                );
127
128                // Clone tensors and operation for async execution
129                let a_clone = a.clone();
130                let b_clone = b.clone();
131                let scheduler_clone = Arc::clone(scheduler);
132
133                let future = async move {
134                    // Submit work to hybrid scheduler
135                    let work_future = scheduler_clone.submit_work(work_item);
136
137                    // Execute the actual operation concurrently
138                    let result = std::thread::spawn(move || {
139                        crate::ops::binary::binary_op(&a_clone, &b_clone, op)
140                    })
141                    .join();
142
143                    // Wait for scheduler to complete
144                    work_future.await?;
145
146                    // Return the result
147                    result.map_err(|e| {
148                        TensorError::compute_error_simple(format!(
149                            "Async execution failed: {:?}",
150                            e
151                        ))
152                    })?
153                };
154
155                return AsyncBinaryOpFuture {
156                    inner: Box::pin(future),
157                };
158            }
159        }
160
161        // Fallback to CPU-only execution
162        let a_clone = a.clone();
163        let b_clone = b.clone();
164        let future = async move {
165            std::thread::spawn(move || crate::ops::binary::binary_op(&a_clone, &b_clone, op))
166                .join()
167                .map_err(|e| {
168                    TensorError::compute_error_simple(format!("Async execution failed: {e:?}"))
169                })?
170        };
171
172        AsyncBinaryOpFuture {
173            inner: Box::pin(future),
174        }
175    }
176
177    /// Execute an async binary operation with custom priority
178    pub fn execute_async_with_priority<T, Op>(
179        &self,
180        a: &Tensor<T>,
181        b: &Tensor<T>,
182        op: Op,
183        _priority: WorkPriority,
184    ) -> AsyncBinaryOpFuture<T>
185    where
186        T: Clone
187            + Default
188            + scirs2_core::num_traits::Zero
189            + Send
190            + Sync
191            + 'static
192            + bytemuck::Pod
193            + bytemuck::Zeroable,
194        Op: BinaryOp<T> + Send + Sync + 'static,
195    {
196        // Create work item with custom priority
197        let _input_size = a.shape().elements() + b.shape().elements();
198        let _dtype = std::any::type_name::<T>();
199
200        #[cfg(feature = "gpu")]
201        {
202            if let Some(ref scheduler) = self.hybrid_scheduler {
203                let work_item =
204                    scheduler.create_binary_op_work(op.name(), _input_size, _dtype, _priority);
205
206                // Clone tensors and operation for async execution
207                let a_clone = a.clone();
208                let b_clone = b.clone();
209                let scheduler_clone = Arc::clone(scheduler);
210
211                let future = async move {
212                    // Submit work to hybrid scheduler with custom priority
213                    let work_future = scheduler_clone.submit_work(work_item);
214
215                    // Execute the actual operation concurrently
216                    let result = std::thread::spawn(move || {
217                        crate::ops::binary::binary_op(&a_clone, &b_clone, op)
218                    })
219                    .join();
220
221                    // Wait for scheduler to complete
222                    work_future.await?;
223
224                    // Return the result
225                    result.map_err(|e| {
226                        TensorError::compute_error_simple(format!(
227                            "Async execution failed: {:?}",
228                            e
229                        ))
230                    })?
231                };
232
233                return AsyncBinaryOpFuture {
234                    inner: Box::pin(future),
235                };
236            }
237        }
238
239        // Fallback to CPU-only execution
240        let a_clone = a.clone();
241        let b_clone = b.clone();
242        let future = async move {
243            std::thread::spawn(move || crate::ops::binary::binary_op(&a_clone, &b_clone, op))
244                .join()
245                .map_err(|e| {
246                    TensorError::compute_error_simple(format!("Async execution failed: {e:?}"))
247                })?
248        };
249
250        AsyncBinaryOpFuture {
251            inner: Box::pin(future),
252        }
253    }
254
255    /// Execute multiple binary operations concurrently with CPU-GPU overlap
256    pub async fn execute_batch_async<T, Op>(
257        &self,
258        operations: Vec<(&Tensor<T>, &Tensor<T>, Op)>,
259    ) -> Result<Vec<Tensor<T>>>
260    where
261        T: Clone
262            + Default
263            + scirs2_core::num_traits::Zero
264            + Send
265            + Sync
266            + 'static
267            + bytemuck::Pod
268            + bytemuck::Zeroable,
269        Op: BinaryOp<T> + Send + Sync + Clone + 'static,
270    {
271        let mut futures = Vec::new();
272
273        for (a, b, op) in operations {
274            let future = self.execute_async(a, b, op);
275            futures.push(future);
276        }
277
278        // Execute all operations concurrently
279        let mut results = Vec::new();
280        for future in futures {
281            results.push(future.await?);
282        }
283
284        Ok(results)
285    }
286
287    /// Synchronize all pending operations
288    pub fn synchronize(&self) {
289        #[cfg(feature = "gpu")]
290        {
291            if let Some(ref scheduler) = self.hybrid_scheduler {
292                scheduler.synchronize_all();
293            }
294        }
295    }
296
297    /// Check if executor is idle
298    pub fn is_idle(&self) -> bool {
299        #[cfg(feature = "gpu")]
300        {
301            if let Some(ref scheduler) = self.hybrid_scheduler {
302                return scheduler.is_idle();
303            }
304        }
305
306        #[cfg(not(feature = "gpu"))]
307        {
308            self.cpu_executor.queue_length() == 0
309        }
310
311        #[cfg(feature = "gpu")]
312        true
313    }
314}
315
316/// Global async binary operation executor
317static GLOBAL_ASYNC_EXECUTOR: std::sync::OnceLock<AsyncBinaryOperationExecutor> =
318    std::sync::OnceLock::new();
319
320/// Get the global async binary operation executor
321pub fn global_async_executor() -> &'static AsyncBinaryOperationExecutor {
322    GLOBAL_ASYNC_EXECUTOR.get_or_init(|| {
323        AsyncBinaryOperationExecutor::new(0).unwrap_or_else(|_| {
324            // Fallback to CPU-only if GPU initialization fails
325            #[cfg(not(feature = "gpu"))]
326            {
327                AsyncBinaryOperationExecutor::new(0)
328                    .expect("CPU-only async binary executor initialization must succeed")
329            }
330            #[cfg(feature = "gpu")]
331            {
332                // Create a fallback CPU-only executor
333                AsyncBinaryOperationExecutor {
334                    hybrid_scheduler: None,
335                }
336            }
337        })
338    })
339}
340
341/// Async binary operation functions using the global executor
342/// Async add operation
343pub async fn add_async<T>(a: &Tensor<T>, b: &Tensor<T>) -> Result<Tensor<T>>
344where
345    T: Clone
346        + Default
347        + scirs2_core::num_traits::Zero
348        + std::ops::Add<Output = T>
349        + Send
350        + Sync
351        + 'static
352        + bytemuck::Pod
353        + bytemuck::Zeroable,
354{
355    global_async_executor().execute_async(a, b, AddOp).await
356}
357
358/// Async sub operation
359pub async fn sub_async<T>(a: &Tensor<T>, b: &Tensor<T>) -> Result<Tensor<T>>
360where
361    T: Clone
362        + Default
363        + scirs2_core::num_traits::Zero
364        + std::ops::Sub<Output = T>
365        + Send
366        + Sync
367        + 'static
368        + bytemuck::Pod
369        + bytemuck::Zeroable,
370{
371    global_async_executor().execute_async(a, b, SubOp).await
372}
373
374/// Async mul operation
375pub async fn mul_async<T>(a: &Tensor<T>, b: &Tensor<T>) -> Result<Tensor<T>>
376where
377    T: Clone
378        + Default
379        + scirs2_core::num_traits::Zero
380        + std::ops::Mul<Output = T>
381        + Send
382        + Sync
383        + 'static
384        + bytemuck::Pod
385        + bytemuck::Zeroable,
386{
387    global_async_executor().execute_async(a, b, MulOp).await
388}
389
390/// Async div operation
391pub async fn div_async<T>(a: &Tensor<T>, b: &Tensor<T>) -> Result<Tensor<T>>
392where
393    T: Clone
394        + Default
395        + scirs2_core::num_traits::Zero
396        + std::ops::Div<Output = T>
397        + Send
398        + Sync
399        + 'static
400        + bytemuck::Pod
401        + bytemuck::Zeroable,
402{
403    global_async_executor().execute_async(a, b, DivOp).await
404}
405
406/// Async pow operation
407pub async fn pow_async<T>(a: &Tensor<T>, b: &Tensor<T>) -> Result<Tensor<T>>
408where
409    T: Clone
410        + Default
411        + scirs2_core::num_traits::Zero
412        + scirs2_core::num_traits::Float
413        + Send
414        + Sync
415        + 'static
416        + bytemuck::Pod
417        + bytemuck::Zeroable,
418{
419    global_async_executor().execute_async(a, b, PowOp).await
420}
421
422/// Async PReLU operation
423pub async fn prelu_async<T>(a: &Tensor<T>, b: &Tensor<T>) -> Result<Tensor<T>>
424where
425    T: Clone
426        + Default
427        + scirs2_core::num_traits::Zero
428        + scirs2_core::num_traits::Float
429        + PartialOrd
430        + std::ops::Mul<Output = T>
431        + Send
432        + Sync
433        + 'static
434        + bytemuck::Pod
435        + bytemuck::Zeroable,
436{
437    global_async_executor().execute_async(a, b, PReLUOp).await
438}
439
440/// Async add operation with priority
441pub async fn add_async_priority<T>(
442    a: &Tensor<T>,
443    b: &Tensor<T>,
444    priority: WorkPriority,
445) -> Result<Tensor<T>>
446where
447    T: Clone
448        + Default
449        + scirs2_core::num_traits::Zero
450        + std::ops::Add<Output = T>
451        + Send
452        + Sync
453        + 'static
454        + bytemuck::Pod
455        + bytemuck::Zeroable,
456{
457    global_async_executor()
458        .execute_async_with_priority(a, b, AddOp, priority)
459        .await
460}
461
462/// Async mul operation with priority
463pub async fn mul_async_priority<T>(
464    a: &Tensor<T>,
465    b: &Tensor<T>,
466    priority: WorkPriority,
467) -> Result<Tensor<T>>
468where
469    T: Clone
470        + Default
471        + scirs2_core::num_traits::Zero
472        + std::ops::Mul<Output = T>
473        + Send
474        + Sync
475        + 'static
476        + bytemuck::Pod
477        + bytemuck::Zeroable,
478{
479    global_async_executor()
480        .execute_async_with_priority(a, b, MulOp, priority)
481        .await
482}
483
484/// Batch processing for multiple operations
485pub async fn batch_add_async<T>(operations: Vec<(&Tensor<T>, &Tensor<T>)>) -> Result<Vec<Tensor<T>>>
486where
487    T: Clone
488        + Default
489        + scirs2_core::num_traits::Zero
490        + std::ops::Add<Output = T>
491        + Send
492        + Sync
493        + 'static
494        + bytemuck::Pod
495        + bytemuck::Zeroable,
496{
497    let ops: Vec<_> = operations.into_iter().map(|(a, b)| (a, b, AddOp)).collect();
498    global_async_executor().execute_batch_async(ops).await
499}
500
501/// Batch processing for multiple multiplication operations
502pub async fn batch_mul_async<T>(operations: Vec<(&Tensor<T>, &Tensor<T>)>) -> Result<Vec<Tensor<T>>>
503where
504    T: Clone
505        + Default
506        + scirs2_core::num_traits::Zero
507        + std::ops::Mul<Output = T>
508        + Send
509        + Sync
510        + 'static
511        + bytemuck::Pod
512        + bytemuck::Zeroable,
513{
514    let ops: Vec<_> = operations.into_iter().map(|(a, b)| (a, b, MulOp)).collect();
515    global_async_executor().execute_batch_async(ops).await
516}
517
518/// Synchronize all async operations
519pub fn synchronize_async_operations() {
520    global_async_executor().synchronize();
521}
522
523/// Check if async operations are idle
524pub fn is_async_operations_idle() -> bool {
525    global_async_executor().is_idle()
526}
527
528#[cfg(test)]
529#[allow(irrefutable_let_patterns)] // Pattern matching on TensorStorage is irrefutable when GPU feature is disabled
530mod tests {
531    use super::*;
532    use crate::Tensor;
533
534    #[test]
535    fn test_async_executor_creation() {
536        let executor = AsyncBinaryOperationExecutor::new(0).expect("test: new should succeed");
537
538        // Test that executor starts idle
539        assert!(executor.is_idle());
540    }
541
542    #[test]
543    fn test_sync_fallback() {
544        let a = Tensor::<f32>::from_vec(vec![1.0, 2.0, 3.0], &[3])
545            .expect("test: from_vec should succeed");
546        let b = Tensor::<f32>::from_vec(vec![4.0, 5.0, 6.0], &[3])
547            .expect("test: from_vec should succeed");
548
549        // Test that sync operations still work
550        let result = crate::ops::add(&a, &b).expect("test: add should succeed");
551        let expected = vec![5.0, 7.0, 9.0];
552
553        if let TensorStorage::Cpu(arr) = &result.storage {
554            assert_eq!(
555                arr.as_slice().expect("tensor should be contiguous"),
556                &expected
557            );
558        }
559    }
560
561    #[test]
562    fn test_work_priority_ordering() {
563        let high = self::WorkPriority::High;
564        let normal = self::WorkPriority::Normal;
565        let low = self::WorkPriority::Low;
566
567        assert!(high > normal);
568        assert!(normal > low);
569    }
570}