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