Skip to main content

oxirs_embed/
gpu_acceleration.rs

1//! GPU acceleration and optimization features for embedding models
2//!
3//! This module provides advanced GPU acceleration capabilities including
4//! memory pooling, tensor caching, mixed precision, and compute optimization
5//! with full SciRS2 integration for maximum performance.
6
7use 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/// GPU acceleration configuration
18#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct GpuAccelerationConfig {
20    /// Enable GPU acceleration
21    pub enabled: bool,
22    /// GPU device IDs to use
23    pub device_ids: Vec<usize>,
24    /// Memory pool size in MB
25    pub memory_pool_size_mb: usize,
26    /// Enable mixed precision
27    pub mixed_precision: bool,
28    /// Enable tensor caching
29    pub tensor_caching: bool,
30    /// Cache size in MB
31    pub cache_size_mb: usize,
32    /// Enable kernel fusion
33    pub kernel_fusion: bool,
34    /// Enable memory mapping
35    pub memory_mapping: bool,
36    /// Enable unified memory
37    pub unified_memory: bool,
38    /// Enable multi-stream processing
39    pub multi_stream: bool,
40    /// Number of streams for multi-stream processing
41    pub num_streams: usize,
42    /// Enable pipeline parallelism
43    pub pipeline_parallelism: bool,
44    /// Pipeline stages
45    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, // 2GB default
54            mixed_precision: true,
55            tensor_caching: true,
56            cache_size_mb: 512, // 512MB cache
57            kernel_fusion: true,
58            memory_mapping: true,
59            unified_memory: false, // Conservative default
60            multi_stream: true,
61            num_streams: 4,
62            pipeline_parallelism: false, // Requires careful setup
63            pipeline_stages: 2,
64        }
65    }
66}
67
68/// GPU memory pool for efficient memory management
69pub 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/// Memory block descriptor
78#[derive(Debug, Clone)]
79struct MemoryBlock {
80    device_id: usize,
81    size_bytes: usize,
82    ptr: usize, // In real implementation, this would be a GPU pointer
83    allocated_at: Instant,
84    last_used: Instant,
85}
86
87/// Memory allocation statistics
88#[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    /// Create new GPU memory pool
100    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    /// Allocate GPU memory block
111    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        // Try to find a suitable free block first
117        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        // No suitable free block found, allocate new one
139        stats.cache_misses += 1;
140        stats.total_allocations += 1;
141
142        let block_id = stats.total_allocations; // Simple ID generation
143        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    /// Deallocate GPU memory block
171    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            // Add to free blocks for reuse
181            free_blocks.push_back(block);
182
183            // Limit free blocks to prevent memory leaks
184            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    /// Get allocation statistics
196    pub fn get_stats(&self) -> AllocationStats {
197        (*self.allocation_stats.lock().expect("lock poisoned")).clone()
198    }
199
200    /// Defragment memory by consolidating free blocks
201    pub fn defragment(&self) -> Result<()> {
202        let mut free_blocks = self.free_blocks.lock().expect("lock poisoned");
203
204        // Sort free blocks by device and size
205        let mut blocks: Vec<_> = free_blocks.drain(..).collect();
206        blocks.sort_by_key(|b| (b.device_id, b.size_bytes));
207
208        // Merge adjacent blocks (simplified implementation)
209        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                    // In a real implementation, we'd check if blocks are adjacent
216                    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
240/// Tensor cache for frequently used tensors
241pub 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/// Cached tensor with metadata
250#[derive(Debug, Clone)]
251struct CachedTensor {
252    data: Array2<f32>, // In real implementation, this would be GPU tensor
253    device_id: usize,
254    last_accessed: Instant,
255    access_count: usize,
256    size_bytes: usize,
257}
258
259/// Cache statistics
260#[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    /// Create new tensor cache
270    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    /// Cache entity tensor.
281    ///
282    /// Locks are always acquired in the canonical order
283    /// `cache_stats -> entity_tensors -> attention_weights ->
284    /// intermediate_activations` to keep eviction and lookups deadlock-free.
285    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        // Actually free memory (evict real entries) before inserting.
301        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        // Replacing an existing entry: reclaim its bytes from the accounting.
311        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    /// Get cached entity tensor
320    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    /// Cache attention weights
339    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    /// Get cached attention weights
372    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    /// Evict genuinely-least-recently-used entries across all three caches until
391    /// the projected memory usage (current + `incoming_bytes`) fits within
392    /// `max_memory`, actually removing the backing tensors rather than merely
393    /// adjusting a statistic. Stops early if every cache is empty (e.g. a single
394    /// tensor larger than the whole budget), leaving the cache momentarily over
395    /// budget for that one oversized item instead of looping forever.
396    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            // Least-recently-used candidate within each cache.
406            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            // Pick the globally oldest entry across the three caches.
420            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                // Nothing left to evict.
444                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    /// Get cache statistics
468    pub fn get_stats(&self) -> CacheStats {
469        (*self.cache_stats.lock().expect("lock poisoned")).clone()
470    }
471
472    /// Clear all caches.
473    ///
474    /// Locks are taken in the canonical order (`cache_stats` first) to stay
475    /// consistent with the eviction and lookup paths.
476    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
494/// Mixed precision training and inference
495pub struct MixedPrecisionProcessor {
496    config: GpuAccelerationConfig,
497    fp16_enabled: bool,
498    loss_scaling: f32,
499    overflow_detection: bool,
500}
501
502impl MixedPrecisionProcessor {
503    /// Create new mixed precision processor
504    pub fn new(config: GpuAccelerationConfig) -> Self {
505        Self {
506            config: config.clone(),
507            fp16_enabled: config.mixed_precision,
508            loss_scaling: 65536.0, // Default loss scaling for FP16
509            overflow_detection: true,
510        }
511    }
512
513    /// Convert tensor to FP16 for computation
514    pub fn to_fp16(&self, tensor: &Array2<f32>) -> Array2<f32> {
515        if !self.fp16_enabled {
516            return tensor.clone();
517        }
518
519        // Simulate FP16 conversion (real implementation would use GPU ops)
520        tensor.mapv(|x| {
521            // Clamp to FP16 range and simulate precision loss
522            let clamped = x.clamp(-65504.0, 65504.0);
523            (clamped * 1024.0).round() / 1024.0 // Simulate FP16 precision
524        })
525    }
526
527    /// Apply loss scaling for gradient computation
528    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    /// Unscale gradients after loss scaling
537    pub fn unscale_gradients(&self, gradients: &mut Array2<f32>) -> bool {
538        if !self.fp16_enabled {
539            return true;
540        }
541
542        // Check for overflow
543        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        // Unscale gradients
552        gradients.mapv_inplace(|x| x / self.loss_scaling);
553        true
554    }
555
556    /// Adjust loss scaling based on overflow detection
557    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            // Gradually increase loss scaling if no overflow
563            self.loss_scaling = (self.loss_scaling * 1.1).min(65536.0);
564        }
565    }
566}
567
568/// Multi-stream processor for parallel GPU operations
569pub struct MultiStreamProcessor {
570    config: GpuAccelerationConfig,
571    pub stream_ids: Vec<usize>,
572    current_stream: usize,
573}
574
575impl MultiStreamProcessor {
576    /// Create new multi-stream processor
577    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    /// Get next available stream
588    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    /// Process embeddings in parallel across multiple streams
595    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        // Collect results from all streams
620        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    /// Synchronize all streams
630    pub fn synchronize_all(&self) {
631        // In real implementation, this would synchronize GPU streams
632        debug!("Synchronized {} GPU streams", self.stream_ids.len());
633    }
634}
635
636/// Main GPU acceleration manager
637pub 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    /// Create new GPU acceleration manager
647    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    /// Get memory pool
663    pub fn memory_pool(&self) -> &GpuMemoryPool {
664        &self.memory_pool
665    }
666
667    /// Get tensor cache
668    pub fn tensor_cache(&self) -> &TensorCache {
669        &self.tensor_cache
670    }
671
672    /// Get mixed precision processor
673    pub fn mixed_precision(&mut self) -> &mut MixedPrecisionProcessor {
674        &mut self.mixed_precision
675    }
676
677    /// Get multi-stream processor
678    pub fn multi_stream(&mut self) -> &mut MultiStreamProcessor {
679        &mut self.multi_stream
680    }
681
682    /// Optimize embedding computation with GPU acceleration
683    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            // Fallback to CPU computation
690            return Ok(entities.iter().map(|e| base_compute_fn(e)).collect());
691        }
692
693        // Use multi-stream processing for parallel computation
694        let results = self
695            .multi_stream
696            .process_batch_parallel(entities, move |entity, stream_id| {
697                // In real implementation, this would use the appropriate GPU stream
698                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    /// Get comprehensive performance stats
708    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/// GPU performance statistics
730#[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
746/// Memory defragmentation utilities
747pub struct MemoryDefragmenter {
748    config: GpuAccelerationConfig,
749    defrag_threshold: f32,
750    last_defrag: Instant,
751    defrag_interval: Duration,
752}
753
754impl MemoryDefragmenter {
755    /// Create new memory defragmenter
756    pub fn new(config: GpuAccelerationConfig) -> Self {
757        Self {
758            config,
759            defrag_threshold: 0.7, // Defrag when 70% fragmented
760            last_defrag: Instant::now(),
761            defrag_interval: Duration::from_secs(300), // Defrag every 5 minutes max
762        }
763    }
764
765    /// Check if defragmentation is needed
766    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    /// Calculate memory fragmentation ratio
775    fn calculate_fragmentation_ratio(&self, stats: &AllocationStats) -> f32 {
776        if stats.current_memory_usage == 0 {
777            return 0.0;
778        }
779
780        // Simplified fragmentation calculation
781        // In real implementation, would analyze actual memory layout
782        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    /// Perform memory defragmentation
793    pub fn defragment(&mut self, memory_pool: &GpuMemoryPool) -> Result<DefragmentationResult> {
794        info!("Starting GPU memory defragmentation");
795        let start_time = Instant::now();
796
797        // In real implementation, would:
798        // 1. Identify fragmented memory regions
799        // 2. Move active allocations to contiguous regions
800        // 3. Release fragmented blocks back to the pool
801
802        // Simulate defragmentation work
803        std::thread::sleep(Duration::from_millis(100));
804
805        let stats_before = memory_pool.get_stats();
806
807        // Simulate memory compaction (in real implementation would actually move memory)
808        // This would involve GPU kernel calls to move data
809
810        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/// Results of memory defragmentation operation
828#[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
836/// Out-of-core processing for handling datasets larger than GPU memory
837pub struct OutOfCoreProcessor {
838    config: GpuAccelerationConfig,
839    chunk_size: usize,
840    overlap_size: usize,
841    memory_limit: usize,
842}
843
844impl OutOfCoreProcessor {
845    /// Create new out-of-core processor
846    pub fn new(config: GpuAccelerationConfig) -> Self {
847        let memory_limit = config.memory_pool_size_mb * 1024 * 1024; // Convert to bytes
848        let chunk_size = memory_limit / 4; // Use 25% of available memory per chunk
849        let overlap_size = chunk_size / 10; // 10% overlap between chunks
850
851        Self {
852            config,
853            chunk_size,
854            overlap_size,
855            memory_limit,
856        }
857    }
858
859    /// Process large embedding batch using out-of-core strategy
860    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        // Calculate optimal chunk size based on data size and memory constraints
873        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); // Between 1 and 1000 items
876
877        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            // Process chunk on GPU
888            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            // Yield control to allow other tasks to run
898            tokio::task::yield_now().await;
899        }
900
901        Ok(results)
902    }
903
904    /// Process with overlapping windows for context-dependent embeddings
905    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            // Handle overlap by only taking non-overlapping results
931            let take_count = if start_idx == 0 {
932                chunk_results.len()
933            } else {
934                // Skip overlap_size results from the beginning
935                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
950/// Dynamic shape handling for variable-size inputs
951pub struct DynamicShapeHandler {
952    config: GpuAccelerationConfig,
953    shape_cache: HashMap<Vec<usize>, ShapeInfo>,
954    max_cached_shapes: usize,
955}
956
957/// Information about tensor shapes for optimization
958#[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    /// Create new dynamic shape handler
968    pub fn new(config: GpuAccelerationConfig) -> Self {
969        Self {
970            config,
971            shape_cache: HashMap::new(),
972            max_cached_shapes: 100,
973        }
974    }
975
976    /// Optimize tensor shapes for GPU processing
977    pub fn optimize_shape(&mut self, shape: Vec<usize>) -> Vec<usize> {
978        // Check cache first
979        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        // Calculate optimal shape based on GPU characteristics
985        let optimized_shape = self.calculate_optimal_shape(&shape);
986
987        // Cache the result
988        self.cache_shape_info(shape.clone(), optimized_shape.clone());
989
990        optimized_shape
991    }
992
993    /// Calculate optimal shape for GPU processing
994    fn calculate_optimal_shape(&self, shape: &[usize]) -> Vec<usize> {
995        let mut optimized = shape.to_vec();
996
997        // Align dimensions to GPU warp/wavefront sizes (typically 32)
998        const WARP_SIZE: usize = 32;
999
1000        for dim in &mut optimized {
1001            if *dim > 0 {
1002                // Round up to next multiple of warp size for better GPU utilization
1003                *dim = ((*dim + WARP_SIZE - 1) / WARP_SIZE) * WARP_SIZE;
1004            }
1005        }
1006
1007        optimized
1008    }
1009
1010    /// Cache shape information
1011    fn cache_shape_info(&mut self, original_shape: Vec<usize>, optimized_shape: Vec<usize>) {
1012        // Evict old entries if cache is full
1013        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; // Assume f32
1018        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    /// Calculate optimal batch size for given memory requirement
1031    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; // Use 50% of available memory
1037        let max_batch_size = available_memory / memory_per_item;
1038
1039        // Clamp to reasonable range
1040        max_batch_size.clamp(1, 1024)
1041    }
1042
1043    /// Evict oldest cached shape
1044    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    /// Get optimal batch size for given shape
1056    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
1064/// Batch size optimizer for maximizing GPU utilization
1065pub struct BatchSizeOptimizer {
1066    config: GpuAccelerationConfig,
1067    performance_history: VecDeque<BatchPerformance>,
1068    max_history_size: usize,
1069    current_optimal_batch_size: usize,
1070}
1071
1072/// Performance metrics for a batch processing operation
1073#[derive(Debug, Clone)]
1074struct BatchPerformance {
1075    batch_size: usize,
1076    processing_time: Duration,
1077    memory_usage: usize,
1078    throughput: f64, // items per second
1079    gpu_utilization: f64,
1080    timestamp: Instant,
1081}
1082
1083impl BatchSizeOptimizer {
1084    /// Create new batch size optimizer
1085    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, // Conservative starting point
1091        }
1092    }
1093
1094    /// Find optimal batch size through adaptive testing
1095    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            // Test this batch size
1121            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            // Add to performance history
1142            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            // Small delay between tests
1148            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    /// Test performance of a specific batch size
1158    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        // Process the batch
1171        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        // Calculate throughput
1178        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        // Estimate GPU utilization (simplified)
1185        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    /// Estimate current memory usage
1198    fn estimate_memory_usage(&self) -> usize {
1199        // In real implementation, would query actual GPU memory usage
1200        // For simulation, return a reasonable estimate
1201        (self.config.memory_pool_size_mb * 1024 * 1024) / 4 // Assume 25% usage
1202    }
1203
1204    /// Estimate GPU utilization based on batch size and processing time
1205    fn estimate_gpu_utilization(&self, batch_size: usize, processing_time: Duration) -> f64 {
1206        // Simplified model: larger batches generally improve utilization up to a point
1207        let base_utilization = (batch_size as f64).log2() / 10.0; // Log scale
1208        let time_factor = if processing_time.as_millis() < 10 {
1209            0.5 // Very fast suggests underutilization
1210        } else if processing_time.as_millis() > 1000 {
1211            0.7 // Very slow might indicate bottlenecks
1212        } else {
1213            1.0
1214        };
1215
1216        (base_utilization * time_factor).clamp(0.0, 1.0)
1217    }
1218
1219    /// Get current optimal batch size
1220    pub fn get_optimal_batch_size(&self) -> usize {
1221        self.current_optimal_batch_size
1222    }
1223
1224    /// Get performance statistics
1225    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/// Statistics from batch size optimization
1256#[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    /// Regression: `TensorCache` eviction previously only adjusted a statistic
1269    /// and never removed backing tensors, so real memory grew without bound. It
1270    /// must now genuinely drop entries and keep the map size bounded.
1271    #[test]
1272    fn regression_tensor_cache_evicts_real_entries() {
1273        // Tiny 1 MB budget so a handful of tensors forces eviction.
1274        let config = GpuAccelerationConfig {
1275            cache_size_mb: 1,
1276            ..GpuAccelerationConfig::default()
1277        };
1278        let cache = TensorCache::new(config);
1279
1280        // Each tensor is 256 * 256 * 4 bytes = 256 KiB; inserting 40 of them
1281        // (~10 MiB) vastly exceeds the 1 MiB budget.
1282        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        // Real memory must be bounded and evictions must have occurred.
1290        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        // The most recently inserted entry must still be present; an old one
1299        // must have been dropped (bounded backing store).
1300        assert!(cache.get_entity_tensor("entity_39").is_some());
1301        assert!(cache.get_entity_tensor("entity_0").is_none());
1302    }
1303
1304    /// Regression: the mixed-precision path used to be a no-op branch identical
1305    /// to full precision. `round_f32_to_bf16` must genuinely reduce mantissa
1306    /// precision so the flag produces different numerics.
1307    #[test]
1308    fn regression_bf16_rounding_reduces_precision() {
1309        // A value whose low mantissa bits are dropped by bf16 rounding.
1310        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        // But it must stay close (same magnitude / exponent).
1317        assert!((rounded - x).abs() < 0.01);
1318
1319        // Exactly-representable bf16 values are unchanged.
1320        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        // NaN stays NaN.
1324        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        // Should reuse the block
1348        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        // Use a value that will definitely cause precision loss in FP16 simulation
1372        let tensor = Array2::from_elem((2, 2), 1.0001);
1373        let fp16_tensor = processor.to_fp16(&tensor);
1374
1375        if processor.fp16_enabled {
1376            // Should have some precision loss in FP16 simulation
1377            assert!(fp16_tensor[[0, 0]] != tensor[[0, 0]]);
1378        } else {
1379            // If FP16 is disabled, values should be identical
1380            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        // Test initialization - skip if no GPU available
1404        let config = GpuAccelerationConfig::default();
1405
1406        match SciRS2GpuAccelerator::new(config) {
1407            Ok(accelerator) => {
1408                // Verify initialization if GPU is available
1409                assert!(accelerator.num_devices() > 0);
1410            }
1411            Err(_) => {
1412                // Skip test if no GPU hardware is available
1413                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        // Skip test if no GPU available
1423        if let Ok(accelerator) = SciRS2GpuAccelerator::new(config) {
1424            // Test matrix dimensions
1425            let _matrix_a = Array2::<f32>::ones((256, 512));
1426            let _matrix_b = Array2::<f32>::ones((512, 256));
1427
1428            // This would use tensor cores in production
1429            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
1437/// GPU-gated accelerator built on SciRS2's GPU device abstractions.
1438///
1439/// IMPORTANT — current execution model: constructing this type validates that
1440/// at least one GPU device/context is available (see [`SciRS2GpuAccelerator::new`],
1441/// which returns an error when no device can be initialized), but the numeric
1442/// kernels below (`tensor_core_gemm`, `batch_embed`, `simd_similarity`) currently
1443/// execute on the **CPU**. Device-side kernel dispatch through the held
1444/// [`GpuContext`]s is not yet wired. These methods are therefore honest CPU
1445/// implementations rather than GPU/tensor-core execution:
1446/// - `tensor_core_gemm` performs an f32 matmul; with mixed precision enabled it
1447///   genuinely rounds operands to bfloat16 precision before multiplying and
1448///   accumulates in f32, mirroring tensor-core numerics on the CPU.
1449/// - `batch_embed` / `simd_similarity` perform standard CPU linear algebra.
1450///
1451/// The retained `contexts` field records the validated devices and gates
1452/// construction; it is the anchor point for future real GPU dispatch.
1453pub struct SciRS2GpuAccelerator {
1454    config: GpuAccelerationConfig,
1455    contexts: Vec<GpuContext>,
1456    operations: Arc<AtomicUsize>,
1457}
1458
1459/// Round an `f32` to bfloat16 precision (round-to-nearest-even) and back to
1460/// `f32`. bfloat16 keeps the f32 exponent but truncates the mantissa from 23 to
1461/// 7 bits; this is a pure-Rust emulation of the reduced-precision operands used
1462/// by hardware tensor cores, so the mixed-precision code path produces genuinely
1463/// different (lower-precision) numerics rather than being a no-op branch.
1464#[inline]
1465fn round_f32_to_bf16(x: f32) -> f32 {
1466    let bits = x.to_bits();
1467    // NaN: keep as-is (avoid turning a NaN into infinity via rounding).
1468    if x.is_nan() {
1469        return x;
1470    }
1471    // Round-to-nearest-even on the 16-bit boundary.
1472    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    /// Create new SciRS2 GPU accelerator
1479    pub fn new(config: GpuAccelerationConfig) -> Result<Self> {
1480        let mut contexts = Vec::new();
1481
1482        // Initialize GPU contexts for each device
1483        // Note: This uses a default backend since device IDs are configuration-specific
1484        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    /// Get number of available GPU devices
1508    pub fn num_devices(&self) -> usize {
1509        self.contexts.len()
1510    }
1511
1512    /// Matrix multiplication, optionally in emulated mixed precision.
1513    ///
1514    /// CPU implementation (see the type-level note). When `use_mixed_precision`
1515    /// is requested and enabled in the config, both operands are rounded to
1516    /// bfloat16 precision before the product and the result is accumulated in
1517    /// f32 — a faithful CPU emulation of tensor-core mixed-precision numerics,
1518    /// not a no-op branch. The inner dimensions must be compatible; a mismatch
1519    /// is reported as an error rather than panicking.
1520    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            // Genuine reduced-precision operands (bf16), f32 accumulation.
1536            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    /// Batch embedding lookup: `embedding_matrix · input` for each input.
1549    ///
1550    /// CPU implementation (see the type-level note). Each input's dimension must
1551    /// match the embedding matrix's column count; a mismatch is reported as an
1552    /// error rather than panicking on the request path.
1553    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    /// Dot-product similarity of `query` against each candidate.
1580    ///
1581    /// CPU implementation (see the type-level note). All candidates must share
1582    /// the query's dimension; a mismatch is reported as an error rather than
1583    /// panicking.
1584    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    /// Get acceleration statistics
1610    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    /// Clear profiling data
1619    pub fn clear_stats(&self) {
1620        self.operations.store(0, Ordering::Relaxed);
1621    }
1622}
1623
1624/// Statistics for GPU accelerator
1625#[derive(Debug, Clone)]
1626pub struct AcceleratorStats {
1627    pub total_operations: usize,
1628    pub num_devices: usize,
1629    pub profiler_report: String,
1630}