Skip to main content

optirs_core/
gpu_optimizer.rs

1//! GPU optimizer scaffolding
2//!
3//! # Status: no GPU backend is wired up yet
4//!
5//! This module defines the API surface for GPU-accelerated optimization, but **no
6//! device backend is currently implemented**. [`GpuUtils::detect_backends`] returns
7//! an empty list, [`GpuUtils::device_count`] returns `0`, and consequently
8//! [`GpuOptimizer::is_gpu_available`] reports `false` and every optimization step
9//! executes on the CPU through the wrapped base optimizer.
10//!
11//! The wrapper is still useful today: it lets calling code be written once against
12//! the GPU-aware API and keep working unchanged when a backend lands. It will not,
13//! however, make anything faster right now — treat it as a compatibility shim, not
14//! as an accelerator.
15//!
16//! # What a real backend must provide
17//!
18//! When SciRS2's GPU abstractions become available, the integration points are:
19//! - `scirs2_core::gpu::GpuContext` for GPU context management
20//! - `scirs2_core::gpu::GpuBuffer` for GPU memory allocation
21//! - `scirs2_core::gpu::GpuKernel` for GPU kernel execution
22//! - `scirs2_core::tensor_cores` for mixed-precision optimization
23//! - `scirs2_core::array_protocol::GPUArray` for the GPU array interface
24//!
25//! Wiring those up means implementing [`GpuUtils::detect_backends`],
26//! [`GpuUtils::device_count`] and `GpuOptimizer::step_gpu`; the availability
27//! reporting below then becomes truthful automatically.
28
29use scirs2_core::ndarray::{Array1, ArrayView1, ScalarOperand};
30use scirs2_core::numeric::Float;
31use std::fmt::Debug;
32use std::marker::PhantomData;
33
34use crate::error::Result;
35use crate::optimizers::Optimizer;
36
37/// GPU optimizer configuration
38#[derive(Debug, Clone)]
39pub struct GpuConfig {
40    /// Enable tensor core acceleration
41    pub use_tensor_cores: bool,
42    /// Enable mixed-precision training (FP16/FP32)
43    pub use_mixed_precision: bool,
44    /// Preferred GPU backend (auto-detected if None)
45    pub preferred_backend: Option<String>,
46    /// Maximum GPU memory usage (bytes)
47    pub max_gpu_memory: Option<usize>,
48    /// Enable GPU memory tracking
49    pub track_memory: bool,
50}
51
52impl Default for GpuConfig {
53    fn default() -> Self {
54        Self {
55            use_tensor_cores: true,
56            use_mixed_precision: false,
57            preferred_backend: None,
58            max_gpu_memory: None,
59            track_memory: true,
60        }
61    }
62}
63
64/// GPU-accelerated optimizer wrapper
65///
66/// Wraps any CPU optimizer to provide GPU acceleration using SciRS2's GPU abstractions.
67/// Automatically handles host-device data transfer and GPU memory management.
68///
69/// # Examples
70///
71/// ```
72/// use optirs_core::optimizers::SGD;
73/// use optirs_core::gpu_optimizer::{GpuOptimizer, GpuConfig};
74/// use scirs2_core::ndarray::Array1;
75///
76/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
77/// let optimizer = SGD::new(0.01);
78/// let config = GpuConfig::default();
79///
80/// // Create GPU-accelerated optimizer
81/// let mut gpu_opt = GpuOptimizer::new(optimizer, config)?;
82///
83/// // Use like a normal optimizer - GPU acceleration is automatic
84/// let params = Array1::from_vec(vec![1.0, 2.0, 3.0]);
85/// let grads = Array1::from_vec(vec![0.1, 0.2, 0.3]);
86///
87/// let updated = gpu_opt.step(&params, &grads)?;
88/// # Ok(())
89/// # }
90/// ```
91pub struct GpuOptimizer<O, A>
92where
93    O: Optimizer<A, scirs2_core::ndarray::Ix1>,
94    A: Float + ScalarOperand + Debug,
95{
96    /// Base CPU optimizer
97    base_optimizer: O,
98    /// GPU configuration
99    config: GpuConfig,
100    /// GPU context (lazily initialized)
101    gpu_context: Option<GpuContextWrapper>,
102    /// Phantom data for type parameter
103    _phantom: PhantomData<A>,
104}
105
106/// Wrapper for GPU context to handle initialization
107struct GpuContextWrapper {
108    /// Whether a GPU backend was actually found and initialized
109    available: bool,
110    /// GPU backend name (CUDA, Metal, OpenCL, WebGPU); `None` when unavailable
111    backend: Option<String>,
112}
113
114impl<O, A> GpuOptimizer<O, A>
115where
116    O: Optimizer<A, scirs2_core::ndarray::Ix1> + Clone,
117    A: Float + ScalarOperand + Debug,
118{
119    /// Creates a new GPU-accelerated optimizer
120    ///
121    /// # Arguments
122    ///
123    /// * `base_optimizer` - The CPU optimizer to accelerate
124    /// * `config` - GPU configuration settings
125    ///
126    /// # Returns
127    ///
128    /// A GPU-accelerated optimizer or an error if GPU initialization fails
129    pub fn new(base_optimizer: O, config: GpuConfig) -> Result<Self> {
130        // Initialize GPU context
131        let gpu_context = Self::initialize_gpu(&config)?;
132
133        Ok(Self {
134            base_optimizer,
135            config,
136            gpu_context: Some(gpu_context),
137            _phantom: PhantomData,
138        })
139    }
140
141    /// Creates a new GPU optimizer with default configuration
142    pub fn with_default_config(base_optimizer: O) -> Result<Self> {
143        Self::new(base_optimizer, GpuConfig::default())
144    }
145
146    /// Initialize the GPU context, if a backend is actually present
147    ///
148    /// No backend is implemented yet, so this reports "unavailable" and the optimizer
149    /// transparently runs on the CPU. Construction still succeeds: an unavailable GPU
150    /// is a supported configuration, not an error.
151    fn initialize_gpu(config: &GpuConfig) -> Result<GpuContextWrapper> {
152        let available_backends = GpuUtils::detect_backends();
153
154        let backend = match &config.preferred_backend {
155            // A specific backend was requested: honour it only if it is present.
156            Some(preferred) => available_backends
157                .iter()
158                .find(|candidate| candidate.eq_ignore_ascii_case(preferred))
159                .cloned(),
160            // Otherwise take whatever the platform offers first.
161            None => available_backends.first().cloned(),
162        };
163
164        Ok(GpuContextWrapper {
165            available: backend.is_some() && GpuUtils::device_count() > 0,
166            backend,
167        })
168    }
169
170    /// Perform GPU-accelerated optimization step
171    ///
172    /// # Arguments
173    ///
174    /// * `params` - Current parameters
175    /// * `gradients` - Gradients
176    ///
177    /// # Returns
178    ///
179    /// Updated parameters after GPU-accelerated optimization
180    pub fn step(&mut self, params: &Array1<A>, gradients: &Array1<A>) -> Result<Array1<A>> {
181        // Use the device path only when a backend really exists. Today this is never
182        // taken; the CPU fallback below is the executed path.
183        let gpu_ready = self
184            .gpu_context
185            .as_ref()
186            .map(|ctx| ctx.available)
187            .unwrap_or(false);
188
189        if gpu_ready {
190            return self.step_gpu(params, gradients);
191        }
192
193        self.base_optimizer.step(params, gradients)
194    }
195
196    /// Device-side step implementation
197    ///
198    /// Unimplemented: a real version would transfer `params`/`gradients` into
199    /// `scirs2_core::gpu::GpuBuffer`s, run a `scirs2_core::gpu::GpuKernel` (optionally
200    /// through `scirs2_core::tensor_cores`), copy the result back and account the
201    /// memory. Until then this returns an error rather than pretending the work
202    /// happened on a device.
203    fn step_gpu(&mut self, _params: &Array1<A>, _gradients: &Array1<A>) -> Result<Array1<A>> {
204        Err(crate::error::OptimError::InvalidConfig(
205            "GPU execution path is not implemented; no device backend is available".to_string(),
206        ))
207    }
208
209    /// Transfer array to GPU
210    ///
211    /// Unimplemented: returns an error because there is no device to copy to.
212    /// A real version would use `scirs2_core::gpu::GpuBuffer::from_slice()`.
213    pub fn to_gpu(&self, _data: &ArrayView1<A>) -> Result<()> {
214        Err(crate::error::OptimError::InvalidConfig(
215            "GPU transfer is not implemented; no device backend is available".to_string(),
216        ))
217    }
218
219    /// Transfer array from GPU
220    ///
221    /// Unimplemented: returns an error because there is no device to copy from.
222    /// A real version would use `scirs2_core::gpu::GpuBuffer::to_host()`.
223    pub fn from_gpu(&self) -> Result<Array1<A>> {
224        Err(crate::error::OptimError::InvalidConfig(
225            "GPU transfer is not implemented; no device backend is available".to_string(),
226        ))
227    }
228
229    /// Check whether a GPU backend is actually available and initialized
230    ///
231    /// Currently always `false`: no device backend is implemented, so all work runs
232    /// on the CPU. This is guaranteed to agree with
233    /// [`GpuUtils::device_count`] / [`GpuUtils::detect_backends`].
234    pub fn is_gpu_available(&self) -> bool {
235        self.gpu_context
236            .as_ref()
237            .map(|ctx| ctx.available)
238            .unwrap_or(false)
239    }
240
241    /// Get the active GPU backend name, or `None` when running on the CPU
242    pub fn gpu_backend(&self) -> Option<&str> {
243        self.gpu_context
244            .as_ref()
245            .and_then(|ctx| ctx.backend.as_deref())
246    }
247
248    /// Get GPU configuration
249    pub fn config(&self) -> &GpuConfig {
250        &self.config
251    }
252
253    /// Enable/disable tensor core acceleration
254    pub fn set_use_tensor_cores(&mut self, enable: bool) {
255        self.config.use_tensor_cores = enable;
256    }
257
258    /// Enable/disable mixed-precision training
259    pub fn set_use_mixed_precision(&mut self, enable: bool) {
260        self.config.use_mixed_precision = enable;
261    }
262
263    /// Get estimated GPU memory usage for given parameter count
264    pub fn estimate_gpu_memory(
265        num_params: usize,
266        dtype_size: usize,
267        optimizer_states: usize,
268    ) -> usize {
269        // Parameters + gradients + optimizer states
270        num_params * dtype_size * (2 + optimizer_states)
271    }
272}
273
274/// GPU memory statistics
275#[derive(Debug, Clone)]
276pub struct GpuMemoryStats {
277    /// Total GPU memory (bytes)
278    pub total: usize,
279    /// Used GPU memory (bytes)
280    pub used: usize,
281    /// Free GPU memory (bytes)
282    pub free: usize,
283    /// Memory used by optimizer (bytes)
284    pub optimizer_usage: usize,
285}
286
287impl GpuMemoryStats {
288    /// Create memory stats
289    pub fn new(total: usize, used: usize) -> Self {
290        Self {
291            total,
292            used,
293            free: total.saturating_sub(used),
294            optimizer_usage: 0,
295        }
296    }
297
298    /// Get memory utilization percentage
299    pub fn utilization_percent(&self) -> f64 {
300        if self.total == 0 {
301            0.0
302        } else {
303            (self.used as f64 / self.total as f64) * 100.0
304        }
305    }
306}
307
308/// GPU optimizer utilities
309pub struct GpuUtils;
310
311impl GpuUtils {
312    /// Detect available GPU backends
313    ///
314    /// Returns the list of usable backends (CUDA, Metal, OpenCL, WebGPU).
315    ///
316    /// No backend is implemented yet, so this is always empty. It must stay empty
317    /// until a backend can genuinely execute kernels — reporting a phantom "auto"
318    /// backend would make [`GpuOptimizer::is_gpu_available`] lie.
319    pub fn detect_backends() -> Vec<String> {
320        // Note: Full implementation would use scirs2_core::gpu::detect_backends()
321        Vec::new()
322    }
323
324    /// Check if tensor cores are available
325    pub fn has_tensor_cores() -> bool {
326        // Note: Full implementation would use scirs2_core::tensor_cores::is_available()
327        false
328    }
329
330    /// Get GPU device count
331    pub fn device_count() -> usize {
332        // Note: Full implementation would use scirs2_core::gpu::device_count()
333        0
334    }
335
336    /// Get GPU memory stats for device
337    pub fn memory_stats(device_id: usize) -> Result<GpuMemoryStats> {
338        // Note: Full implementation would use scirs2_core::gpu::get_memory_info()
339        let _ = device_id;
340        Ok(GpuMemoryStats::new(0, 0))
341    }
342
343    /// Synchronize GPU operations
344    pub fn synchronize() -> Result<()> {
345        // Note: Full implementation would use scirs2_core::gpu::synchronize()
346        Ok(())
347    }
348}
349
350#[cfg(test)]
351mod tests {
352    use super::*;
353    use crate::optimizers::SGD;
354    use scirs2_core::ndarray::Array1;
355
356    #[test]
357    fn test_gpu_config_default() {
358        let config = GpuConfig::default();
359        assert!(config.use_tensor_cores);
360        assert!(!config.use_mixed_precision);
361        assert!(config.track_memory);
362    }
363
364    #[test]
365    fn test_gpu_optimizer_creation() {
366        let optimizer = SGD::new(0.01);
367        let config = GpuConfig::default();
368        let gpu_opt = GpuOptimizer::new(optimizer, config);
369        assert!(gpu_opt.is_ok());
370    }
371
372    #[test]
373    fn test_gpu_optimizer_with_default_config() {
374        let optimizer = SGD::new(0.01);
375        let gpu_opt = GpuOptimizer::with_default_config(optimizer);
376        assert!(gpu_opt.is_ok());
377    }
378
379    #[test]
380    fn test_gpu_optimizer_step() {
381        let optimizer = SGD::new(0.01);
382        let mut gpu_opt = GpuOptimizer::with_default_config(optimizer)
383            .expect("GpuOptimizer::with_default_config succeeds in test_gpu_optimizer_step");
384
385        let params = Array1::from_vec(vec![1.0, 2.0, 3.0]);
386        let grads = Array1::from_vec(vec![0.1, 0.2, 0.3]);
387
388        let result = gpu_opt.step(&params, &grads);
389        assert!(result.is_ok());
390    }
391
392    /// Regression test: availability reporting must match the actual device inventory.
393    ///
394    /// `is_gpu_available()` used to return `true` unconditionally while
395    /// `device_count()` returned `0` and every step ran on the CPU.
396    #[test]
397    fn test_gpu_availability_matches_device_inventory() {
398        let optimizer = SGD::new(0.01);
399        let gpu_opt = GpuOptimizer::with_default_config(optimizer).expect("GpuOptimizer::with_default_config succeeds in test_gpu_availability_matches_device_inventory");
400
401        let devices = GpuUtils::device_count();
402        let backends = GpuUtils::detect_backends();
403
404        assert_eq!(
405            gpu_opt.is_gpu_available(),
406            devices > 0 && !backends.is_empty(),
407            "availability must agree with the detected device inventory"
408        );
409        assert_eq!(backends.is_empty(), devices == 0);
410
411        // No backend is implemented yet, so the honest answer is "not available".
412        assert!(!gpu_opt.is_gpu_available());
413    }
414
415    #[test]
416    fn test_gpu_backend() {
417        let optimizer = SGD::new(0.01);
418        let gpu_opt = GpuOptimizer::with_default_config(optimizer)
419            .expect("GpuOptimizer::with_default_config succeeds in test_gpu_backend");
420
421        // No device => no backend name to report.
422        assert_eq!(gpu_opt.gpu_backend().is_some(), gpu_opt.is_gpu_available());
423        assert!(gpu_opt.gpu_backend().is_none());
424    }
425
426    /// Requesting a backend that does not exist must not fabricate one.
427    #[test]
428    fn test_gpu_preferred_backend_is_not_fabricated() {
429        let optimizer = SGD::new(0.01);
430        let config = GpuConfig {
431            preferred_backend: Some("cuda".to_string()),
432            ..GpuConfig::default()
433        };
434        let gpu_opt = GpuOptimizer::new(optimizer, config).expect("construction must succeed");
435
436        assert!(!gpu_opt.is_gpu_available());
437        assert!(gpu_opt.gpu_backend().is_none());
438    }
439
440    /// The device transfer helpers must report that they are unimplemented.
441    #[test]
442    fn test_gpu_transfers_report_unavailable() {
443        let optimizer = SGD::new(0.01);
444        let gpu_opt = GpuOptimizer::with_default_config(optimizer).expect(
445            "GpuOptimizer::with_default_config succeeds in test_gpu_transfers_report_unavailable",
446        );
447
448        let data = Array1::from_vec(vec![1.0f64, 2.0]);
449        assert!(gpu_opt.to_gpu(&data.view()).is_err());
450        assert!(gpu_opt.from_gpu().is_err());
451    }
452
453    #[test]
454    fn test_gpu_config_mutations() {
455        let optimizer = SGD::new(0.01);
456        let mut gpu_opt = GpuOptimizer::with_default_config(optimizer)
457            .expect("GpuOptimizer::with_default_config succeeds in test_gpu_config_mutations");
458
459        gpu_opt.set_use_tensor_cores(false);
460        assert!(!gpu_opt.config().use_tensor_cores);
461
462        gpu_opt.set_use_mixed_precision(true);
463        assert!(gpu_opt.config().use_mixed_precision);
464    }
465
466    #[test]
467    fn test_estimate_gpu_memory() {
468        // SGD: params + gradients + velocity = 3 states
469        let mem = GpuOptimizer::<SGD<f32>, f32>::estimate_gpu_memory(1_000_000, 4, 1);
470        assert_eq!(mem, 12_000_000); // 12 MB
471
472        // Adam: params + gradients + m + v = 4 states
473        let mem = GpuOptimizer::<SGD<f32>, f32>::estimate_gpu_memory(1_000_000, 4, 2);
474        assert_eq!(mem, 16_000_000); // 16 MB
475    }
476
477    #[test]
478    fn test_gpu_memory_stats() {
479        let stats = GpuMemoryStats::new(1_000_000_000, 500_000_000);
480        assert_eq!(stats.total, 1_000_000_000);
481        assert_eq!(stats.used, 500_000_000);
482        assert_eq!(stats.free, 500_000_000);
483        assert_eq!(stats.utilization_percent(), 50.0);
484    }
485
486    #[test]
487    fn test_gpu_utils_detect_backends() {
488        // No backend is implemented, so nothing may be advertised.
489        let backends = GpuUtils::detect_backends();
490        assert!(backends.is_empty());
491        assert_eq!(GpuUtils::device_count(), 0);
492    }
493
494    #[test]
495    fn test_gpu_utils_synchronize() {
496        let result = GpuUtils::synchronize();
497        assert!(result.is_ok());
498    }
499}