Skip to main content

kizzasi_core/
gpu_utils.rs

1//! GPU memory management and tensor transfer utilities
2//!
3//! Provides efficient utilities for:
4//! - Moving tensors between CPU and GPU
5//! - Memory pooling for GPU tensors
6//! - Batch transfer operations
7//! - Memory usage tracking
8//!
9//! # Examples
10//!
11//! ```rust
12//! use kizzasi_core::gpu_utils::{TensorTransfer, TransferBatch};
13//! use kizzasi_core::device::DeviceConfig;
14//! use candle_core::{Tensor, Device};
15//!
16//! # fn example() -> Result<(), Box<dyn std::error::Error>> {
17//! // Transfer single tensor to GPU
18//! let cpu_tensor = Tensor::zeros((100, 100), candle_core::DType::F32, &Device::Cpu)?;
19//! let gpu_device = DeviceConfig::default().create_device()?;
20//!
21//! let gpu_tensor = TensorTransfer::to_device(&cpu_tensor, &gpu_device)?;
22//!
23//! // Batch transfer multiple tensors
24//! let tensors = vec![cpu_tensor.clone(), cpu_tensor.clone()];
25//! let gpu_tensors = TransferBatch::transfer_all(&tensors, &gpu_device)?;
26//! # Ok(())
27//! # }
28//! ```
29
30use crate::error::{CoreError, CoreResult};
31use candle_core::{Device, Tensor};
32use std::collections::HashMap;
33
34/// Tensor transfer utilities for CPU/GPU operations
35pub struct TensorTransfer;
36
37impl TensorTransfer {
38    /// Transfer a tensor to a specific device
39    ///
40    /// # Arguments
41    /// * `tensor` - The tensor to transfer
42    /// * `device` - Target device
43    ///
44    /// # Returns
45    /// Tensor on the target device
46    pub fn to_device(tensor: &Tensor, device: &Device) -> CoreResult<Tensor> {
47        tensor
48            .to_device(device)
49            .map_err(|e| CoreError::DeviceError(format!("Failed to transfer tensor: {}", e)))
50    }
51
52    /// Transfer a tensor to CPU
53    pub fn to_cpu(tensor: &Tensor) -> CoreResult<Tensor> {
54        Self::to_device(tensor, &Device::Cpu)
55    }
56
57    /// Transfer a tensor to GPU (auto-detect best GPU)
58    pub fn to_gpu(tensor: &Tensor) -> CoreResult<Tensor> {
59        let device = crate::device::get_best_device();
60        if matches!(device, Device::Cpu) {
61            return Err(CoreError::DeviceError(
62                "No GPU device available".to_string(),
63            ));
64        }
65        Self::to_device(tensor, &device)
66    }
67
68    /// Check if a tensor is on GPU
69    pub fn is_on_gpu(tensor: &Tensor) -> bool {
70        !matches!(tensor.device(), Device::Cpu)
71    }
72
73    /// Check if a tensor is on CPU
74    pub fn is_on_cpu(tensor: &Tensor) -> bool {
75        matches!(tensor.device(), Device::Cpu)
76    }
77
78    /// Get device of a tensor
79    pub fn get_device(tensor: &Tensor) -> Device {
80        tensor.device().clone()
81    }
82}
83
84/// Batch tensor transfer operations
85pub struct TransferBatch;
86
87impl TransferBatch {
88    /// Transfer multiple tensors to a device in batch
89    ///
90    /// This is more efficient than transferring tensors one by one
91    /// as it can leverage async transfers on some backends.
92    pub fn transfer_all(tensors: &[Tensor], device: &Device) -> CoreResult<Vec<Tensor>> {
93        tensors
94            .iter()
95            .map(|t| TensorTransfer::to_device(t, device))
96            .collect()
97    }
98
99    /// Transfer all tensors to CPU
100    pub fn to_cpu_all(tensors: &[Tensor]) -> CoreResult<Vec<Tensor>> {
101        Self::transfer_all(tensors, &Device::Cpu)
102    }
103
104    /// Transfer all tensors to GPU
105    pub fn to_gpu_all(tensors: &[Tensor]) -> CoreResult<Vec<Tensor>> {
106        let device = crate::device::get_best_device();
107        if matches!(device, Device::Cpu) {
108            return Err(CoreError::DeviceError(
109                "No GPU device available".to_string(),
110            ));
111        }
112        Self::transfer_all(tensors, &device)
113    }
114}
115
116/// Memory usage tracking for GPU tensors
117#[derive(Debug, Clone)]
118pub struct MemoryStats {
119    /// Total memory allocated (bytes)
120    pub total_allocated: usize,
121    /// Number of tensors tracked
122    pub tensor_count: usize,
123    /// Memory by tensor name
124    pub memory_by_name: HashMap<String, usize>,
125}
126
127impl MemoryStats {
128    /// Create a new memory stats tracker
129    pub fn new() -> Self {
130        Self {
131            total_allocated: 0,
132            tensor_count: 0,
133            memory_by_name: HashMap::new(),
134        }
135    }
136
137    /// Track a tensor's memory usage
138    pub fn track_tensor(&mut self, name: String, tensor: &Tensor) {
139        let size = Self::tensor_size(tensor);
140        self.total_allocated += size;
141        self.tensor_count += 1;
142        self.memory_by_name.insert(name, size);
143    }
144
145    /// Untrack a tensor
146    pub fn untrack_tensor(&mut self, name: &str) {
147        if let Some(size) = self.memory_by_name.remove(name) {
148            self.total_allocated = self.total_allocated.saturating_sub(size);
149            self.tensor_count = self.tensor_count.saturating_sub(1);
150        }
151    }
152
153    /// Get total allocated memory in bytes
154    pub fn total_bytes(&self) -> usize {
155        self.total_allocated
156    }
157
158    /// Get total allocated memory in MB
159    pub fn total_mb(&self) -> f64 {
160        self.total_allocated as f64 / (1024.0 * 1024.0)
161    }
162
163    /// Get total allocated memory in GB
164    pub fn total_gb(&self) -> f64 {
165        self.total_allocated as f64 / (1024.0 * 1024.0 * 1024.0)
166    }
167
168    /// Calculate tensor size in bytes
169    fn tensor_size(tensor: &Tensor) -> usize {
170        let elem_count: usize = tensor.dims().iter().product();
171        let dtype_size = match tensor.dtype() {
172            candle_core::DType::U8 => 1,
173            candle_core::DType::I16 => 2,
174            candle_core::DType::U32 => 4,
175            candle_core::DType::I32 => 4,
176            candle_core::DType::I64 => 8,
177            candle_core::DType::BF16 => 2,
178            candle_core::DType::F16 => 2,
179            candle_core::DType::F32 => 4,
180            candle_core::DType::F64 => 8,
181            // 8-bit float formats: 1 byte per element
182            candle_core::DType::F8E4M3 => 1,
183            candle_core::DType::F8E8M0 => 1,
184            // Sub-byte formats: round up to 1 byte per element for memory tracking purposes
185            // (actual storage is packed, but we conservatively over-count here)
186            candle_core::DType::F6E2M3 => 1,
187            candle_core::DType::F6E3M2 => 1,
188            candle_core::DType::F4 => 1,
189            // Unknown future variants from #[non_exhaustive] DType; not tracked
190            _ => 0,
191        };
192        elem_count * dtype_size
193    }
194
195    /// Clear all tracked tensors
196    pub fn clear(&mut self) {
197        self.total_allocated = 0;
198        self.tensor_count = 0;
199        self.memory_by_name.clear();
200    }
201}
202
203impl Default for MemoryStats {
204    fn default() -> Self {
205        Self::new()
206    }
207}
208
209/// GPU memory pool for efficient tensor allocation
210pub struct GPUMemoryPool {
211    device: Device,
212    stats: MemoryStats,
213}
214
215impl GPUMemoryPool {
216    /// Create a new GPU memory pool
217    pub fn new(device: Device) -> Self {
218        Self {
219            device,
220            stats: MemoryStats::new(),
221        }
222    }
223
224    /// Allocate a tensor on GPU
225    pub fn allocate(
226        &mut self,
227        name: String,
228        shape: &[usize],
229        dtype: candle_core::DType,
230    ) -> CoreResult<Tensor> {
231        let tensor = Tensor::zeros(shape, dtype, &self.device)
232            .map_err(|e| CoreError::DeviceError(format!("Failed to allocate tensor: {}", e)))?;
233
234        self.stats.track_tensor(name, &tensor);
235        Ok(tensor)
236    }
237
238    /// Release a tensor from the pool
239    pub fn release(&mut self, name: &str) {
240        self.stats.untrack_tensor(name);
241    }
242
243    /// Get memory statistics
244    pub fn stats(&self) -> &MemoryStats {
245        &self.stats
246    }
247
248    /// Get device
249    pub fn device(&self) -> &Device {
250        &self.device
251    }
252}
253
254/// Prefetching utilities for optimizing data transfer
255pub struct TensorPrefetch;
256
257impl TensorPrefetch {
258    /// Prefetch tensor to device (async hint for backends that support it)
259    ///
260    /// Note: This is a hint to the backend. Actual async behavior depends on
261    /// the device backend (CUDA/Metal).
262    pub fn prefetch(tensor: &Tensor, device: &Device) -> CoreResult<Tensor> {
263        // For now, this is synchronous. Future implementations could leverage
264        // async streams on CUDA or Metal command buffers
265        TensorTransfer::to_device(tensor, device)
266    }
267
268    /// Prefetch multiple tensors
269    pub fn prefetch_batch(tensors: &[Tensor], device: &Device) -> CoreResult<Vec<Tensor>> {
270        TransferBatch::transfer_all(tensors, device)
271    }
272}
273
274#[cfg(test)]
275mod tests {
276    use super::*;
277    use candle_core::DType;
278
279    #[test]
280    fn test_tensor_transfer_to_cpu() {
281        let tensor = Tensor::zeros((10, 10), DType::F32, &Device::Cpu).unwrap();
282        let cpu_tensor = TensorTransfer::to_cpu(&tensor).unwrap();
283
284        assert!(TensorTransfer::is_on_cpu(&cpu_tensor));
285        assert!(!TensorTransfer::is_on_gpu(&cpu_tensor));
286    }
287
288    #[test]
289    fn test_batch_transfer() {
290        let tensors = vec![
291            Tensor::zeros((5, 5), DType::F32, &Device::Cpu).unwrap(),
292            Tensor::zeros((10, 10), DType::F32, &Device::Cpu).unwrap(),
293        ];
294
295        let cpu_tensors = TransferBatch::to_cpu_all(&tensors).unwrap();
296        assert_eq!(cpu_tensors.len(), 2);
297
298        for tensor in &cpu_tensors {
299            assert!(TensorTransfer::is_on_cpu(tensor));
300        }
301    }
302
303    #[test]
304    fn test_memory_stats() {
305        let mut stats = MemoryStats::new();
306
307        let tensor1 = Tensor::zeros((10, 10), DType::F32, &Device::Cpu).unwrap();
308        let tensor2 = Tensor::zeros((20, 20), DType::F32, &Device::Cpu).unwrap();
309
310        stats.track_tensor("tensor1".to_string(), &tensor1);
311        stats.track_tensor("tensor2".to_string(), &tensor2);
312
313        assert_eq!(stats.tensor_count, 2);
314        // 10*10*4 + 20*20*4 = 400 + 1600 = 2000 bytes
315        assert_eq!(stats.total_bytes(), 2000);
316
317        stats.untrack_tensor("tensor1");
318        assert_eq!(stats.tensor_count, 1);
319        assert_eq!(stats.total_bytes(), 1600);
320
321        stats.clear();
322        assert_eq!(stats.tensor_count, 0);
323        assert_eq!(stats.total_bytes(), 0);
324    }
325
326    #[test]
327    fn test_memory_stats_mb_gb() {
328        let mut stats = MemoryStats::new();
329
330        // Create a large tensor: 1000 * 1000 * f32 (4 bytes) = 4,000,000 bytes
331        let tensor = Tensor::zeros((1000, 1000), DType::F32, &Device::Cpu).unwrap();
332        stats.track_tensor("large_tensor".to_string(), &tensor);
333
334        // 4,000,000 bytes = 3.814 MB (1024^2) or 4.0 MB (1000^2)
335        // Using 1024-based calculation: 4,000,000 / (1024 * 1024) ≈ 3.814 MB
336        let expected_mb = 4_000_000.0 / (1024.0 * 1024.0);
337        assert!((stats.total_mb() - expected_mb).abs() < 0.01);
338
339        let expected_gb = 4_000_000.0 / (1024.0 * 1024.0 * 1024.0);
340        assert!((stats.total_gb() - expected_gb).abs() < 0.0001);
341    }
342
343    #[test]
344    fn test_gpu_memory_pool() {
345        let mut pool = GPUMemoryPool::new(Device::Cpu);
346
347        let tensor = pool
348            .allocate("test_tensor".to_string(), &[100, 100], DType::F32)
349            .unwrap();
350
351        assert_eq!(tensor.dims(), &[100, 100]);
352        assert_eq!(pool.stats().tensor_count, 1);
353        assert_eq!(pool.stats().total_bytes(), 100 * 100 * 4);
354
355        pool.release("test_tensor");
356        assert_eq!(pool.stats().tensor_count, 0);
357    }
358
359    #[test]
360    fn test_get_device() {
361        let tensor = Tensor::zeros((10, 10), DType::F32, &Device::Cpu).unwrap();
362        let device = TensorTransfer::get_device(&tensor);
363        assert!(matches!(device, Device::Cpu));
364    }
365
366    #[test]
367    fn test_tensor_size_calculation() {
368        let tensor_f32 = Tensor::zeros((10, 20), DType::F32, &Device::Cpu).unwrap();
369        assert_eq!(MemoryStats::tensor_size(&tensor_f32), 10 * 20 * 4);
370
371        let tensor_f16 = Tensor::zeros((10, 20), DType::F16, &Device::Cpu).unwrap();
372        assert_eq!(MemoryStats::tensor_size(&tensor_f16), 10 * 20 * 2);
373
374        let tensor_i64 = Tensor::zeros((5, 5), DType::I64, &Device::Cpu).unwrap();
375        assert_eq!(MemoryStats::tensor_size(&tensor_i64), 5 * 5 * 8);
376    }
377}