Skip to main content

cubecl_runtime/storage/
bytes_cpu.rs

1use crate::server::IoError;
2
3use super::{ComputeStorage, StorageHandle, StorageId, StorageUtilization};
4use alloc::alloc::{Layout, alloc_zeroed, dealloc};
5use cubecl_environment::backtrace::BackTrace;
6use cubecl_environment::collections::HashMap;
7
8/// The bytes storage maps ids to pointers of bytes in a contiguous layout.
9#[derive(Default)]
10pub struct BytesStorage {
11    memory: HashMap<StorageId, AllocatedBytes>,
12}
13
14impl core::fmt::Debug for BytesStorage {
15    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
16        f.write_str("BytesStorage")
17    }
18}
19
20/// Can send to other threads.
21unsafe impl Send for BytesStorage {}
22unsafe impl Send for BytesResource {}
23
24/// This struct is a pointer to a memory chunk or slice.
25#[derive(Debug)]
26pub struct BytesResource {
27    ptr: *mut u8,
28    utilization: StorageUtilization,
29}
30
31/// This struct refers to a specific (contiguous) layout of bytes.
32struct AllocatedBytes {
33    ptr: *mut u8,
34    layout: Layout,
35}
36
37impl BytesResource {
38    /// Returns a mutable pointer to the start of the resource and its length.
39    pub fn get_write_ptr_and_length(&self) -> (*mut u8, usize) {
40        (
41            // SAFETY:
42            // - The offset is created to be within the bounds of the allocation.
43            unsafe { self.ptr.add(self.utilization.offset as usize) },
44            self.utilization.size as usize,
45        )
46    }
47
48    /// Returns the resource as a mutable slice of bytes.
49    ///
50    /// The lifetime `'a` is the lifetime of the underlying `BytesStorage` allocation,
51    /// not of `self`. The `&mut self` ensures only one mutable slice is created per
52    /// resource. Multiple resources may point to non-overlapping regions of the same
53    /// allocation (like `split_at_mut`); each resource owns its region exclusively.
54    pub fn write<'a>(&mut self) -> &'a mut [u8] {
55        let (ptr, len) = self.get_write_ptr_and_length();
56
57        // SAFETY:
58        // - ptr is non-null and aligned (from BytesStorage::alloc).
59        // - The region [ptr..ptr+len) is within a single allocation.
60        // - Memory is initialized (BytesStorage uses alloc_zeroed).
61        // - `&mut self` ensures exclusive access to this resource's region.
62        // - `StorageHandle` assigns non-overlapping regions per resource.
63        // - Systems must make sure this is the only `BytesResource` with an outstanding mutable borrow.
64        unsafe { core::slice::from_raw_parts_mut(ptr, len) }
65    }
66
67    /// Returns the resource as an immutable slice of bytes.
68    ///
69    /// See [`write`](Self::write) for lifetime and safety notes.
70    pub fn read<'a>(&self) -> &'a [u8] {
71        let (ptr, len) = self.get_write_ptr_and_length();
72
73        // SAFETY:
74        // - ptr is non-null and aligned (from BytesStorage::alloc).
75        // - The region [ptr..ptr+len) is within a single allocation.
76        // - Memory is initialized (BytesStorage uses alloc_zeroed).
77        unsafe { core::slice::from_raw_parts(ptr, len) }
78    }
79}
80
81impl ComputeStorage for BytesStorage {
82    type Resource = BytesResource;
83
84    fn alignment(&self) -> usize {
85        4
86    }
87
88    fn get(&mut self, handle: &StorageHandle) -> Result<Self::Resource, IoError> {
89        let allocated_bytes =
90            self.memory
91                .get(&handle.id)
92                .ok_or_else(|| IoError::StorageHandleNotFound {
93                    reason: alloc::format!("{} in the bytes storage", handle.id).into(),
94                    backtrace: BackTrace::capture(),
95                })?;
96
97        Ok(BytesResource {
98            ptr: allocated_bytes.ptr,
99            utilization: handle.utilization.clone(),
100        })
101    }
102
103    #[cfg_attr(
104        feature = "tracing",
105        tracing::instrument(level = "trace", skip(self, size))
106    )]
107    fn alloc(&mut self, size: u64) -> Result<StorageHandle, IoError> {
108        let id = StorageId::new();
109        let handle = StorageHandle {
110            id,
111            utilization: StorageUtilization { offset: 0, size },
112        };
113
114        if size == 0 {
115            // Zero-size allocations are valid handles but don't need real memory.
116            let memory = AllocatedBytes {
117                ptr: core::ptr::NonNull::dangling().as_ptr(),
118                layout: Layout::new::<()>(),
119            };
120            self.memory.insert(id, memory);
121        } else {
122            unsafe {
123                let layout = Layout::array::<u8>(size as usize).unwrap();
124
125                // We allocate zeroed memory since we expose it as &[u8] / &mut [u8]
126                // which requires initialization.
127                let ptr = alloc_zeroed(layout);
128                if ptr.is_null() {
129                    return Err(IoError::BufferTooBig {
130                        size,
131                        backtrace: BackTrace::capture(),
132                    });
133                }
134                let memory = AllocatedBytes { ptr, layout };
135                self.memory.insert(id, memory);
136            }
137        }
138
139        Ok(handle)
140    }
141
142    #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip(self)))]
143    fn dealloc(&mut self, id: StorageId) {
144        if let Some(memory) = self.memory.remove(&id)
145            && memory.layout.size() > 0
146        {
147            unsafe {
148                dealloc(memory.ptr, memory.layout);
149            }
150        }
151    }
152
153    fn flush(&mut self) {
154        // We don't wait for dealloc.
155    }
156}
157
158#[cfg(test)]
159mod tests {
160    use super::*;
161
162    #[test_log::test]
163    fn test_can_alloc_and_dealloc() {
164        let mut storage = BytesStorage::default();
165        let handle_1 = storage.alloc(64).unwrap();
166
167        assert_eq!(handle_1.size(), 64);
168        storage.dealloc(handle_1.id);
169    }
170
171    #[test_log::test]
172    fn test_slices() {
173        let mut storage = BytesStorage::default();
174        let handle_1 = storage.alloc(64).unwrap();
175        let handle_2 = StorageHandle::new(
176            handle_1.id,
177            StorageUtilization {
178                offset: 24,
179                size: 8,
180            },
181        );
182
183        storage
184            .get(&handle_1)
185            .unwrap()
186            .write()
187            .iter_mut()
188            .enumerate()
189            .for_each(|(i, b)| {
190                *b = i as u8;
191            });
192
193        let bytes = storage.get(&handle_2).unwrap().read().to_vec();
194
195        storage.dealloc(handle_1.id);
196        assert_eq!(bytes, &[24, 25, 26, 27, 28, 29, 30, 31]);
197    }
198
199    /// Miri catches: "reading memory, but memory is uninitialized"
200    #[test_log::test]
201    fn test_read_after_alloc_without_write() {
202        let mut storage = BytesStorage::default();
203        let handle = storage.alloc(16).unwrap();
204        let resource = storage.get(&handle).unwrap();
205        assert!(resource.read().iter().all(|&b| b == 0));
206        storage.dealloc(handle.id);
207    }
208
209    /// Miri catches: "creating allocation with size 0"
210    #[test_log::test]
211    fn test_zero_size_alloc_and_dealloc() {
212        let mut storage = BytesStorage::default();
213        let handle = storage.alloc(0).unwrap();
214        assert_eq!(handle.size(), 0);
215        storage.dealloc(handle.id);
216    }
217
218    #[test_log::test]
219    fn test_alloc_dealloc_realloc() {
220        let mut storage = BytesStorage::default();
221        let h1 = storage.alloc(32).unwrap();
222        storage.get(&h1).unwrap().write()[0] = 0xAA;
223        storage.dealloc(h1.id);
224        let h2 = storage.alloc(32).unwrap();
225        storage.dealloc(h2.id);
226    }
227
228    #[test_log::test]
229    fn test_multiple_non_overlapping_regions() {
230        let mut storage = BytesStorage::default();
231        let base = storage.alloc(64).unwrap();
232
233        let regions: alloc::vec::Vec<_> = (0..4)
234            .map(|i| {
235                StorageHandle::new(
236                    base.id,
237                    StorageUtilization {
238                        offset: i * 16,
239                        size: 16,
240                    },
241                )
242            })
243            .collect();
244
245        for (i, region) in regions.iter().enumerate() {
246            storage.get(region).unwrap().write().fill(i as u8);
247        }
248        for (i, region) in regions.iter().enumerate() {
249            assert!(
250                storage
251                    .get(region)
252                    .unwrap()
253                    .read()
254                    .iter()
255                    .all(|&b| b == i as u8)
256            );
257        }
258        storage.dealloc(base.id);
259    }
260}