Skip to main content

burn_backend/backend/
device.rs

1pub use burn_std::device::*;
2use burn_std::{BoolDType, DType, FloatDType, IntDType};
3pub use burn_std::{DeviceError, DeviceSettings};
4
5use burn_std::sync::{Arc, LazyLock, RwLock};
6
7use core::any::TypeId;
8
9#[cfg(feature = "std")]
10pub use std::collections::HashMap;
11#[cfg(feature = "std")]
12use std::sync::OnceLock;
13
14#[cfg(not(feature = "std"))]
15pub use hashbrown::HashMap;
16#[cfg(not(feature = "std"))]
17use spin::Once as OnceLock;
18
19use crate::Backend;
20
21/// Device trait for all burn backend devices.
22pub trait DeviceOps: Clone + Default + PartialEq + Send + Sync + core::fmt::Debug + Device {
23    /// Returns the [device id](DeviceId).
24    fn id(&self) -> DeviceId {
25        self.to_id()
26    }
27
28    /// Returns the default [settings](DeviceSettings) for this device.
29    fn defaults(&self) -> DeviceSettings;
30}
31
32/// Key for the registry: physical device type + device id
33type RegistryKey = (DeviceId, TypeId);
34
35/// Global registry mapping devices to their settings.
36///
37/// Each value is wrapped in a `OnceLock` to enforce that settings are initialized only once
38/// per device.
39static REGISTRY: LazyLock<RwLock<HashMap<RegistryKey, Arc<OnceLock<DeviceSettings>>>>> =
40    LazyLock::new(|| RwLock::new(HashMap::new()));
41
42struct DeviceSettingsRegistry;
43
44impl DeviceSettingsRegistry {
45    /// Returns initialized settings without committing defaults or populating the cache.
46    fn get<D: DeviceOps>(device: &D) -> Option<DeviceSettings> {
47        REGISTRY
48            .read()
49            .get(&Self::key(device))
50            .and_then(|settings| settings.get().copied())
51    }
52
53    /// Returns the settings for the given device, inserting the default if absent.
54    fn get_or_insert<D: DeviceOps>(
55        device: &D,
56        default_fn: impl FnOnce() -> DeviceSettings,
57    ) -> DeviceSettings {
58        let key = Self::key(device);
59        #[cfg(feature = "std")]
60        {
61            let cached = LOCAL_CACHE.with(|cache| cache.borrow().get(&key).copied());
62            if let Some(settings) = cached {
63                return settings;
64            }
65
66            // Entry does not exist in cache
67            let settings = {
68                let read = REGISTRY.read();
69                read.get(&key).cloned()
70            }
71            .unwrap_or_else(|| {
72                let mut map = REGISTRY.write();
73                Arc::clone(map.entry(key).or_default())
74            });
75
76            let settings = *settings.get_or_init(default_fn);
77
78            LOCAL_CACHE.with(|cache| {
79                cache.borrow_mut().insert(key, settings);
80            });
81
82            settings
83        }
84        #[cfg(not(feature = "std"))]
85        {
86            let settings = {
87                let read = REGISTRY.read();
88                read.get(&key).cloned()
89            }
90            .unwrap_or_else(|| {
91                let mut map = REGISTRY.write();
92                Arc::clone(map.entry(key).or_default())
93            });
94
95            settings.call_once(default_fn);
96            *settings.get().unwrap()
97        }
98    }
99
100    /// Initializes the settings for the given device.
101    ///
102    /// Returns `Err` with the existing settings if already initialized.
103    fn init<D: DeviceOps>(device: &D, settings: DeviceSettings) -> Result<(), DeviceError> {
104        let key = Self::key(device);
105        let mut map = REGISTRY.write();
106        let cell = map.entry(key).or_insert_with(|| Arc::new(OnceLock::new()));
107
108        #[cfg(feature = "std")]
109        return cell
110            .set(settings)
111            .map_err(|_| DeviceError::already_initialized(device));
112
113        #[cfg(not(feature = "std"))]
114        if cell.get().is_some() {
115            Err(DeviceError::already_initialized(device))
116        } else {
117            cell.call_once(|| settings);
118            Ok(())
119        }
120    }
121
122    /// Returns the device registry key.
123    fn key<D: Device>(device: &D) -> RegistryKey {
124        (device.to_id(), TypeId::of::<D>())
125    }
126}
127
128#[cfg(feature = "std")]
129thread_local! {
130    /// Thread-local cache access to initialized device settings is lock-free.
131    static LOCAL_CACHE: core::cell::RefCell<HashMap<RegistryKey, DeviceSettings>> =
132        core::cell::RefCell::new(HashMap::new());
133}
134
135/// Get the [`device`'s settings](DeviceSettings), initializing them to defaults if unset.
136pub fn get_or_init_device_settings<B: Backend>(device: &B::Device) -> DeviceSettings {
137    DeviceSettingsRegistry::get_or_insert(device, || device.defaults())
138}
139
140/// Returns the configured settings, or backend defaults without initializing them.
141///
142/// Before initialization, this is a snapshot: subsequent configuration may change the settings.
143/// Use [`get_or_init_device_settings`] when the returned settings must remain fixed for subsequent
144/// operations.
145pub fn get_device_settings<B: Backend>(device: &B::Device) -> DeviceSettings {
146    DeviceSettingsRegistry::get(device).unwrap_or_else(|| device.defaults())
147}
148
149fn check_dtype_support<B: Backend>(
150    device: &B::Device,
151    dtype: impl Into<DType>,
152) -> Result<(), DeviceError> {
153    let dtype = dtype.into();
154    // Default dtypes should have `DTypeUsage::general()`. Types restricted to specialized
155    // operations should not be used as default.
156    if B::supports_dtype(device, dtype) {
157        Ok(())
158    } else {
159        Err(DeviceError::unsupported_dtype(device, dtype))
160    }
161}
162
163/// Sets the default data types for the device.
164///
165/// This updates the device's default data types used for tensor creation.
166///
167/// Settings can only be initialized once per device. Subsequent calls for
168/// the same device return [`DeviceError::AlreadyInitialized`].
169///
170/// # Note
171///
172/// Initialization must happen before any tensor creation on the device.
173/// The first tensor operation will lock the device to its defaults, causing
174/// any subsequent initialization attempt to return [`DeviceError::AlreadyInitialized`].
175///
176/// # Example
177///
178/// ```rust, ignore
179/// fn example<B: Backend>() {
180///     let device = B::Device::default();
181///     
182///     // Update the device settings
183///     set_default_dtypes::<B>(&device, DType::F16, DType::I32);
184///     
185///     // All float tensors created after this will use F16 by default
186///     let tensor = Tensor::<B, 2>::zeros([2, 3], &device);
187///     // All int tensors created after this will use I32 default
188///     let tensor = Tensor::<B, 2, Int>::zeros([2, 3], &device);
189/// }
190/// ```
191pub fn set_default_dtypes<B: Backend>(
192    device: &B::Device,
193    float_dtype: impl Into<FloatDType>,
194    int_dtype: impl Into<IntDType>,
195    bool_dtype: impl Into<BoolDType>,
196) -> Result<(), DeviceError> {
197    let float_dtype = float_dtype.into();
198    let int_dtype = int_dtype.into();
199    let bool_dtype = bool_dtype.into();
200    check_dtype_support::<B>(device, float_dtype)?;
201    check_dtype_support::<B>(device, int_dtype)?;
202    check_dtype_support::<B>(device, bool_dtype)?;
203
204    let q_config = device.defaults().quantization;
205    let settings = DeviceSettings::new(float_dtype, int_dtype, bool_dtype, q_config);
206
207    initialize_unchecked(device, settings)?;
208    Ok(())
209}
210
211// Unchecked dtypes
212fn initialize_unchecked<D: DeviceOps>(
213    device: &D,
214    settings: DeviceSettings,
215) -> Result<(), DeviceError> {
216    DeviceSettingsRegistry::init(device, settings)
217}
218
219#[cfg(all(test, feature = "std"))]
220mod tests {
221    use serial_test::serial;
222
223    use super::*;
224
225    fn clear_registry() {
226        REGISTRY.write().clear();
227    }
228
229    #[derive(Clone, Debug, Default, PartialEq, new)]
230    pub struct TestDeviceA {
231        index: u16,
232    }
233
234    impl Device for TestDeviceA {
235        fn from_id(device_id: DeviceId) -> Self {
236            Self {
237                index: device_id.index_id,
238            }
239        }
240
241        fn to_id(&self) -> DeviceId {
242            DeviceId {
243                type_id: 0,
244                index_id: self.index,
245            }
246        }
247    }
248
249    impl DeviceOps for TestDeviceA {
250        fn defaults(&self) -> DeviceSettings {
251            DeviceSettings::with_dtypes(FloatDType::F32, IntDType::I32, BoolDType::Native)
252        }
253    }
254
255    #[derive(Clone, Debug, Default, PartialEq, new)]
256    pub struct TestDeviceB {
257        index: u16,
258    }
259
260    impl Device for TestDeviceB {
261        fn from_id(device_id: DeviceId) -> Self {
262            Self {
263                index: device_id.index_id,
264            }
265        }
266
267        fn to_id(&self) -> DeviceId {
268            DeviceId {
269                type_id: 0,
270                index_id: self.index,
271            }
272        }
273    }
274
275    impl DeviceOps for TestDeviceB {
276        fn defaults(&self) -> DeviceSettings {
277            DeviceSettings::with_dtypes(FloatDType::F32, IntDType::I32, BoolDType::Native)
278        }
279    }
280
281    fn get_test_device_settings<D: DeviceOps>(device: &D) -> DeviceSettings {
282        DeviceSettingsRegistry::get_or_insert(device, || device.defaults())
283    }
284
285    #[test]
286    #[serial]
287    fn default_settings_returned_when_uninitialized() {
288        clear_registry(); // reset registry for each test
289
290        let device = TestDeviceA::new(0);
291
292        let s1 = get_test_device_settings(&device);
293        let s2 = get_test_device_settings(&device);
294
295        assert_eq!(s1, s2);
296        assert_eq!(s1, device.defaults());
297    }
298
299    #[test]
300    #[serial]
301    fn initialized_settings_are_returned() {
302        clear_registry(); // reset registry for each test
303
304        let device = TestDeviceA::new(0);
305        let settings =
306            DeviceSettings::with_dtypes(FloatDType::BF16, IntDType::I32, BoolDType::Native);
307
308        initialize_unchecked(&device, settings).unwrap();
309        let s1 = get_test_device_settings(&device);
310        let s2 = get_test_device_settings(&device);
311
312        assert_eq!(s1, s2);
313        assert_eq!(s1, settings);
314        assert_eq!(s2, settings);
315    }
316
317    #[test]
318    #[serial]
319    fn querying_settings_leaves_initialization_open() {
320        clear_registry();
321
322        let device = TestDeviceA::new(2);
323        assert_eq!(DeviceSettingsRegistry::get(&device), None);
324
325        let settings =
326            DeviceSettings::with_dtypes(FloatDType::F16, IntDType::I64, BoolDType::Native);
327        initialize_unchecked(&device, settings).unwrap();
328
329        assert_eq!(DeviceSettingsRegistry::get(&device), Some(settings));
330        assert_eq!(get_test_device_settings(&device), settings);
331    }
332
333    #[test]
334    #[serial]
335    fn settings_are_device_id_specific() {
336        clear_registry(); // reset registry for each test
337
338        let d1 = TestDeviceA::new(0);
339        let d2 = TestDeviceA::new(1);
340        let settings =
341            DeviceSettings::with_dtypes(FloatDType::F16, IntDType::I64, BoolDType::Native);
342
343        initialize_unchecked(&d1, settings).unwrap();
344
345        let s1 = get_test_device_settings(&d1);
346        let s2 = get_test_device_settings(&d2);
347
348        assert_ne!(s1, s2);
349        assert_eq!(s1, settings);
350        assert_eq!(s2, d2.defaults());
351    }
352
353    #[test]
354    #[serial]
355    fn settings_are_device_type_specific() {
356        clear_registry(); // reset registry for each test
357
358        let d1 = TestDeviceA::new(0);
359        let d2 = TestDeviceB::new(0);
360        let settings =
361            DeviceSettings::with_dtypes(FloatDType::F16, IntDType::I64, BoolDType::Native);
362
363        initialize_unchecked(&d2, settings).unwrap();
364
365        let s1 = get_test_device_settings(&d1);
366        let s2 = get_test_device_settings(&d2);
367
368        assert_ne!(s1, s2);
369        assert_eq!(s1, d1.defaults());
370        assert_eq!(s2, settings);
371    }
372
373    #[test]
374    #[serial]
375    fn initialization_after_default_returns_error() {
376        clear_registry(); // reset registry for each test
377
378        let device = TestDeviceA::new(0);
379        // Settings are set to default on first access, which forces consistency
380        let _before = get_test_device_settings(&device);
381
382        let settings =
383            DeviceSettings::with_dtypes(FloatDType::BF16, IntDType::I64, BoolDType::Native);
384        let result = initialize_unchecked(&device, settings);
385
386        assert!(matches!(
387            result,
388            Err(DeviceError::AlreadyInitialized { .. })
389        ));
390    }
391
392    #[test]
393    #[serial]
394    fn second_initialization_returns_error() {
395        clear_registry(); // reset registry for each test
396
397        let device = TestDeviceA::new(0);
398        let settings =
399            DeviceSettings::with_dtypes(FloatDType::F16, IntDType::I32, BoolDType::Native);
400        initialize_unchecked(&device, settings).unwrap();
401
402        let result = initialize_unchecked(&device, device.defaults());
403        assert!(matches!(
404            result,
405            Err(DeviceError::AlreadyInitialized { .. })
406        ));
407    }
408
409    #[cfg(feature = "std")]
410    #[test]
411    #[serial]
412    fn initialized_settings_are_global() {
413        clear_registry();
414
415        let device = TestDeviceA::new(0);
416        let settings =
417            DeviceSettings::with_dtypes(FloatDType::F16, IntDType::I32, BoolDType::Native);
418
419        initialize_unchecked(&device, settings).unwrap();
420        let settings_actual = get_test_device_settings(&device);
421        assert_eq!(settings_actual, settings);
422
423        // The other thread will see the initialized settings
424        let seen_by_new_thread =
425            std::thread::spawn(move || get_test_device_settings(&TestDeviceA::new(0)))
426                .join()
427                .unwrap();
428        assert_eq!(seen_by_new_thread, settings_actual);
429    }
430}