Skip to main content

tenflowers_core/
fallback.rs

1//! Automatic fallback mechanisms for operation recovery
2//!
3//! This module provides utilities for automatic fallback when operations fail,
4//! particularly for GPU-to-CPU fallback scenarios.
5
6#[cfg(feature = "gpu")]
7use crate::Device;
8use crate::{Result, Tensor, TensorError};
9use scirs2_core::num_traits;
10use std::sync::atomic::{AtomicBool, Ordering};
11
12/// Global flag to enable/disable automatic fallback
13static AUTO_FALLBACK_ENABLED: AtomicBool = AtomicBool::new(true);
14
15/// Configuration for fallback behavior
16#[derive(Debug, Clone)]
17pub struct FallbackConfig {
18    /// Enable automatic GPU-to-CPU fallback
19    pub gpu_to_cpu: bool,
20    /// Enable automatic precision reduction
21    pub reduce_precision: bool,
22    /// Enable memory cleanup and retry
23    pub memory_cleanup: bool,
24    /// Maximum number of retry attempts
25    pub max_retries: usize,
26    /// Log fallback attempts
27    pub log_fallbacks: bool,
28}
29
30impl Default for FallbackConfig {
31    fn default() -> Self {
32        Self {
33            gpu_to_cpu: true,
34            reduce_precision: false,
35            memory_cleanup: true,
36            max_retries: 3,
37            log_fallbacks: true,
38        }
39    }
40}
41
42/// Global fallback configuration
43static GLOBAL_FALLBACK_CONFIG: std::sync::OnceLock<std::sync::RwLock<FallbackConfig>> =
44    std::sync::OnceLock::new();
45
46/// Get the global fallback configuration
47pub fn get_fallback_config() -> FallbackConfig {
48    let lock =
49        GLOBAL_FALLBACK_CONFIG.get_or_init(|| std::sync::RwLock::new(FallbackConfig::default()));
50    match lock.read() {
51        Ok(guard) => guard.clone(),
52        Err(poisoned) => poisoned.into_inner().clone(),
53    }
54}
55
56/// Set the global fallback configuration
57pub fn set_fallback_config(config: FallbackConfig) {
58    let lock =
59        GLOBAL_FALLBACK_CONFIG.get_or_init(|| std::sync::RwLock::new(FallbackConfig::default()));
60    match lock.write() {
61        Ok(mut guard) => *guard = config,
62        Err(poisoned) => *poisoned.into_inner() = config,
63    }
64}
65
66/// Enable or disable automatic fallback globally
67pub fn set_auto_fallback_enabled(enabled: bool) {
68    AUTO_FALLBACK_ENABLED.store(enabled, Ordering::SeqCst);
69}
70
71/// Check if automatic fallback is enabled
72pub fn is_auto_fallback_enabled() -> bool {
73    AUTO_FALLBACK_ENABLED.load(Ordering::SeqCst)
74}
75
76/// Trait for operations that support fallback
77pub trait FallbackOperation<T> {
78    /// Execute the operation with automatic fallback
79    fn with_fallback(self) -> Result<T>;
80
81    /// Execute the operation on CPU as fallback
82    fn fallback_to_cpu(self) -> Result<T>;
83}
84
85/// Execute a binary operation with automatic fallback
86pub fn execute_binary_op_with_fallback<T, F>(
87    operation_name: &str,
88    tensor_a: &Tensor<T>,
89    tensor_b: &Tensor<T>,
90    gpu_op: F,
91    #[allow(unused_variables)] cpu_op: F,
92) -> Result<Tensor<T>>
93where
94    T: Clone
95        + Default
96        + scirs2_core::num_traits::Zero
97        + scirs2_core::num_traits::One
98        + Send
99        + Sync
100        + 'static
101        + bytemuck::Pod,
102    F: Fn(&Tensor<T>, &Tensor<T>) -> Result<Tensor<T>>,
103{
104    let config = get_fallback_config();
105
106    if !is_auto_fallback_enabled() {
107        return gpu_op(tensor_a, tensor_b);
108    }
109
110    // Try the primary operation first
111    match gpu_op(tensor_a, tensor_b) {
112        Ok(result) => Ok(result),
113        Err(error) => {
114            if config.log_fallbacks {
115                eprintln!("Operation '{operation_name}' failed: {error}. Attempting fallback...");
116            }
117
118            // Check if this error supports fallback
119            if error.supports_fallback() && config.gpu_to_cpu {
120                // Try to move tensors to CPU and retry
121                match (tensor_a.device(), tensor_b.device()) {
122                    #[cfg(feature = "gpu")]
123                    (Device::Gpu(_), _) | (_, Device::Gpu(_)) => {
124                        if config.log_fallbacks {
125                            eprintln!(
126                                "Falling back to CPU execution for operation '{}'",
127                                operation_name
128                            );
129                        }
130
131                        // Move tensors to CPU
132                        let cpu_a = tensor_a.to_device(Device::Cpu)?;
133                        let cpu_b = tensor_b.to_device(Device::Cpu)?;
134
135                        // Execute on CPU
136                        match cpu_op(&cpu_a, &cpu_b) {
137                            Ok(result) => {
138                                if config.log_fallbacks {
139                                    eprintln!(
140                                        "CPU fallback successful for operation '{}'",
141                                        operation_name
142                                    );
143                                }
144                                Ok(result)
145                            }
146                            Err(cpu_error) => {
147                                if config.log_fallbacks {
148                                    eprintln!(
149                                        "CPU fallback also failed for operation '{}': {}",
150                                        operation_name, cpu_error
151                                    );
152                                }
153                                Err(cpu_error)
154                            }
155                        }
156                    }
157                    _ => {
158                        // Already on CPU or other device, can't fallback further
159                        Err(error)
160                    }
161                }
162            } else {
163                Err(error)
164            }
165        }
166    }
167}
168
169/// Execute a unary operation with automatic fallback
170pub fn execute_unary_op_with_fallback<T, F>(
171    operation_name: &str,
172    tensor: &Tensor<T>,
173    gpu_op: F,
174    #[allow(unused_variables)] cpu_op: F,
175) -> Result<Tensor<T>>
176where
177    T: Clone
178        + Default
179        + scirs2_core::num_traits::Zero
180        + scirs2_core::num_traits::One
181        + Send
182        + Sync
183        + 'static
184        + bytemuck::Pod,
185    F: Fn(&Tensor<T>) -> Result<Tensor<T>>,
186{
187    let config = get_fallback_config();
188
189    if !is_auto_fallback_enabled() {
190        return gpu_op(tensor);
191    }
192
193    // Try the primary operation first
194    match gpu_op(tensor) {
195        Ok(result) => Ok(result),
196        Err(error) => {
197            if config.log_fallbacks {
198                eprintln!("Operation '{operation_name}' failed: {error}. Attempting fallback...");
199            }
200
201            // Check if this error supports fallback
202            if error.supports_fallback() && config.gpu_to_cpu {
203                // Try to move tensor to CPU and retry
204                #[cfg(feature = "gpu")]
205                return if let Device::Gpu(_) = tensor.device() {
206                    if config.log_fallbacks {
207                        eprintln!(
208                            "Falling back to CPU execution for operation '{}'",
209                            operation_name
210                        );
211                    }
212
213                    // Move tensor to CPU
214                    let cpu_tensor = tensor.to_device(Device::Cpu)?;
215
216                    // Execute on CPU
217                    match cpu_op(&cpu_tensor) {
218                        Ok(result) => {
219                            if config.log_fallbacks {
220                                eprintln!(
221                                    "CPU fallback successful for operation '{}'",
222                                    operation_name
223                                );
224                            }
225                            Ok(result)
226                        }
227                        Err(cpu_error) => {
228                            if config.log_fallbacks {
229                                eprintln!(
230                                    "CPU fallback also failed for operation '{}': {}",
231                                    operation_name, cpu_error
232                                );
233                            }
234                            Err(cpu_error)
235                        }
236                    }
237                } else {
238                    // Already on CPU or other device, can't fallback further
239                    Err(error)
240                };
241
242                #[cfg(not(feature = "gpu"))]
243                return Err(error);
244            } else {
245                Err(error)
246            }
247        }
248    }
249}
250
251/// Memory cleanup utility for fallback scenarios
252pub fn cleanup_memory_and_retry<T, F>(operation: F, max_retries: usize) -> Result<T>
253where
254    F: Fn() -> Result<T>,
255{
256    let mut attempt = 0;
257
258    loop {
259        match operation() {
260            Ok(result) => return Ok(result),
261            Err(error) => {
262                attempt += 1;
263
264                if attempt >= max_retries {
265                    return Err(error);
266                }
267
268                // Check if this is a memory-related error
269                match &error {
270                    TensorError::AllocationError { .. } | TensorError::ResourceExhausted { .. } => {
271                        eprintln!("Memory error detected, attempting cleanup (attempt {attempt}/{max_retries})");
272
273                        // Trigger garbage collection if available
274                        #[cfg(feature = "gpu")]
275                        {
276                            // Clear GPU memory pools
277                            crate::memory::global_monitor().clear();
278                        }
279
280                        // Force garbage collection
281                        std::hint::black_box(Vec::<u8>::new());
282
283                        // Short delay before retry
284                        std::thread::sleep(std::time::Duration::from_millis(100));
285                    }
286                    _ => {
287                        // Not a memory error, don't retry
288                        return Err(error);
289                    }
290                }
291            }
292        }
293    }
294}
295
296/// Wrapper for automatic fallback of results
297pub struct FallbackWrapper<T> {
298    result: Result<T>,
299    operation_name: String,
300}
301
302impl<T> FallbackWrapper<T> {
303    pub fn new(result: Result<T>, operation_name: &str) -> Self {
304        Self {
305            result,
306            operation_name: operation_name.to_string(),
307        }
308    }
309
310    pub fn with_cpu_fallback<F>(self, cpu_fallback: F) -> Result<T>
311    where
312        F: FnOnce() -> Result<T>,
313    {
314        match self.result {
315            Ok(result) => Ok(result),
316            Err(error) => {
317                if error.supports_fallback() && is_auto_fallback_enabled() {
318                    let config = get_fallback_config();
319                    if config.log_fallbacks {
320                        eprintln!(
321                            "Attempting CPU fallback for operation '{}'",
322                            self.operation_name
323                        );
324                    }
325                    cpu_fallback()
326                } else {
327                    Err(error)
328                }
329            }
330        }
331    }
332}
333
334#[cfg(test)]
335mod tests {
336    use super::*;
337    use crate::{DType, Device, Tensor};
338
339    #[test]
340    fn test_fallback_config() {
341        let config = FallbackConfig::default();
342        assert!(config.gpu_to_cpu);
343        assert!(config.memory_cleanup);
344        assert_eq!(config.max_retries, 3);
345    }
346
347    #[test]
348    fn test_auto_fallback_flag() {
349        assert!(is_auto_fallback_enabled()); // Default is true
350
351        set_auto_fallback_enabled(false);
352        assert!(!is_auto_fallback_enabled());
353
354        set_auto_fallback_enabled(true);
355        assert!(is_auto_fallback_enabled());
356    }
357
358    #[test]
359    fn test_fallback_wrapper() {
360        let success_result: Result<i32> = Ok(42);
361        let wrapper = FallbackWrapper::new(success_result, "test_op");
362
363        let result = wrapper.with_cpu_fallback(|| Ok(100));
364        assert_eq!(result.expect("test: operation should succeed"), 42);
365    }
366
367    #[test]
368    fn test_error_supports_fallback() {
369        let gpu_error = TensorError::unsupported_device("test", "gpu:0", true);
370        assert!(gpu_error.supports_fallback());
371
372        let shape_error = TensorError::shape_mismatch("test", "[2, 2]", "[3, 3]");
373        assert!(!shape_error.supports_fallback());
374    }
375}