Skip to main content

torsh_tensor/
auto_batching.rs

1//! Automatic Operation Batching for Performance Optimization
2//!
3//! This module provides automatic batching of tensor operations to improve throughput
4//! and reduce overhead. Small operations can be automatically grouped together for
5//! better hardware utilization and reduced synchronization costs.
6//!
7//! # Features
8//!
9//! - **Automatic batching**: Transparently groups small operations
10//! - **Adaptive sizing**: Automatically determines optimal batch sizes
11//! - **Parallel execution**: Batches execute in parallel when possible
12//! - **Low overhead**: Minimal runtime cost for batching logic
13//! - **Configurable thresholds**: Customize when batching occurs
14
15use std::sync::{Arc, Mutex};
16use std::time::{Duration, Instant};
17use torsh_core::sync::MutexExt;
18
19use scirs2_core::parallel_ops::*; // SciRS2 parallel operations
20use torsh_core::{device::DeviceType, dtype::TensorElement, error::Result};
21
22use crate::Tensor;
23
24/// Configuration for automatic batching
25#[derive(Debug, Clone)]
26pub struct BatchingConfig {
27    /// Minimum number of operations to form a batch
28    pub min_batch_size: usize,
29    /// Maximum number of operations in a batch
30    pub max_batch_size: usize,
31    /// Maximum time to wait for more operations before executing batch
32    pub max_wait_time: Duration,
33    /// Whether to enable parallel execution within batches
34    pub parallel_execution: bool,
35    /// Size threshold below which operations are batched (in elements)
36    pub small_op_threshold: usize,
37    /// Whether batching is enabled
38    pub enabled: bool,
39}
40
41impl Default for BatchingConfig {
42    fn default() -> Self {
43        Self {
44            min_batch_size: 4,
45            max_batch_size: 32,
46            max_wait_time: Duration::from_micros(100),
47            parallel_execution: true,
48            small_op_threshold: 1000,
49            enabled: true,
50        }
51    }
52}
53
54impl BatchingConfig {
55    /// Create a configuration optimized for small operations
56    pub fn small_ops() -> Self {
57        Self {
58            min_batch_size: 8,
59            max_batch_size: 64,
60            max_wait_time: Duration::from_micros(50),
61            parallel_execution: true,
62            small_op_threshold: 500,
63            enabled: true,
64        }
65    }
66
67    /// Create a configuration for large operations (minimal batching)
68    pub fn large_ops() -> Self {
69        Self {
70            min_batch_size: 2,
71            max_batch_size: 8,
72            max_wait_time: Duration::from_micros(20),
73            parallel_execution: false,
74            small_op_threshold: 10000,
75            enabled: false, // Disabled for large ops
76        }
77    }
78
79    /// Disable batching completely
80    pub fn disabled() -> Self {
81        Self {
82            enabled: false,
83            ..Default::default()
84        }
85    }
86}
87
88/// Type of tensor operation that can be batched
89#[derive(Debug, Clone)]
90pub enum BatchableOp<T: TensorElement> {
91    /// Element-wise addition
92    Add(Arc<Tensor<T>>, Arc<Tensor<T>>),
93    /// Element-wise multiplication
94    Mul(Arc<Tensor<T>>, Arc<Tensor<T>>),
95    /// Element-wise subtraction
96    Sub(Arc<Tensor<T>>, Arc<Tensor<T>>),
97    /// Element-wise division
98    Div(Arc<Tensor<T>>, Arc<Tensor<T>>),
99    /// Scalar addition
100    AddScalar(Arc<Tensor<T>>, T),
101    /// Scalar multiplication
102    MulScalar(Arc<Tensor<T>>, T),
103    /// ReLU activation
104    ReLU(Arc<Tensor<T>>),
105    /// Sigmoid activation
106    Sigmoid(Arc<Tensor<T>>),
107    /// Tanh activation
108    Tanh(Arc<Tensor<T>>),
109}
110
111impl<T: TensorElement> BatchableOp<T> {
112    /// Get the estimated size (in elements) of the operation
113    pub fn size(&self) -> usize {
114        match self {
115            BatchableOp::Add(a, _)
116            | BatchableOp::Mul(a, _)
117            | BatchableOp::Sub(a, _)
118            | BatchableOp::Div(a, _)
119            | BatchableOp::AddScalar(a, _)
120            | BatchableOp::MulScalar(a, _)
121            | BatchableOp::ReLU(a)
122            | BatchableOp::Sigmoid(a)
123            | BatchableOp::Tanh(a) => a.numel(),
124        }
125    }
126
127    /// Get the device type of the operation
128    pub fn device(&self) -> DeviceType {
129        match self {
130            BatchableOp::Add(a, _)
131            | BatchableOp::Mul(a, _)
132            | BatchableOp::Sub(a, _)
133            | BatchableOp::Div(a, _)
134            | BatchableOp::AddScalar(a, _)
135            | BatchableOp::MulScalar(a, _)
136            | BatchableOp::ReLU(a)
137            | BatchableOp::Sigmoid(a)
138            | BatchableOp::Tanh(a) => a.device,
139        }
140    }
141
142    /// Check if this operation should be batched based on config
143    pub fn should_batch(&self, config: &BatchingConfig) -> bool {
144        config.enabled && self.size() < config.small_op_threshold
145    }
146}
147
148/// A batch of operations ready for execution
149struct OperationBatch<T: TensorElement> {
150    /// Operations in this batch
151    operations: Vec<BatchableOp<T>>,
152    /// When this batch was created
153    created_at: Instant,
154    /// Device type for all operations in batch
155    device: DeviceType,
156}
157
158impl<T: TensorElement> OperationBatch<T> {
159    /// Create a new empty batch
160    fn new(device: DeviceType) -> Self {
161        Self {
162            operations: Vec::new(),
163            created_at: Instant::now(),
164            device,
165        }
166    }
167
168    /// Add an operation to the batch
169    fn add(&mut self, op: BatchableOp<T>) {
170        self.operations.push(op);
171    }
172
173    /// Check if the batch is ready to execute based on config
174    fn is_ready(&self, config: &BatchingConfig) -> bool {
175        if self.operations.len() >= config.max_batch_size {
176            return true;
177        }
178
179        if self.operations.len() >= config.min_batch_size {
180            let elapsed = self.created_at.elapsed();
181            if elapsed >= config.max_wait_time {
182                return true;
183            }
184        }
185
186        false
187    }
188
189    /// Check if the batch can accept more operations
190    fn can_add(&self, config: &BatchingConfig) -> bool {
191        self.operations.len() < config.max_batch_size
192    }
193
194    /// Get the number of operations in the batch
195    fn len(&self) -> usize {
196        self.operations.len()
197    }
198
199    /// Check if the batch is empty
200    fn is_empty(&self) -> bool {
201        self.operations.is_empty()
202    }
203}
204
205/// Automatic operation batcher
206pub struct AutoBatcher<T: TensorElement> {
207    /// Current batch being assembled
208    current_batch: Arc<Mutex<Option<OperationBatch<T>>>>,
209    /// Configuration
210    config: BatchingConfig,
211    /// Statistics
212    stats: Arc<Mutex<BatchingStats>>,
213}
214
215impl<
216        T: TensorElement
217            + Copy
218            + std::ops::Add<Output = T>
219            + std::ops::Sub<Output = T>
220            + std::ops::Mul<Output = T>
221            + std::ops::Div<Output = T>
222            + torsh_core::FloatElement
223            + Send
224            + Sync,
225    > AutoBatcher<T>
226{
227    /// Create a new auto-batcher with default configuration
228    pub fn new() -> Self {
229        Self::with_config(BatchingConfig::default())
230    }
231
232    /// Create a new auto-batcher with custom configuration
233    pub fn with_config(config: BatchingConfig) -> Self {
234        Self {
235            current_batch: Arc::new(Mutex::new(None)),
236            config,
237            stats: Arc::new(Mutex::new(BatchingStats::default())),
238        }
239    }
240
241    /// Submit an operation for batching
242    pub fn submit(&self, op: BatchableOp<T>) -> Result<BatchHandle<T>> {
243        if !self.config.enabled || !op.should_batch(&self.config) {
244            // Execute immediately if batching is disabled or operation is too large
245            return Ok(BatchHandle::Immediate(self.execute_single(op)?));
246        }
247
248        let mut batch_lock = self.current_batch.lock_or_recover();
249
250        // Get or create current batch
251        let batch = batch_lock.get_or_insert_with(|| OperationBatch::new(op.device()));
252
253        // Check if we can add to current batch
254        if !batch.can_add(&self.config) || batch.device != op.device() {
255            // Execute current batch and create a new one
256            let ready_batch = batch_lock
257                .take()
258                .expect("batch should exist after get_or_insert_with");
259            drop(batch_lock);
260
261            self.execute_batch(ready_batch)?;
262
263            let mut new_batch_lock = self.current_batch.lock_or_recover();
264            let new_batch = new_batch_lock.get_or_insert_with(|| OperationBatch::new(op.device()));
265            new_batch.add(op);
266        } else {
267            batch.add(op);
268
269            // Check if batch is ready to execute
270            if batch.is_ready(&self.config) {
271                let ready_batch = batch_lock
272                    .take()
273                    .expect("batch should exist after is_ready check");
274                drop(batch_lock);
275                self.execute_batch(ready_batch)?;
276            }
277        }
278
279        Ok(BatchHandle::Batched)
280    }
281
282    /// Force execution of any pending batch
283    pub fn flush(&self) -> Result<()> {
284        let batch = self.current_batch.lock_or_recover().take();
285
286        if let Some(batch) = batch {
287            if !batch.is_empty() {
288                self.execute_batch(batch)?;
289            }
290        }
291
292        Ok(())
293    }
294
295    /// Execute a single operation immediately
296    fn execute_single(&self, op: BatchableOp<T>) -> Result<Tensor<T>>
297    where
298        T: std::ops::Add<Output = T>
299            + std::ops::Sub<Output = T>
300            + std::ops::Mul<Output = T>
301            + std::ops::Div<Output = T>
302            + torsh_core::FloatElement,
303    {
304        let mut stats = self.stats.lock_or_recover();
305        stats.single_ops_executed += 1;
306        drop(stats);
307
308        match op {
309            BatchableOp::Add(a, b) => a.add_op(&b),
310            BatchableOp::Mul(a, b) => a.mul_op(&b),
311            BatchableOp::Sub(a, b) => a.sub(&b),
312            BatchableOp::Div(a, b) => a.div(&b),
313            BatchableOp::AddScalar(a, s) => a.add_scalar(s),
314            BatchableOp::MulScalar(a, s) => a.mul_scalar(s),
315            BatchableOp::ReLU(a) => a.relu(),
316            BatchableOp::Sigmoid(a) => a.sigmoid(),
317            BatchableOp::Tanh(a) => a.tanh(),
318        }
319    }
320
321    /// Execute a batch of operations
322    fn execute_batch(&self, batch: OperationBatch<T>) -> Result<()>
323    where
324        T: std::ops::Add<Output = T>
325            + std::ops::Sub<Output = T>
326            + std::ops::Mul<Output = T>
327            + std::ops::Div<Output = T>
328            + torsh_core::FloatElement
329            + Send
330            + Sync,
331    {
332        let batch_size = batch.len();
333
334        let mut stats = self.stats.lock_or_recover();
335        stats.batches_executed += 1;
336        stats.total_ops_batched += batch_size;
337        stats.avg_batch_size = (stats.avg_batch_size * (stats.batches_executed - 1) as f64
338            + batch_size as f64)
339            / stats.batches_executed as f64;
340        drop(stats);
341
342        if self.config.parallel_execution && batch_size > 1 {
343            // Execute operations in parallel using scirs2 parallel ops
344            let results: Vec<Result<()>> = batch
345                .operations
346                .into_par_iter()
347                .map(|op| {
348                    self.execute_single(op)?;
349                    Ok(())
350                })
351                .collect();
352
353            // Check for errors
354            for result in results {
355                result?;
356            }
357        } else {
358            // Sequential execution
359            for op in batch.operations {
360                self.execute_single(op)?;
361            }
362        }
363
364        Ok(())
365    }
366
367    /// Get batching statistics
368    pub fn stats(&self) -> BatchingStats {
369        self.stats.lock_or_recover().clone()
370    }
371
372    /// Reset statistics
373    pub fn reset_stats(&self) {
374        *self.stats.lock_or_recover() = BatchingStats::default();
375    }
376}
377
378impl<
379        T: TensorElement
380            + Copy
381            + std::ops::Add<Output = T>
382            + std::ops::Sub<Output = T>
383            + std::ops::Mul<Output = T>
384            + std::ops::Div<Output = T>
385            + torsh_core::FloatElement
386            + Send
387            + Sync,
388    > Default for AutoBatcher<T>
389{
390    fn default() -> Self {
391        Self::new()
392    }
393}
394
395/// Handle returned when submitting an operation
396pub enum BatchHandle<T: TensorElement> {
397    /// Operation was executed immediately
398    Immediate(Tensor<T>),
399    /// Operation was added to a batch
400    Batched,
401}
402
403/// Statistics about batching performance
404#[derive(Debug, Clone)]
405pub struct BatchingStats {
406    /// Number of batches executed
407    pub batches_executed: usize,
408    /// Total operations batched
409    pub total_ops_batched: usize,
410    /// Average batch size
411    pub avg_batch_size: f64,
412    /// Number of single operations executed (not batched)
413    pub single_ops_executed: usize,
414}
415
416impl Default for BatchingStats {
417    fn default() -> Self {
418        Self {
419            batches_executed: 0,
420            total_ops_batched: 0,
421            avg_batch_size: 0.0,
422            single_ops_executed: 0,
423        }
424    }
425}
426
427impl BatchingStats {
428    /// Calculate batching efficiency (percentage of operations that were batched)
429    pub fn batching_efficiency(&self) -> f64 {
430        let total_ops = self.total_ops_batched + self.single_ops_executed;
431        if total_ops == 0 {
432            0.0
433        } else {
434            (self.total_ops_batched as f64 / total_ops as f64) * 100.0
435        }
436    }
437
438    /// Calculate average operations saved by batching
439    pub fn ops_saved(&self) -> f64 {
440        if self.batches_executed == 0 {
441            0.0
442        } else {
443            self.total_ops_batched as f64 - self.batches_executed as f64
444        }
445    }
446}
447
448#[cfg(test)]
449mod tests {
450    use super::*;
451    use crate::creation::*;
452
453    #[test]
454    fn test_batching_config_presets() {
455        let default_config = BatchingConfig::default();
456        assert!(default_config.enabled);
457        assert_eq!(default_config.min_batch_size, 4);
458
459        let small_ops = BatchingConfig::small_ops();
460        assert_eq!(small_ops.min_batch_size, 8);
461        assert_eq!(small_ops.max_batch_size, 64);
462
463        let large_ops = BatchingConfig::large_ops();
464        assert!(!large_ops.enabled);
465
466        let disabled = BatchingConfig::disabled();
467        assert!(!disabled.enabled);
468    }
469
470    #[test]
471    fn test_batchable_op_size() {
472        let a = tensor_1d(&[1.0f32, 2.0, 3.0, 4.0]).expect("tensor_1d creation should succeed");
473        let b = tensor_1d(&[2.0f32, 2.0, 2.0, 2.0]).expect("tensor_1d creation should succeed");
474
475        let op = BatchableOp::Add(Arc::new(a), Arc::new(b));
476        assert_eq!(op.size(), 4);
477    }
478
479    #[test]
480    fn test_batchable_op_should_batch() {
481        let a = tensor_1d(&[1.0f32; 100]).expect("tensor_1d creation should succeed");
482        let b = tensor_1d(&[2.0f32; 100]).expect("tensor_1d creation should succeed");
483
484        let op = BatchableOp::Add(Arc::new(a), Arc::new(b));
485
486        let config = BatchingConfig::default();
487        assert!(op.should_batch(&config));
488
489        let disabled_config = BatchingConfig::disabled();
490        assert!(!op.should_batch(&disabled_config));
491    }
492
493    #[test]
494    fn test_operation_batch() {
495        let a = tensor_1d(&[1.0f32, 2.0]).expect("tensor_1d creation should succeed");
496        let op = BatchableOp::AddScalar(Arc::new(a), 1.0);
497
498        let mut batch = OperationBatch::new(DeviceType::Cpu);
499        assert!(batch.is_empty());
500
501        batch.add(op);
502        assert!(!batch.is_empty());
503        assert_eq!(batch.len(), 1);
504    }
505
506    #[test]
507    fn test_batch_readiness() {
508        let config = BatchingConfig {
509            min_batch_size: 2,
510            max_batch_size: 5,
511            max_wait_time: Duration::from_millis(10),
512            ..Default::default()
513        };
514
515        let mut batch = OperationBatch::<f32>::new(DeviceType::Cpu);
516
517        // Empty batch is not ready
518        assert!(!batch.is_ready(&config));
519
520        // Single operation, not ready yet
521        let a = tensor_1d(&[1.0f32]).expect("tensor_1d creation should succeed");
522        batch.add(BatchableOp::AddScalar(Arc::new(a), 1.0));
523        assert!(!batch.is_ready(&config));
524
525        // Two operations, but wait time not elapsed
526        let b = tensor_1d(&[2.0f32]).expect("tensor_1d creation should succeed");
527        batch.add(BatchableOp::AddScalar(Arc::new(b), 1.0));
528
529        // Max batch size reached
530        for _ in 0..3 {
531            let c = tensor_1d(&[3.0f32]).expect("tensor_1d creation should succeed");
532            batch.add(BatchableOp::AddScalar(Arc::new(c), 1.0));
533        }
534        assert!(batch.is_ready(&config)); // Max size reached
535    }
536
537    #[test]
538    fn test_batching_stats() {
539        let mut stats = BatchingStats::default();
540
541        stats.batches_executed = 10;
542        stats.total_ops_batched = 50;
543        stats.single_ops_executed = 10;
544
545        let efficiency = stats.batching_efficiency();
546        assert!((efficiency - 83.33).abs() < 0.1); // ~83.33%
547
548        let ops_saved = stats.ops_saved();
549        assert_eq!(ops_saved, 40.0); // 50 - 10 = 40 operations saved
550    }
551
552    #[test]
553    fn test_auto_batcher_creation() {
554        let batcher = AutoBatcher::<f32>::new();
555        let stats = batcher.stats();
556
557        assert_eq!(stats.batches_executed, 0);
558        assert_eq!(stats.total_ops_batched, 0);
559        assert_eq!(stats.single_ops_executed, 0);
560    }
561
562    #[test]
563    fn test_auto_batcher_disabled() {
564        let config = BatchingConfig::disabled();
565        let batcher = AutoBatcher::<f32>::with_config(config);
566
567        let a = tensor_1d(&[1.0f32, 2.0]).expect("tensor_1d creation should succeed");
568        let op = BatchableOp::AddScalar(Arc::new(a), 1.0);
569
570        let handle = batcher.submit(op).expect("submit should succeed");
571
572        // Should execute immediately when disabled
573        assert!(matches!(handle, BatchHandle::Immediate(_)));
574
575        let stats = batcher.stats();
576        assert_eq!(stats.single_ops_executed, 1);
577        assert_eq!(stats.total_ops_batched, 0);
578    }
579}