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