Skip to main content

cubecl_environment/bytes/
shared_arc.rs

1//! Allocation controller that shares another [`Bytes`] through an [`Arc`].
2//!
3//! Unlike the other controllers, [`SharedAllocationController`] does not own an
4//! allocation directly. Instead it composes over an existing [`Bytes`] held
5//! behind an [`Arc`], optionally viewing only a sub-range of it. This makes
6//! cloning and splitting cheap (reference counted, zero-copy) at the cost of
7//! never being able to detach the allocation into a `Vec` (so
8//! [`Bytes::try_into_vec`](super::Bytes::try_into_vec) always fails on shared
9//! bytes).
10//!
11//! # Example
12//!
13//! ```
14//! use cubecl_environment::bytes::Bytes;
15//!
16//! let bytes = Bytes::from_elems(vec![1u32, 2, 3, 4]);
17//! let shared = bytes.shared();
18//! // Cloning is cheap, both clones reference the same backing allocation.
19//! let clone = shared.clone();
20//! assert_eq!(&shared[..], &clone[..]);
21//! ```
22
23use super::{
24    AccessError, AccessPolicy, AllocationController, AllocationProperty, Bytes, SplitError,
25    default_controller::{MAX_ALIGN, NativeAllocationController},
26};
27use crate::sync::Arc;
28use alloc::boxed::Box;
29use core::mem::MaybeUninit;
30use spin::Once;
31
32/// Allocation controller that shares a view into another [`Bytes`] behind an [`Arc`].
33///
34/// The shared content is lazily copied into a private buffer on first mutable access
35/// (copy-on-write), behind a [`Once`] so the private buffer is materialized exactly once.
36pub struct SharedAllocationController {
37    /// The shared underlying bytes.
38    inner: Arc<Bytes>,
39    /// Offset, in bytes, of this view into `inner`.
40    offset: usize,
41    /// Length, in bytes, of this view.
42    len: usize,
43    /// Lazily initialized private buffer (copy-on-write).
44    controller: Once<Box<dyn AllocationController>>,
45}
46
47impl SharedAllocationController {
48    /// Create a controller viewing `inner[offset..offset + len]`.
49    pub(crate) fn new(inner: Arc<Bytes>, offset: usize, len: usize) -> Self {
50        debug_assert!(
51            offset + len <= inner.len(),
52            "shared view must stay within the bounds of the inner bytes"
53        );
54        Self {
55            inner,
56            offset,
57            len,
58            controller: Once::new(),
59        }
60    }
61
62    /// The shared view, valid as long as no copy-on-write has occurred.
63    fn view(&self) -> &[u8] {
64        &self.inner[self.offset..self.offset + self.len]
65    }
66
67    /// Copy the shared view into a private, writable native allocation on first call.
68    /// Called lazily on first mutable access (copy-on-write).
69    fn init_mutable(&self) -> &dyn AllocationController {
70        &**self.controller.call_once(|| {
71            // Allocate with `MAX_ALIGN` to keep the data usable for any element type.
72            Box::new(
73                NativeAllocationController::alloc_with_data(self.view(), MAX_ALIGN)
74                    .expect("failed to allocate copy-on-write buffer for shared bytes"),
75            ) as Box<dyn AllocationController>
76        })
77    }
78}
79
80impl AllocationController for SharedAllocationController {
81    fn alloc_align(&self) -> usize {
82        if self.controller.is_completed() {
83            MAX_ALIGN
84        } else {
85            // Report the inner allocation's alignment. `try_into_vec` still
86            // fails because `try_detach` is never implemented for shared bytes.
87            self.inner.align()
88        }
89    }
90
91    fn property(&self) -> AllocationProperty {
92        self.inner.property()
93    }
94
95    fn may_alias_mutable_storage(&self) -> bool {
96        match self.controller.get() {
97            Some(controller) => controller.may_alias_mutable_storage(),
98            None => self.inner.may_alias_mutable_storage(),
99        }
100    }
101
102    // The view length, known without copy-on-write (never materializes).
103    fn capacity(&self) -> usize {
104        self.len
105    }
106
107    fn memory(&self, policy: AccessPolicy) -> Result<&[MaybeUninit<u8>], AccessError> {
108        match self.controller.get() {
109            // After copy-on-write, read from the private controller.
110            Some(controller) => controller.memory(policy),
111            None => {
112                // Reading the shared view is always zero-copy, so no policy check is needed.
113                let slice = self.view();
114                // SAFETY: `&[u8]` and `&[MaybeUninit<u8>]` share a layout, and every
115                // byte of the shared view is initialized.
116                Ok(unsafe { core::slice::from_raw_parts(slice.as_ptr().cast(), slice.len()) })
117            }
118        }
119    }
120
121    unsafe fn memory_mut(
122        &mut self,
123        policy: AccessPolicy,
124    ) -> Result<&mut [MaybeUninit<u8>], AccessError> {
125        // Mutating still-shared data requires copying it into a private buffer first.
126        if !self.controller.is_completed() && !policy.copy_allowed() {
127            return Err(AccessError::WouldCopy);
128        }
129        // Trigger copy-on-write so we never mutate the shared allocation.
130        self.init_mutable();
131
132        // SAFETY: `init_mutable` guarantees the private controller is set, and `&mut self` is
133        // exclusive.
134        let controller = self
135            .controller
136            .get_mut()
137            .expect("controller must be set after init_mutable");
138        unsafe { controller.memory_mut(policy) }
139    }
140
141    fn split(
142        &mut self,
143        offset: usize,
144    ) -> Result<(Box<dyn AllocationController>, Box<dyn AllocationController>), SplitError> {
145        if self.controller.is_completed() {
146            // After copy-on-write the private buffer is no longer shared.
147            return Err(SplitError::Unsupported);
148        }
149        // Use `>` (not `>=`) to allow boundary splits where one side is empty.
150        if offset > self.len {
151            return Err(SplitError::InvalidOffset);
152        }
153
154        let left = SharedAllocationController::new(self.inner.clone(), self.offset, offset);
155        let right = SharedAllocationController::new(
156            self.inner.clone(),
157            self.offset + offset,
158            self.len - offset,
159        );
160
161        Ok((Box::new(left), Box::new(right)))
162    }
163
164    fn view(&self, start: usize, end: usize) -> Option<Box<dyn AllocationController>> {
165        if self.controller.is_completed() {
166            // After copy-on-write the private buffer is no longer shared.
167            return None;
168        }
169        if start > end || end > self.len {
170            return None;
171        }
172
173        Some(Box::new(SharedAllocationController::new(
174            self.inner.clone(),
175            self.offset + start,
176            end - start,
177        )))
178    }
179
180    fn duplicate(&self) -> Option<Box<dyn AllocationController>> {
181        if self.controller.is_completed() {
182            // After mutation the private buffer can't be shared cheaply.
183            return None;
184        }
185
186        Some(Box::new(SharedAllocationController::new(
187            self.inner.clone(),
188            self.offset,
189            self.len,
190        )))
191    }
192
193    unsafe fn copy_into(&self, buf: &mut [u8]) {
194        match self.controller.get() {
195            Some(controller) => {
196                let memory = controller
197                    .memory(AccessPolicy::default())
198                    .expect("shared: host access failed");
199                let copy_len = buf.len().min(memory.len());
200                // SAFETY: every byte of the private buffer up to its length is initialized.
201                let data =
202                    unsafe { core::slice::from_raw_parts(memory.as_ptr().cast::<u8>(), copy_len) };
203                buf[..copy_len].copy_from_slice(data);
204            }
205            None => {
206                let src = self.view();
207                let copy_len = buf.len().min(src.len());
208                buf[..copy_len].copy_from_slice(&src[..copy_len]);
209            }
210        }
211    }
212}
213
214// The type is no-std; its tests need std (test_log, std collections).
215#[cfg(all(test, feature = "std"))]
216mod tests {
217    use super::super::{AccessError, Bytes, Reader, SplitPolicy, Writer};
218    use alloc::vec;
219
220    #[test_log::test]
221    fn test_shared_no_copy_write_errors_until_cow() {
222        let mut shared = Bytes::from_elems(vec![1u8, 2, 3, 4]).shared();
223
224        // Still shared: a no-copy write must refuse (it would trigger copy-on-write).
225        assert_eq!(
226            shared.write(Writer::new().no_copy()).err(),
227            Some(AccessError::WouldCopy)
228        );
229        // A no-copy *read* of shared data is always fine (zero-copy view).
230        assert_eq!(shared.read(Reader::new().no_copy()).unwrap(), &[1, 2, 3, 4]);
231
232        // A copy-allowed write triggers copy-on-write...
233        shared.write(Writer::new()).unwrap()[0] = 9;
234        // ...after which the buffer is private and a no-copy write succeeds.
235        assert!(shared.write(Writer::new().no_copy()).is_ok());
236        assert_eq!(&shared[..], &[9, 2, 3, 4]);
237    }
238
239    #[test_log::test]
240    fn test_shared_is_zero_copy_view() {
241        let bytes = Bytes::from_elems(vec![1u8, 2, 3, 4]);
242        let shared = bytes.shared();
243        assert_eq!(&shared[..], &[1, 2, 3, 4]);
244        assert_eq!(shared.len(), 4);
245    }
246
247    #[test_log::test]
248    fn test_shared_clone_is_cheap_and_equal() {
249        let shared = Bytes::from_elems(vec![10u32, 20, 30]).shared();
250        let clone = shared.clone();
251        assert_eq!(&shared[..], &clone[..]);
252    }
253
254    #[test_log::test]
255    fn test_shared_try_into_vec_never_succeeds() {
256        let shared = Bytes::from_elems(vec![1u8, 2, 3, 4]).shared();
257        assert!(shared.try_into_vec::<u8>().is_err());
258    }
259
260    #[test_log::test]
261    fn test_shared_split() {
262        let shared = Bytes::from_elems(vec![0u8, 1, 2, 3, 4, 5, 6, 7]).shared();
263        let (left, right) = shared.split(3, SplitPolicy::Shared).unwrap();
264        assert_eq!(&left[..], &[0, 1, 2]);
265        assert_eq!(&right[..], &[3, 4, 5, 6, 7]);
266    }
267
268    #[test_log::test]
269    fn test_shared_split_then_clone() {
270        let shared = Bytes::from_elems(vec![0u8, 1, 2, 3, 4, 5]).shared();
271        let (left, right) = shared.split(2, SplitPolicy::Shared).unwrap();
272        assert_eq!(&left.clone()[..], &[0, 1]);
273        assert_eq!(&right.clone()[..], &[2, 3, 4, 5]);
274    }
275
276    #[test_log::test]
277    fn test_shared_view_is_zero_copy() {
278        let shared = Bytes::from_elems(vec![0u8, 1, 2, 3, 4, 5]).shared();
279        let view = shared.view(1, 4).unwrap();
280        assert_eq!(&view[..], &[1, 2, 3]);
281        // The original is untouched and the window can't be detached into a Vec.
282        assert_eq!(&shared[..], &[0, 1, 2, 3, 4, 5]);
283        assert!(view.try_into_vec::<u8>().is_err());
284    }
285
286    #[test_log::test]
287    fn test_shared_copy_on_write() {
288        let shared = Bytes::from_elems(vec![1u8, 2, 3, 4]).shared();
289        let mut clone = shared.clone();
290        clone[0] = 99;
291
292        // The mutation is private to the clone, the original is untouched.
293        assert_eq!(&clone[..], &[99, 2, 3, 4]);
294        assert_eq!(&shared[..], &[1, 2, 3, 4]);
295    }
296
297    #[test_log::test]
298    fn test_shared_extend() {
299        let mut shared = Bytes::from_elems(vec![1u8, 2, 3]).shared();
300        shared.extend_from_byte_slice(&[4, 5, 6]);
301        assert_eq!(&shared[..], &[1, 2, 3, 4, 5, 6]);
302    }
303}