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(¶ms, &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(¶ms, &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}