1use std::sync::{Arc, Mutex};
16use std::time::{Duration, Instant};
17use torsh_core::sync::MutexExt;
18
19use scirs2_core::parallel_ops::*; use torsh_core::{device::DeviceType, dtype::TensorElement, error::Result};
21
22use crate::Tensor;
23
24#[derive(Debug, Clone)]
26pub struct BatchingConfig {
27 pub min_batch_size: usize,
29 pub max_batch_size: usize,
31 pub max_wait_time: Duration,
33 pub parallel_execution: bool,
35 pub small_op_threshold: usize,
37 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 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 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, }
77 }
78
79 pub fn disabled() -> Self {
81 Self {
82 enabled: false,
83 ..Default::default()
84 }
85 }
86}
87
88#[derive(Debug, Clone)]
90pub enum BatchableOp<T: TensorElement> {
91 Add(Arc<Tensor<T>>, Arc<Tensor<T>>),
93 Mul(Arc<Tensor<T>>, Arc<Tensor<T>>),
95 Sub(Arc<Tensor<T>>, Arc<Tensor<T>>),
97 Div(Arc<Tensor<T>>, Arc<Tensor<T>>),
99 AddScalar(Arc<Tensor<T>>, T),
101 MulScalar(Arc<Tensor<T>>, T),
103 ReLU(Arc<Tensor<T>>),
105 Sigmoid(Arc<Tensor<T>>),
107 Tanh(Arc<Tensor<T>>),
109}
110
111impl<T: TensorElement> BatchableOp<T> {
112 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 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 pub fn should_batch(&self, config: &BatchingConfig) -> bool {
144 config.enabled && self.size() < config.small_op_threshold
145 }
146}
147
148struct OperationBatch<T: TensorElement> {
150 operations: Vec<BatchableOp<T>>,
152 created_at: Instant,
154 device: DeviceType,
156}
157
158impl<T: TensorElement> OperationBatch<T> {
159 fn new(device: DeviceType) -> Self {
161 Self {
162 operations: Vec::new(),
163 created_at: Instant::now(),
164 device,
165 }
166 }
167
168 fn add(&mut self, op: BatchableOp<T>) {
170 self.operations.push(op);
171 }
172
173 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 fn can_add(&self, config: &BatchingConfig) -> bool {
191 self.operations.len() < config.max_batch_size
192 }
193
194 fn len(&self) -> usize {
196 self.operations.len()
197 }
198
199 fn is_empty(&self) -> bool {
201 self.operations.is_empty()
202 }
203}
204
205pub struct AutoBatcher<T: TensorElement> {
207 current_batch: Arc<Mutex<Option<OperationBatch<T>>>>,
209 config: BatchingConfig,
211 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 pub fn new() -> Self {
229 Self::with_config(BatchingConfig::default())
230 }
231
232 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 pub fn submit(&self, op: BatchableOp<T>) -> Result<BatchHandle<T>> {
243 if !self.config.enabled || !op.should_batch(&self.config) {
244 return Ok(BatchHandle::Immediate(self.execute_single(op)?));
246 }
247
248 let mut batch_lock = self.current_batch.lock_or_recover();
249
250 let batch = batch_lock.get_or_insert_with(|| OperationBatch::new(op.device()));
252
253 if !batch.can_add(&self.config) || batch.device != op.device() {
255 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 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 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 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 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 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 for result in results {
355 result?;
356 }
357 } else {
358 for op in batch.operations {
360 self.execute_single(op)?;
361 }
362 }
363
364 Ok(())
365 }
366
367 pub fn stats(&self) -> BatchingStats {
369 self.stats.lock_or_recover().clone()
370 }
371
372 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
395pub enum BatchHandle<T: TensorElement> {
397 Immediate(Tensor<T>),
399 Batched,
401}
402
403#[derive(Debug, Clone)]
405pub struct BatchingStats {
406 pub batches_executed: usize,
408 pub total_ops_batched: usize,
410 pub avg_batch_size: f64,
412 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 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 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 assert!(!batch.is_ready(&config));
519
520 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 let b = tensor_1d(&[2.0f32]).expect("tensor_1d creation should succeed");
527 batch.add(BatchableOp::AddScalar(Arc::new(b), 1.0));
528
529 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)); }
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); let ops_saved = stats.ops_saved();
549 assert_eq!(ops_saved, 40.0); }
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 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}