tenflowers_core/
fallback.rs1#[cfg(feature = "gpu")]
7use crate::Device;
8use crate::{Result, Tensor, TensorError};
9use scirs2_core::num_traits;
10use std::sync::atomic::{AtomicBool, Ordering};
11
12static AUTO_FALLBACK_ENABLED: AtomicBool = AtomicBool::new(true);
14
15#[derive(Debug, Clone)]
17pub struct FallbackConfig {
18 pub gpu_to_cpu: bool,
20 pub reduce_precision: bool,
22 pub memory_cleanup: bool,
24 pub max_retries: usize,
26 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
42static GLOBAL_FALLBACK_CONFIG: std::sync::OnceLock<std::sync::RwLock<FallbackConfig>> =
44 std::sync::OnceLock::new();
45
46pub 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
56pub 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
66pub fn set_auto_fallback_enabled(enabled: bool) {
68 AUTO_FALLBACK_ENABLED.store(enabled, Ordering::SeqCst);
69}
70
71pub fn is_auto_fallback_enabled() -> bool {
73 AUTO_FALLBACK_ENABLED.load(Ordering::SeqCst)
74}
75
76pub trait FallbackOperation<T> {
78 fn with_fallback(self) -> Result<T>;
80
81 fn fallback_to_cpu(self) -> Result<T>;
83}
84
85pub 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 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 if error.supports_fallback() && config.gpu_to_cpu {
120 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 let cpu_a = tensor_a.to_device(Device::Cpu)?;
133 let cpu_b = tensor_b.to_device(Device::Cpu)?;
134
135 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 Err(error)
160 }
161 }
162 } else {
163 Err(error)
164 }
165 }
166 }
167}
168
169pub 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 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 if error.supports_fallback() && config.gpu_to_cpu {
203 #[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 let cpu_tensor = tensor.to_device(Device::Cpu)?;
215
216 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 Err(error)
240 };
241
242 #[cfg(not(feature = "gpu"))]
243 return Err(error);
244 } else {
245 Err(error)
246 }
247 }
248 }
249}
250
251pub 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 match &error {
270 TensorError::AllocationError { .. } | TensorError::ResourceExhausted { .. } => {
271 eprintln!("Memory error detected, attempting cleanup (attempt {attempt}/{max_retries})");
272
273 #[cfg(feature = "gpu")]
275 {
276 crate::memory::global_monitor().clear();
278 }
279
280 std::hint::black_box(Vec::<u8>::new());
282
283 std::thread::sleep(std::time::Duration::from_millis(100));
285 }
286 _ => {
287 return Err(error);
289 }
290 }
291 }
292 }
293 }
294}
295
296pub 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()); 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}