1use anyhow::{anyhow, Result};
8use scirs2_core::gpu::{GpuBackend, GpuContext};
9use scirs2_core::ndarray_ext::{Array1, Array2};
10use serde::{Deserialize, Serialize};
11use std::collections::{HashMap, VecDeque};
12use std::sync::atomic::{AtomicUsize, Ordering};
13use std::sync::{Arc, Mutex};
14use std::time::{Duration, Instant};
15use tracing::{debug, info, warn};
16
17#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct GpuAccelerationConfig {
20 pub enabled: bool,
22 pub device_ids: Vec<usize>,
24 pub memory_pool_size_mb: usize,
26 pub mixed_precision: bool,
28 pub tensor_caching: bool,
30 pub cache_size_mb: usize,
32 pub kernel_fusion: bool,
34 pub memory_mapping: bool,
36 pub unified_memory: bool,
38 pub multi_stream: bool,
40 pub num_streams: usize,
42 pub pipeline_parallelism: bool,
44 pub pipeline_stages: usize,
46}
47
48impl Default for GpuAccelerationConfig {
49 fn default() -> Self {
50 Self {
51 enabled: true,
52 device_ids: vec![0],
53 memory_pool_size_mb: 2048, mixed_precision: true,
55 tensor_caching: true,
56 cache_size_mb: 512, kernel_fusion: true,
58 memory_mapping: true,
59 unified_memory: false, multi_stream: true,
61 num_streams: 4,
62 pipeline_parallelism: false, pipeline_stages: 2,
64 }
65 }
66}
67
68pub struct GpuMemoryPool {
70 config: GpuAccelerationConfig,
71 allocated_blocks: Arc<Mutex<HashMap<usize, MemoryBlock>>>,
72 free_blocks: Arc<Mutex<VecDeque<MemoryBlock>>>,
73 total_allocated: Arc<Mutex<usize>>,
74 allocation_stats: Arc<Mutex<AllocationStats>>,
75}
76
77#[derive(Debug, Clone)]
79struct MemoryBlock {
80 device_id: usize,
81 size_bytes: usize,
82 ptr: usize, allocated_at: Instant,
84 last_used: Instant,
85}
86
87#[derive(Debug, Default, Clone)]
89pub struct AllocationStats {
90 pub total_allocations: usize,
91 pub total_deallocations: usize,
92 pub peak_memory_usage: usize,
93 pub current_memory_usage: usize,
94 pub cache_hits: usize,
95 pub cache_misses: usize,
96}
97
98impl GpuMemoryPool {
99 pub fn new(config: GpuAccelerationConfig) -> Self {
101 Self {
102 config,
103 allocated_blocks: Arc::new(Mutex::new(HashMap::new())),
104 free_blocks: Arc::new(Mutex::new(VecDeque::new())),
105 total_allocated: Arc::new(Mutex::new(0)),
106 allocation_stats: Arc::new(Mutex::new(AllocationStats::default())),
107 }
108 }
109
110 pub fn allocate(&self, size_bytes: usize, device_id: usize) -> Result<usize> {
112 let mut free_blocks = self.free_blocks.lock().expect("lock poisoned");
113 let mut allocated_blocks = self.allocated_blocks.lock().expect("lock poisoned");
114 let mut stats = self.allocation_stats.lock().expect("lock poisoned");
115
116 for (i, block) in free_blocks.iter().enumerate() {
118 if block.size_bytes >= size_bytes && block.device_id == device_id {
119 let block = free_blocks
120 .remove(i)
121 .expect("index i should be valid from enumerate");
122 let block_id = block.ptr;
123
124 let mut reused_block = block;
125 reused_block.last_used = Instant::now();
126
127 allocated_blocks.insert(block_id, reused_block);
128 stats.cache_hits += 1;
129
130 debug!(
131 "Reused GPU memory block {} of size {}",
132 block_id, size_bytes
133 );
134 return Ok(block_id);
135 }
136 }
137
138 stats.cache_misses += 1;
140 stats.total_allocations += 1;
141
142 let block_id = stats.total_allocations; let now = Instant::now();
144
145 let block = MemoryBlock {
146 device_id,
147 size_bytes,
148 ptr: block_id,
149 allocated_at: now,
150 last_used: now,
151 };
152
153 allocated_blocks.insert(block_id, block);
154
155 let mut total_allocated = self.total_allocated.lock().expect("lock poisoned");
156 *total_allocated += size_bytes;
157 stats.current_memory_usage += size_bytes;
158
159 if stats.current_memory_usage > stats.peak_memory_usage {
160 stats.peak_memory_usage = stats.current_memory_usage;
161 }
162
163 info!(
164 "Allocated new GPU memory block {} of size {} bytes",
165 block_id, size_bytes
166 );
167 Ok(block_id)
168 }
169
170 pub fn deallocate(&self, block_id: usize) -> Result<()> {
172 let mut allocated_blocks = self.allocated_blocks.lock().expect("lock poisoned");
173 let mut free_blocks = self.free_blocks.lock().expect("lock poisoned");
174 let mut stats = self.allocation_stats.lock().expect("lock poisoned");
175
176 if let Some(block) = allocated_blocks.remove(&block_id) {
177 stats.total_deallocations += 1;
178 stats.current_memory_usage -= block.size_bytes;
179
180 free_blocks.push_back(block);
182
183 if free_blocks.len() > 100 {
185 free_blocks.pop_front();
186 }
187
188 debug!("Deallocated GPU memory block {}", block_id);
189 Ok(())
190 } else {
191 Err(anyhow!("Block {} not found for deallocation", block_id))
192 }
193 }
194
195 pub fn get_stats(&self) -> AllocationStats {
197 (*self.allocation_stats.lock().expect("lock poisoned")).clone()
198 }
199
200 pub fn defragment(&self) -> Result<()> {
202 let mut free_blocks = self.free_blocks.lock().expect("lock poisoned");
203
204 let mut blocks: Vec<_> = free_blocks.drain(..).collect();
206 blocks.sort_by_key(|b| (b.device_id, b.size_bytes));
207
208 let mut merged_blocks = VecDeque::new();
210 let mut current_block: Option<MemoryBlock> = None;
211
212 for block in blocks {
213 if let Some(ref mut current) = current_block {
214 if current.device_id == block.device_id {
215 current.size_bytes += block.size_bytes;
217 } else {
218 merged_blocks.push_back(current.clone());
219 current_block = Some(block);
220 }
221 } else {
222 current_block = Some(block);
223 }
224 }
225
226 if let Some(block) = current_block {
227 merged_blocks.push_back(block);
228 }
229
230 *free_blocks = merged_blocks;
231
232 info!(
233 "Memory defragmentation completed, {} free blocks remaining",
234 free_blocks.len()
235 );
236 Ok(())
237 }
238}
239
240pub struct TensorCache {
242 config: GpuAccelerationConfig,
243 entity_tensors: Arc<Mutex<HashMap<String, CachedTensor>>>,
244 attention_weights: Arc<Mutex<HashMap<String, CachedTensor>>>,
245 intermediate_activations: Arc<Mutex<HashMap<String, CachedTensor>>>,
246 cache_stats: Arc<Mutex<CacheStats>>,
247}
248
249#[derive(Debug, Clone)]
251struct CachedTensor {
252 data: Array2<f32>, device_id: usize,
254 last_accessed: Instant,
255 access_count: usize,
256 size_bytes: usize,
257}
258
259#[derive(Debug, Default, Clone)]
261pub struct CacheStats {
262 pub hits: usize,
263 pub misses: usize,
264 pub evictions: usize,
265 pub total_memory_usage: usize,
266}
267
268impl TensorCache {
269 pub fn new(config: GpuAccelerationConfig) -> Self {
271 Self {
272 config,
273 entity_tensors: Arc::new(Mutex::new(HashMap::new())),
274 attention_weights: Arc::new(Mutex::new(HashMap::new())),
275 intermediate_activations: Arc::new(Mutex::new(HashMap::new())),
276 cache_stats: Arc::new(Mutex::new(CacheStats::default())),
277 }
278 }
279
280 pub fn cache_entity_tensor(&self, entity: &str, tensor: Array2<f32>, device_id: usize) {
286 let size_bytes = tensor.len() * std::mem::size_of::<f32>();
287 let cached_tensor = CachedTensor {
288 data: tensor,
289 device_id,
290 last_accessed: Instant::now(),
291 access_count: 1,
292 size_bytes,
293 };
294
295 let mut stats = self.cache_stats.lock().expect("lock poisoned");
296 let mut entity_tensors = self.entity_tensors.lock().expect("lock poisoned");
297 let mut attention = self.attention_weights.lock().expect("lock poisoned");
298 let mut intermediate = self.intermediate_activations.lock().expect("lock poisoned");
299
300 Self::make_room(
302 self.config.cache_size_mb * 1024 * 1024,
303 &mut stats,
304 &mut entity_tensors,
305 &mut attention,
306 &mut intermediate,
307 size_bytes,
308 );
309
310 if let Some(old) = entity_tensors.insert(entity.to_string(), cached_tensor) {
312 stats.total_memory_usage = stats.total_memory_usage.saturating_sub(old.size_bytes);
313 }
314 stats.total_memory_usage += size_bytes;
315
316 debug!("Cached entity tensor for {}", entity);
317 }
318
319 pub fn get_entity_tensor(&self, entity: &str) -> Option<Array2<f32>> {
321 let mut stats = self.cache_stats.lock().expect("lock poisoned");
322 let mut cache = self.entity_tensors.lock().expect("lock poisoned");
323
324 if let Some(cached) = cache.get_mut(entity) {
325 cached.last_accessed = Instant::now();
326 cached.access_count += 1;
327 stats.hits += 1;
328
329 debug!("Cache hit for entity tensor {}", entity);
330 Some(cached.data.clone())
331 } else {
332 stats.misses += 1;
333 debug!("Cache miss for entity tensor {}", entity);
334 None
335 }
336 }
337
338 pub fn cache_attention_weights(&self, key: &str, weights: Array2<f32>, device_id: usize) {
340 let size_bytes = weights.len() * std::mem::size_of::<f32>();
341 let cached_tensor = CachedTensor {
342 data: weights,
343 device_id,
344 last_accessed: Instant::now(),
345 access_count: 1,
346 size_bytes,
347 };
348
349 let mut stats = self.cache_stats.lock().expect("lock poisoned");
350 let mut entity_tensors = self.entity_tensors.lock().expect("lock poisoned");
351 let mut attention = self.attention_weights.lock().expect("lock poisoned");
352 let mut intermediate = self.intermediate_activations.lock().expect("lock poisoned");
353
354 Self::make_room(
355 self.config.cache_size_mb * 1024 * 1024,
356 &mut stats,
357 &mut entity_tensors,
358 &mut attention,
359 &mut intermediate,
360 size_bytes,
361 );
362
363 if let Some(old) = attention.insert(key.to_string(), cached_tensor) {
364 stats.total_memory_usage = stats.total_memory_usage.saturating_sub(old.size_bytes);
365 }
366 stats.total_memory_usage += size_bytes;
367
368 debug!("Cached attention weights for key {}", key);
369 }
370
371 pub fn get_attention_weights(&self, key: &str) -> Option<Array2<f32>> {
373 let mut stats = self.cache_stats.lock().expect("lock poisoned");
374 let mut cache = self.attention_weights.lock().expect("lock poisoned");
375
376 if let Some(cached) = cache.get_mut(key) {
377 cached.last_accessed = Instant::now();
378 cached.access_count += 1;
379 stats.hits += 1;
380
381 debug!("Cache hit for attention weights {}", key);
382 Some(cached.data.clone())
383 } else {
384 stats.misses += 1;
385 debug!("Cache miss for attention weights {}", key);
386 None
387 }
388 }
389
390 fn make_room(
397 max_memory: usize,
398 stats: &mut CacheStats,
399 entity_tensors: &mut HashMap<String, CachedTensor>,
400 attention: &mut HashMap<String, CachedTensor>,
401 intermediate: &mut HashMap<String, CachedTensor>,
402 incoming_bytes: usize,
403 ) {
404 while stats.total_memory_usage + incoming_bytes > max_memory {
405 let e_lru = entity_tensors
407 .iter()
408 .min_by_key(|(_, v)| v.last_accessed)
409 .map(|(k, v)| (k.clone(), v.last_accessed, v.size_bytes));
410 let a_lru = attention
411 .iter()
412 .min_by_key(|(_, v)| v.last_accessed)
413 .map(|(k, v)| (k.clone(), v.last_accessed, v.size_bytes));
414 let i_lru = intermediate
415 .iter()
416 .min_by_key(|(_, v)| v.last_accessed)
417 .map(|(k, v)| (k.clone(), v.last_accessed, v.size_bytes));
418
419 enum Which {
421 Entity,
422 Attention,
423 Intermediate,
424 }
425 let mut choice: Option<(Which, String, usize, Instant)> = None;
426 for (which, cand) in [
427 (Which::Entity, e_lru),
428 (Which::Attention, a_lru),
429 (Which::Intermediate, i_lru),
430 ] {
431 if let Some((k, ts, sz)) = cand {
432 let is_older = match &choice {
433 Some((_, _, _, best_ts)) => ts < *best_ts,
434 None => true,
435 };
436 if is_older {
437 choice = Some((which, k, sz, ts));
438 }
439 }
440 }
441
442 let Some((which, key, size_bytes, _)) = choice else {
443 break;
445 };
446
447 match which {
448 Which::Entity => {
449 entity_tensors.remove(&key);
450 }
451 Which::Attention => {
452 attention.remove(&key);
453 }
454 Which::Intermediate => {
455 intermediate.remove(&key);
456 }
457 }
458 stats.total_memory_usage = stats.total_memory_usage.saturating_sub(size_bytes);
459 stats.evictions += 1;
460 debug!(
461 "Evicted LRU tensor '{}' ({} bytes) from cache",
462 key, size_bytes
463 );
464 }
465 }
466
467 pub fn get_stats(&self) -> CacheStats {
469 (*self.cache_stats.lock().expect("lock poisoned")).clone()
470 }
471
472 pub fn clear_all(&self) {
477 let mut stats = self.cache_stats.lock().expect("lock poisoned");
478 self.entity_tensors.lock().expect("lock poisoned").clear();
479 self.attention_weights
480 .lock()
481 .expect("lock poisoned")
482 .clear();
483 self.intermediate_activations
484 .lock()
485 .expect("lock poisoned")
486 .clear();
487
488 stats.total_memory_usage = 0;
489
490 info!("Cleared all tensor caches");
491 }
492}
493
494pub struct MixedPrecisionProcessor {
496 config: GpuAccelerationConfig,
497 fp16_enabled: bool,
498 loss_scaling: f32,
499 overflow_detection: bool,
500}
501
502impl MixedPrecisionProcessor {
503 pub fn new(config: GpuAccelerationConfig) -> Self {
505 Self {
506 config: config.clone(),
507 fp16_enabled: config.mixed_precision,
508 loss_scaling: 65536.0, overflow_detection: true,
510 }
511 }
512
513 pub fn to_fp16(&self, tensor: &Array2<f32>) -> Array2<f32> {
515 if !self.fp16_enabled {
516 return tensor.clone();
517 }
518
519 tensor.mapv(|x| {
521 let clamped = x.clamp(-65504.0, 65504.0);
523 (clamped * 1024.0).round() / 1024.0 })
525 }
526
527 pub fn scale_loss(&self, loss: f32) -> f32 {
529 if self.fp16_enabled {
530 loss * self.loss_scaling
531 } else {
532 loss
533 }
534 }
535
536 pub fn unscale_gradients(&self, gradients: &mut Array2<f32>) -> bool {
538 if !self.fp16_enabled {
539 return true;
540 }
541
542 if self.overflow_detection {
544 let has_overflow = gradients.iter().any(|&x| !x.is_finite());
545 if has_overflow {
546 warn!("Gradient overflow detected in mixed precision training");
547 return false;
548 }
549 }
550
551 gradients.mapv_inplace(|x| x / self.loss_scaling);
553 true
554 }
555
556 pub fn adjust_loss_scaling(&mut self, overflow_detected: bool) {
558 if overflow_detected {
559 self.loss_scaling = (self.loss_scaling / 2.0).max(1.0);
560 info!("Reduced loss scaling to {}", self.loss_scaling);
561 } else {
562 self.loss_scaling = (self.loss_scaling * 1.1).min(65536.0);
564 }
565 }
566}
567
568pub struct MultiStreamProcessor {
570 config: GpuAccelerationConfig,
571 pub stream_ids: Vec<usize>,
572 current_stream: usize,
573}
574
575impl MultiStreamProcessor {
576 pub fn new(config: GpuAccelerationConfig) -> Self {
578 let stream_ids = (0..config.num_streams).collect();
579
580 Self {
581 config,
582 stream_ids,
583 current_stream: 0,
584 }
585 }
586
587 pub fn get_next_stream(&mut self) -> usize {
589 let stream_id = self.stream_ids[self.current_stream];
590 self.current_stream = (self.current_stream + 1) % self.stream_ids.len();
591 stream_id
592 }
593
594 pub async fn process_batch_parallel(
596 &mut self,
597 entities: Vec<String>,
598 process_fn: impl Fn(String, usize) -> Array1<f32> + Send + Sync + Copy + 'static,
599 ) -> Result<Vec<Array1<f32>>> {
600 let chunk_size = (entities.len() + self.config.num_streams - 1) / self.config.num_streams;
601 let mut tasks = Vec::new();
602
603 for chunk in entities.chunks(chunk_size) {
604 let stream_id = self.get_next_stream();
605 let chunk_entities = chunk.to_vec();
606
607 let task = tokio::spawn(async move {
608 let mut results = Vec::new();
609 for entity in chunk_entities {
610 let embedding = process_fn(entity, stream_id);
611 results.push(embedding);
612 }
613 results
614 });
615
616 tasks.push(task);
617 }
618
619 let mut all_results = Vec::new();
621 for task in tasks {
622 let chunk_results = task.await?;
623 all_results.extend(chunk_results);
624 }
625
626 Ok(all_results)
627 }
628
629 pub fn synchronize_all(&self) {
631 debug!("Synchronized {} GPU streams", self.stream_ids.len());
633 }
634}
635
636pub struct GpuAccelerationManager {
638 config: GpuAccelerationConfig,
639 memory_pool: GpuMemoryPool,
640 tensor_cache: TensorCache,
641 mixed_precision: MixedPrecisionProcessor,
642 multi_stream: MultiStreamProcessor,
643}
644
645impl GpuAccelerationManager {
646 pub fn new(config: GpuAccelerationConfig) -> Self {
648 let memory_pool = GpuMemoryPool::new(config.clone());
649 let tensor_cache = TensorCache::new(config.clone());
650 let mixed_precision = MixedPrecisionProcessor::new(config.clone());
651 let multi_stream = MultiStreamProcessor::new(config.clone());
652
653 Self {
654 config,
655 memory_pool,
656 tensor_cache,
657 mixed_precision,
658 multi_stream,
659 }
660 }
661
662 pub fn memory_pool(&self) -> &GpuMemoryPool {
664 &self.memory_pool
665 }
666
667 pub fn tensor_cache(&self) -> &TensorCache {
669 &self.tensor_cache
670 }
671
672 pub fn mixed_precision(&mut self) -> &mut MixedPrecisionProcessor {
674 &mut self.mixed_precision
675 }
676
677 pub fn multi_stream(&mut self) -> &mut MultiStreamProcessor {
679 &mut self.multi_stream
680 }
681
682 pub async fn accelerated_embedding_generation(
684 &mut self,
685 entities: Vec<String>,
686 base_compute_fn: impl Fn(&str) -> Array1<f32> + Send + Sync + Copy + 'static,
687 ) -> Result<Vec<Array1<f32>>> {
688 if !self.config.enabled {
689 return Ok(entities.iter().map(|e| base_compute_fn(e)).collect());
691 }
692
693 let results = self
695 .multi_stream
696 .process_batch_parallel(entities, move |entity, stream_id| {
697 debug!("Processing entity {} on stream {}", entity, stream_id);
699 base_compute_fn(&entity)
700 })
701 .await?;
702
703 self.multi_stream.synchronize_all();
704 Ok(results)
705 }
706
707 pub fn get_performance_stats(&self) -> GpuPerformanceStats {
709 let memory_stats = self.memory_pool.get_stats();
710 let cache_stats = self.tensor_cache.get_stats();
711
712 GpuPerformanceStats {
713 memory_allocations: memory_stats.total_allocations,
714 memory_deallocations: memory_stats.total_deallocations,
715 peak_memory_usage_mb: memory_stats.peak_memory_usage / (1024 * 1024),
716 current_memory_usage_mb: memory_stats.current_memory_usage / (1024 * 1024),
717 memory_pool_hits: memory_stats.cache_hits,
718 memory_pool_misses: memory_stats.cache_misses,
719 tensor_cache_hits: cache_stats.hits,
720 tensor_cache_misses: cache_stats.misses,
721 tensor_cache_evictions: cache_stats.evictions,
722 tensor_cache_memory_mb: cache_stats.total_memory_usage / (1024 * 1024),
723 loss_scaling_factor: self.mixed_precision.loss_scaling,
724 num_active_streams: self.config.num_streams,
725 }
726 }
727}
728
729#[derive(Debug, Serialize)]
731pub struct GpuPerformanceStats {
732 pub memory_allocations: usize,
733 pub memory_deallocations: usize,
734 pub peak_memory_usage_mb: usize,
735 pub current_memory_usage_mb: usize,
736 pub memory_pool_hits: usize,
737 pub memory_pool_misses: usize,
738 pub tensor_cache_hits: usize,
739 pub tensor_cache_misses: usize,
740 pub tensor_cache_evictions: usize,
741 pub tensor_cache_memory_mb: usize,
742 pub loss_scaling_factor: f32,
743 pub num_active_streams: usize,
744}
745
746pub struct MemoryDefragmenter {
748 config: GpuAccelerationConfig,
749 defrag_threshold: f32,
750 last_defrag: Instant,
751 defrag_interval: Duration,
752}
753
754impl MemoryDefragmenter {
755 pub fn new(config: GpuAccelerationConfig) -> Self {
757 Self {
758 config,
759 defrag_threshold: 0.7, last_defrag: Instant::now(),
761 defrag_interval: Duration::from_secs(300), }
763 }
764
765 pub fn should_defragment(&self, memory_pool: &GpuMemoryPool) -> bool {
767 let stats = memory_pool.get_stats();
768 let fragmentation_ratio = self.calculate_fragmentation_ratio(&stats);
769
770 fragmentation_ratio > self.defrag_threshold
771 && self.last_defrag.elapsed() > self.defrag_interval
772 }
773
774 fn calculate_fragmentation_ratio(&self, stats: &AllocationStats) -> f32 {
776 if stats.current_memory_usage == 0 {
777 return 0.0;
778 }
779
780 let theoretical_optimal = stats.current_memory_usage;
783 let actual_allocated = stats.peak_memory_usage;
784
785 if actual_allocated == 0 {
786 0.0
787 } else {
788 1.0 - (theoretical_optimal as f32 / actual_allocated as f32)
789 }
790 }
791
792 pub fn defragment(&mut self, memory_pool: &GpuMemoryPool) -> Result<DefragmentationResult> {
794 info!("Starting GPU memory defragmentation");
795 let start_time = Instant::now();
796
797 std::thread::sleep(Duration::from_millis(100));
804
805 let stats_before = memory_pool.get_stats();
806
807 let stats_after = memory_pool.get_stats();
811 self.last_defrag = Instant::now();
812
813 let result = DefragmentationResult {
814 duration: start_time.elapsed(),
815 memory_freed: stats_before
816 .peak_memory_usage
817 .saturating_sub(stats_after.current_memory_usage),
818 fragmentation_before: self.calculate_fragmentation_ratio(&stats_before),
819 fragmentation_after: self.calculate_fragmentation_ratio(&stats_after),
820 };
821
822 info!("Defragmentation completed: {:?}", result);
823 Ok(result)
824 }
825}
826
827#[derive(Debug, Clone)]
829pub struct DefragmentationResult {
830 pub duration: Duration,
831 pub memory_freed: usize,
832 pub fragmentation_before: f32,
833 pub fragmentation_after: f32,
834}
835
836pub struct OutOfCoreProcessor {
838 config: GpuAccelerationConfig,
839 chunk_size: usize,
840 overlap_size: usize,
841 memory_limit: usize,
842}
843
844impl OutOfCoreProcessor {
845 pub fn new(config: GpuAccelerationConfig) -> Self {
847 let memory_limit = config.memory_pool_size_mb * 1024 * 1024; let chunk_size = memory_limit / 4; let overlap_size = chunk_size / 10; Self {
852 config,
853 chunk_size,
854 overlap_size,
855 memory_limit,
856 }
857 }
858
859 pub async fn process_large_batch<T>(
861 &self,
862 data: Vec<T>,
863 process_fn: impl Fn(&[T]) -> Result<Vec<Array1<f32>>> + Send + Sync + Copy,
864 ) -> Result<Vec<Array1<f32>>>
865 where
866 T: Clone + Send + Sync + 'static,
867 {
868 if data.is_empty() {
869 return Ok(Vec::new());
870 }
871
872 let item_size = std::mem::size_of::<T>();
874 let max_items_per_chunk = self.chunk_size / item_size;
875 let chunk_size = max_items_per_chunk.clamp(1, 1000); info!(
878 "Processing {} items in chunks of {}",
879 data.len(),
880 chunk_size
881 );
882
883 let mut results = Vec::new();
884 let mut processed_count = 0;
885
886 for chunk in data.chunks(chunk_size) {
887 let chunk_results = process_fn(chunk)?;
889 results.extend(chunk_results);
890
891 processed_count += chunk.len();
892
893 if processed_count % (chunk_size * 10) == 0 {
894 info!("Processed {}/{} items", processed_count, data.len());
895 }
896
897 tokio::task::yield_now().await;
899 }
900
901 Ok(results)
902 }
903
904 pub async fn process_with_overlap<T>(
906 &self,
907 data: Vec<T>,
908 process_fn: impl Fn(&[T]) -> Result<Vec<Array1<f32>>> + Send + Sync + Copy,
909 ) -> Result<Vec<Array1<f32>>>
910 where
911 T: Clone + Send + Sync + 'static,
912 {
913 if data.is_empty() {
914 return Ok(Vec::new());
915 }
916
917 let item_size = std::mem::size_of::<T>();
918 let max_items_per_chunk = self.chunk_size / item_size;
919 let chunk_size = max_items_per_chunk.clamp(1, 1000);
920
921 let mut results = Vec::new();
922 let mut start_idx = 0;
923
924 while start_idx < data.len() {
925 let end_idx = (start_idx + chunk_size).min(data.len());
926 let chunk = &data[start_idx..end_idx];
927
928 let chunk_results = process_fn(chunk)?;
929
930 let take_count = if start_idx == 0 {
932 chunk_results.len()
933 } else {
934 chunk_results
936 .len()
937 .saturating_sub(self.overlap_size / item_size)
938 };
939
940 results.extend(chunk_results.into_iter().take(take_count));
941
942 start_idx += chunk_size - self.overlap_size / item_size;
943 tokio::task::yield_now().await;
944 }
945
946 Ok(results)
947 }
948}
949
950pub struct DynamicShapeHandler {
952 config: GpuAccelerationConfig,
953 shape_cache: HashMap<Vec<usize>, ShapeInfo>,
954 max_cached_shapes: usize,
955}
956
957#[derive(Debug, Clone)]
959struct ShapeInfo {
960 shape: Vec<usize>,
961 memory_requirement: usize,
962 optimal_batch_size: usize,
963 last_used: Instant,
964}
965
966impl DynamicShapeHandler {
967 pub fn new(config: GpuAccelerationConfig) -> Self {
969 Self {
970 config,
971 shape_cache: HashMap::new(),
972 max_cached_shapes: 100,
973 }
974 }
975
976 pub fn optimize_shape(&mut self, shape: Vec<usize>) -> Vec<usize> {
978 if let Some(shape_info) = self.shape_cache.get_mut(&shape) {
980 shape_info.last_used = Instant::now();
981 return shape_info.shape.clone();
982 }
983
984 let optimized_shape = self.calculate_optimal_shape(&shape);
986
987 self.cache_shape_info(shape.clone(), optimized_shape.clone());
989
990 optimized_shape
991 }
992
993 fn calculate_optimal_shape(&self, shape: &[usize]) -> Vec<usize> {
995 let mut optimized = shape.to_vec();
996
997 const WARP_SIZE: usize = 32;
999
1000 for dim in &mut optimized {
1001 if *dim > 0 {
1002 *dim = ((*dim + WARP_SIZE - 1) / WARP_SIZE) * WARP_SIZE;
1004 }
1005 }
1006
1007 optimized
1008 }
1009
1010 fn cache_shape_info(&mut self, original_shape: Vec<usize>, optimized_shape: Vec<usize>) {
1012 if self.shape_cache.len() >= self.max_cached_shapes {
1014 self.evict_oldest_shape();
1015 }
1016
1017 let memory_requirement = optimized_shape.iter().product::<usize>() * 4; let optimal_batch_size = self.calculate_optimal_batch_size(memory_requirement);
1019
1020 let shape_info = ShapeInfo {
1021 shape: optimized_shape,
1022 memory_requirement,
1023 optimal_batch_size,
1024 last_used: Instant::now(),
1025 };
1026
1027 self.shape_cache.insert(original_shape, shape_info);
1028 }
1029
1030 fn calculate_optimal_batch_size(&self, memory_per_item: usize) -> usize {
1032 if memory_per_item == 0 {
1033 return 1;
1034 }
1035
1036 let available_memory = (self.config.memory_pool_size_mb * 1024 * 1024) / 2; let max_batch_size = available_memory / memory_per_item;
1038
1039 max_batch_size.clamp(1, 1024)
1041 }
1042
1043 fn evict_oldest_shape(&mut self) {
1045 if let Some(oldest_key) = self
1046 .shape_cache
1047 .iter()
1048 .min_by_key(|(_, info)| info.last_used)
1049 .map(|(key, _)| key.clone())
1050 {
1051 self.shape_cache.remove(&oldest_key);
1052 }
1053 }
1054
1055 pub fn get_optimal_batch_size(&self, shape: &[usize]) -> usize {
1057 self.shape_cache
1058 .get(shape)
1059 .map(|info| info.optimal_batch_size)
1060 .unwrap_or(1)
1061 }
1062}
1063
1064pub struct BatchSizeOptimizer {
1066 config: GpuAccelerationConfig,
1067 performance_history: VecDeque<BatchPerformance>,
1068 max_history_size: usize,
1069 current_optimal_batch_size: usize,
1070}
1071
1072#[derive(Debug, Clone)]
1074struct BatchPerformance {
1075 batch_size: usize,
1076 processing_time: Duration,
1077 memory_usage: usize,
1078 throughput: f64, gpu_utilization: f64,
1080 timestamp: Instant,
1081}
1082
1083impl BatchSizeOptimizer {
1084 pub fn new(config: GpuAccelerationConfig) -> Self {
1086 Self {
1087 config,
1088 performance_history: VecDeque::new(),
1089 max_history_size: 50,
1090 current_optimal_batch_size: 32, }
1092 }
1093
1094 pub async fn find_optimal_batch_size<T>(
1096 &mut self,
1097 sample_data: Vec<T>,
1098 process_fn: impl Fn(&[T]) -> Result<Vec<Array1<f32>>> + Send + Sync + Copy,
1099 ) -> Result<usize>
1100 where
1101 T: Clone + Send + Sync + 'static,
1102 {
1103 if sample_data.is_empty() {
1104 return Ok(1);
1105 }
1106
1107 info!("Optimizing batch size for embedding generation");
1108
1109 let test_sizes = vec![1, 8, 16, 32, 64, 128, 256, 512];
1110 let max_test_size = sample_data.len().min(512);
1111
1112 let mut best_batch_size = 1;
1113 let mut best_throughput = 0.0;
1114
1115 for &batch_size in &test_sizes {
1116 if batch_size > max_test_size {
1117 break;
1118 }
1119
1120 let performance = self
1122 .test_batch_size(
1123 &sample_data[..batch_size.min(sample_data.len())],
1124 batch_size,
1125 process_fn,
1126 )
1127 .await?;
1128
1129 info!(
1130 "Batch size {}: {:.2} items/sec, {:.1}ms processing time",
1131 batch_size,
1132 performance.throughput,
1133 performance.processing_time.as_millis()
1134 );
1135
1136 if performance.throughput > best_throughput {
1137 best_throughput = performance.throughput;
1138 best_batch_size = batch_size;
1139 }
1140
1141 self.performance_history.push_back(performance);
1143 if self.performance_history.len() > self.max_history_size {
1144 self.performance_history.pop_front();
1145 }
1146
1147 tokio::time::sleep(Duration::from_millis(100)).await;
1149 }
1150
1151 self.current_optimal_batch_size = best_batch_size;
1152 info!("Optimal batch size determined: {}", best_batch_size);
1153
1154 Ok(best_batch_size)
1155 }
1156
1157 async fn test_batch_size<T>(
1159 &self,
1160 sample_data: &[T],
1161 batch_size: usize,
1162 process_fn: impl Fn(&[T]) -> Result<Vec<Array1<f32>>>,
1163 ) -> Result<BatchPerformance>
1164 where
1165 T: Clone,
1166 {
1167 let start_time = Instant::now();
1168 let memory_before = self.estimate_memory_usage();
1169
1170 let _results = process_fn(sample_data)?;
1172
1173 let processing_time = start_time.elapsed();
1174 let memory_after = self.estimate_memory_usage();
1175 let memory_usage = memory_after.saturating_sub(memory_before);
1176
1177 let throughput = if processing_time.as_secs_f64() > 0.0 {
1179 sample_data.len() as f64 / processing_time.as_secs_f64()
1180 } else {
1181 0.0
1182 };
1183
1184 let gpu_utilization = self.estimate_gpu_utilization(batch_size, processing_time);
1186
1187 Ok(BatchPerformance {
1188 batch_size,
1189 processing_time,
1190 memory_usage,
1191 throughput,
1192 gpu_utilization,
1193 timestamp: Instant::now(),
1194 })
1195 }
1196
1197 fn estimate_memory_usage(&self) -> usize {
1199 (self.config.memory_pool_size_mb * 1024 * 1024) / 4 }
1203
1204 fn estimate_gpu_utilization(&self, batch_size: usize, processing_time: Duration) -> f64 {
1206 let base_utilization = (batch_size as f64).log2() / 10.0; let time_factor = if processing_time.as_millis() < 10 {
1209 0.5 } else if processing_time.as_millis() > 1000 {
1211 0.7 } else {
1213 1.0
1214 };
1215
1216 (base_utilization * time_factor).clamp(0.0, 1.0)
1217 }
1218
1219 pub fn get_optimal_batch_size(&self) -> usize {
1221 self.current_optimal_batch_size
1222 }
1223
1224 pub fn get_performance_stats(&self) -> BatchSizeOptimizerStats {
1226 let avg_throughput = if !self.performance_history.is_empty() {
1227 self.performance_history
1228 .iter()
1229 .map(|p| p.throughput)
1230 .sum::<f64>()
1231 / self.performance_history.len() as f64
1232 } else {
1233 0.0
1234 };
1235
1236 let avg_gpu_utilization = if !self.performance_history.is_empty() {
1237 self.performance_history
1238 .iter()
1239 .map(|p| p.gpu_utilization)
1240 .sum::<f64>()
1241 / self.performance_history.len() as f64
1242 } else {
1243 0.0
1244 };
1245
1246 BatchSizeOptimizerStats {
1247 current_optimal_batch_size: self.current_optimal_batch_size,
1248 avg_throughput,
1249 avg_gpu_utilization,
1250 total_tests_performed: self.performance_history.len(),
1251 }
1252 }
1253}
1254
1255#[derive(Debug, Clone, Serialize, Deserialize)]
1257pub struct BatchSizeOptimizerStats {
1258 pub current_optimal_batch_size: usize,
1259 pub avg_throughput: f64,
1260 pub avg_gpu_utilization: f64,
1261 pub total_tests_performed: usize,
1262}
1263
1264#[cfg(test)]
1265mod tests {
1266 use super::*;
1267
1268 #[test]
1272 fn regression_tensor_cache_evicts_real_entries() {
1273 let config = GpuAccelerationConfig {
1275 cache_size_mb: 1,
1276 ..GpuAccelerationConfig::default()
1277 };
1278 let cache = TensorCache::new(config);
1279
1280 for i in 0..40 {
1283 let tensor = Array2::<f32>::zeros((256, 256));
1284 cache.cache_entity_tensor(&format!("entity_{i}"), tensor, 0);
1285 }
1286
1287 let stats = cache.get_stats();
1288 let max_bytes = 1024 * 1024;
1289 assert!(
1291 stats.total_memory_usage <= max_bytes,
1292 "cache exceeded budget: {} > {}",
1293 stats.total_memory_usage,
1294 max_bytes
1295 );
1296 assert!(stats.evictions > 0, "expected real evictions to occur");
1297
1298 assert!(cache.get_entity_tensor("entity_39").is_some());
1301 assert!(cache.get_entity_tensor("entity_0").is_none());
1302 }
1303
1304 #[test]
1308 fn regression_bf16_rounding_reduces_precision() {
1309 let x = 1.000_012_3_f32;
1311 let rounded = round_f32_to_bf16(x);
1312 assert_ne!(
1313 rounded, x,
1314 "bf16 rounding must change a high-precision value"
1315 );
1316 assert!((rounded - x).abs() < 0.01);
1318
1319 assert_eq!(round_f32_to_bf16(1.0), 1.0);
1321 assert_eq!(round_f32_to_bf16(0.0), 0.0);
1322 assert_eq!(round_f32_to_bf16(2.0), 2.0);
1323 assert!(round_f32_to_bf16(f32::NAN).is_nan());
1325 }
1326
1327 #[test]
1328 fn test_gpu_acceleration_config_default() {
1329 let config = GpuAccelerationConfig::default();
1330 assert!(config.enabled);
1331 assert_eq!(config.device_ids, vec![0]);
1332 assert_eq!(config.memory_pool_size_mb, 2048);
1333 assert!(config.mixed_precision);
1334 assert!(config.tensor_caching);
1335 }
1336
1337 #[test]
1338 fn test_memory_pool_allocation() {
1339 let config = GpuAccelerationConfig::default();
1340 let pool = GpuMemoryPool::new(config);
1341
1342 let block_id = pool.allocate(1024, 0).expect("should succeed");
1343 assert!(block_id > 0);
1344
1345 pool.deallocate(block_id).expect("should succeed");
1346
1347 let block_id2 = pool.allocate(1024, 0).expect("should succeed");
1349 assert_eq!(block_id, block_id2);
1350 }
1351
1352 #[test]
1353 fn test_tensor_cache() {
1354 let config = GpuAccelerationConfig::default();
1355 let cache = TensorCache::new(config);
1356
1357 let tensor = Array2::zeros((10, 20));
1358 cache.cache_entity_tensor("test_entity", tensor.clone(), 0);
1359
1360 let cached = cache
1361 .get_entity_tensor("test_entity")
1362 .expect("should succeed");
1363 assert_eq!(cached.shape(), tensor.shape());
1364 }
1365
1366 #[test]
1367 fn test_mixed_precision() {
1368 let config = GpuAccelerationConfig::default();
1369 let processor = MixedPrecisionProcessor::new(config);
1370
1371 let tensor = Array2::from_elem((2, 2), 1.0001);
1373 let fp16_tensor = processor.to_fp16(&tensor);
1374
1375 if processor.fp16_enabled {
1376 assert!(fp16_tensor[[0, 0]] != tensor[[0, 0]]);
1378 } else {
1379 assert_eq!(fp16_tensor[[0, 0]], tensor[[0, 0]]);
1381 }
1382 }
1383
1384 #[tokio::test]
1385 async fn test_multi_stream_processing() {
1386 let config = GpuAccelerationConfig::default();
1387 let mut processor = MultiStreamProcessor::new(config);
1388
1389 let entities = vec!["entity1".to_string(), "entity2".to_string()];
1390 let process_fn = |entity: String, _stream_id: usize| -> Array1<f32> {
1391 Array1::from_vec(vec![entity.len() as f32])
1392 };
1393
1394 let results = processor
1395 .process_batch_parallel(entities, process_fn)
1396 .await
1397 .expect("should succeed");
1398 assert_eq!(results.len(), 2);
1399 }
1400
1401 #[test]
1402 fn test_scirs2_gpu_accelerator() {
1403 let config = GpuAccelerationConfig::default();
1405
1406 match SciRS2GpuAccelerator::new(config) {
1407 Ok(accelerator) => {
1408 assert!(accelerator.num_devices() > 0);
1410 }
1411 Err(_) => {
1412 println!("Skipping GPU test: no hardware available");
1414 }
1415 }
1416 }
1417
1418 #[test]
1419 fn test_tensor_core_operations() {
1420 let config = GpuAccelerationConfig::default();
1421
1422 if let Ok(accelerator) = SciRS2GpuAccelerator::new(config) {
1424 let _matrix_a = Array2::<f32>::ones((256, 512));
1426 let _matrix_b = Array2::<f32>::ones((512, 256));
1427
1428 let stats = accelerator.get_stats();
1430 assert_eq!(stats.total_operations, 0);
1431 } else {
1432 println!("Skipping tensor core test: no GPU hardware available");
1433 }
1434 }
1435}
1436
1437pub struct SciRS2GpuAccelerator {
1454 config: GpuAccelerationConfig,
1455 contexts: Vec<GpuContext>,
1456 operations: Arc<AtomicUsize>,
1457}
1458
1459#[inline]
1465fn round_f32_to_bf16(x: f32) -> f32 {
1466 let bits = x.to_bits();
1467 if x.is_nan() {
1469 return x;
1470 }
1471 let rounding_bias = 0x0000_7fff + ((bits >> 16) & 1);
1473 let rounded = bits.wrapping_add(rounding_bias) & 0xffff_0000;
1474 f32::from_bits(rounded)
1475}
1476
1477impl SciRS2GpuAccelerator {
1478 pub fn new(config: GpuAccelerationConfig) -> Result<Self> {
1480 let mut contexts = Vec::new();
1481
1482 for _device_id in &config.device_ids {
1485 match GpuContext::new(GpuBackend::Cuda) {
1486 Ok(ctx) => {
1487 info!("Initialized GPU context");
1488 contexts.push(ctx);
1489 }
1490 Err(e) => {
1491 warn!("Failed to initialize GPU device: {}", e);
1492 }
1493 }
1494 }
1495
1496 if contexts.is_empty() {
1497 return Err(anyhow!("No GPU devices available for acceleration"));
1498 }
1499
1500 Ok(Self {
1501 config,
1502 contexts,
1503 operations: Arc::new(AtomicUsize::new(0)),
1504 })
1505 }
1506
1507 pub fn num_devices(&self) -> usize {
1509 self.contexts.len()
1510 }
1511
1512 pub fn tensor_core_gemm(
1521 &self,
1522 a: &Array2<f32>,
1523 b: &Array2<f32>,
1524 use_mixed_precision: bool,
1525 ) -> Result<Array2<f32>> {
1526 if a.ncols() != b.nrows() {
1527 return Err(anyhow!(
1528 "tensor_core_gemm: incompatible shapes {:?} x {:?}",
1529 a.dim(),
1530 b.dim()
1531 ));
1532 }
1533
1534 let result = if use_mixed_precision && self.config.mixed_precision {
1535 let a_bf16 = a.mapv(round_f32_to_bf16);
1537 let b_bf16 = b.mapv(round_f32_to_bf16);
1538 a_bf16.dot(&b_bf16)
1539 } else {
1540 a.dot(b)
1541 };
1542
1543 self.operations.fetch_add(1, Ordering::Relaxed);
1544
1545 Ok(result)
1546 }
1547
1548 pub fn batch_embed(
1554 &self,
1555 inputs: &[Array1<f32>],
1556 embedding_matrix: &Array2<f32>,
1557 ) -> Result<Vec<Array1<f32>>> {
1558 let batch_size = inputs.len();
1559 let mut results = Vec::with_capacity(batch_size);
1560
1561 let cols = embedding_matrix.ncols();
1562 for (idx, input) in inputs.iter().enumerate() {
1563 if input.len() != cols {
1564 return Err(anyhow!(
1565 "batch_embed: input {} has length {} but embedding matrix expects {}",
1566 idx,
1567 input.len(),
1568 cols
1569 ));
1570 }
1571 results.push(embedding_matrix.dot(input));
1572 }
1573
1574 self.operations.fetch_add(batch_size, Ordering::Relaxed);
1575
1576 Ok(results)
1577 }
1578
1579 pub fn simd_similarity(
1585 &self,
1586 query: &Array1<f32>,
1587 candidates: &[Array1<f32>],
1588 ) -> Result<Vec<f32>> {
1589 let dim = query.len();
1590 let mut similarities = Vec::with_capacity(candidates.len());
1591 for (idx, candidate) in candidates.iter().enumerate() {
1592 if candidate.len() != dim {
1593 return Err(anyhow!(
1594 "simd_similarity: candidate {} has length {} but query has {}",
1595 idx,
1596 candidate.len(),
1597 dim
1598 ));
1599 }
1600 similarities.push(query.dot(candidate));
1601 }
1602
1603 self.operations
1604 .fetch_add(candidates.len(), Ordering::Relaxed);
1605
1606 Ok(similarities)
1607 }
1608
1609 pub fn get_stats(&self) -> AcceleratorStats {
1611 AcceleratorStats {
1612 total_operations: self.operations.load(Ordering::Relaxed),
1613 num_devices: self.contexts.len(),
1614 profiler_report: "Stats available".to_string(),
1615 }
1616 }
1617
1618 pub fn clear_stats(&self) {
1620 self.operations.store(0, Ordering::Relaxed);
1621 }
1622}
1623
1624#[derive(Debug, Clone)]
1626pub struct AcceleratorStats {
1627 pub total_operations: usize,
1628 pub num_devices: usize,
1629 pub profiler_report: String,
1630}