Skip to main content

cubecl_server/
allocator.rs

1use crate::{
2    memory_management::optimal_align,
3    server::{
4        Handle, MemoryLayout, MemoryLayoutDescriptor, MemoryLayoutPolicy, MemoryLayoutStrategy,
5    },
6};
7use alloc::vec::Vec;
8use cubecl_common::device::ServiceId;
9use cubecl_environment::stream::StreamId;
10use cubecl_zspace::{Shape, Strides, strides};
11
12/// Allocators where every allocations is with contiguous memory.
13pub struct ContiguousMemoryLayoutPolicy {
14    mem_alignment: usize,
15}
16
17/// Allocators where some allocations can leverage a pitched layout.
18pub struct PitchedMemoryLayoutPolicy {
19    mem_alignment: usize,
20}
21
22impl MemoryLayoutPolicy for PitchedMemoryLayoutPolicy {
23    fn apply(
24        &self,
25        service: ServiceId,
26        stream_id: StreamId,
27        descriptors: &[MemoryLayoutDescriptor],
28    ) -> (Handle, Vec<MemoryLayout>) {
29        let mut total_size = 0u64;
30
31        let (sizes, strides): (Vec<_>, Vec<_>) = descriptors
32            .iter()
33            .map(|descriptor| {
34                let last_dim = descriptor.shape.last().copied().unwrap_or(1);
35                let pitch_align = match descriptor.strategy {
36                    MemoryLayoutStrategy::Contiguous => 1,
37                    MemoryLayoutStrategy::Optimized => {
38                        optimal_align(last_dim, descriptor.elem_size, self.mem_alignment)
39                    }
40                };
41
42                let rank = descriptor.shape.len();
43                let width = *descriptor.shape.last().unwrap_or(&1);
44                let height: usize = descriptor.shape.iter().rev().skip(1).product();
45                let height = Ord::max(height, 1);
46
47                let width_bytes = width * descriptor.elem_size;
48                let pitch = width_bytes.next_multiple_of(pitch_align);
49                let size = height * pitch;
50
51                let mut strides = strides![1; rank];
52                if rank > 1 {
53                    strides[rank - 2] = pitch / descriptor.elem_size;
54                }
55                if rank > 2 {
56                    for i in (0..rank - 2).rev() {
57                        strides[i] = strides[i + 1] * descriptor.shape[i + 1];
58                    }
59                }
60                total_size += size.next_multiple_of(self.mem_alignment) as u64;
61                (size, strides)
62            })
63            .unzip();
64
65        let base_handle = Handle::new(service, stream_id, total_size);
66
67        let layouts = offset_handles(base_handle.clone(), &sizes, self.mem_alignment)
68            .into_iter()
69            .zip(strides)
70            .map(|(handle, strides)| MemoryLayout::new(handle, strides))
71            .collect();
72        (base_handle, layouts)
73    }
74}
75
76impl ContiguousMemoryLayoutPolicy {
77    /// Creates a new allocator with the given memory alignment.
78    pub fn new(mem_alignment: usize) -> Self {
79        Self { mem_alignment }
80    }
81}
82
83impl PitchedMemoryLayoutPolicy {
84    /// Creates a new allocator with the given memory alignment.
85    pub fn new(mem_alignment: usize) -> Self {
86        Self { mem_alignment }
87    }
88}
89
90impl MemoryLayoutPolicy for ContiguousMemoryLayoutPolicy {
91    fn apply(
92        &self,
93        service: ServiceId,
94        stream_id: StreamId,
95        descriptors: &[MemoryLayoutDescriptor],
96    ) -> (Handle, Vec<MemoryLayout>) {
97        let mut total_size = 0u64;
98        let (sizes, strides): (Vec<_>, Vec<_>) = descriptors
99            .iter()
100            .map(|desc| {
101                let size = desc.shape.iter().product::<usize>() * desc.elem_size;
102                total_size += size.next_multiple_of(self.mem_alignment) as u64;
103                (size, contiguous_strides(&desc.shape))
104            })
105            .unzip();
106
107        let base_handle = Handle::new(service, stream_id, total_size);
108
109        let layouts = offset_handles(base_handle.clone(), &sizes, self.mem_alignment)
110            .into_iter()
111            .zip(strides)
112            .map(|(handle, stride)| MemoryLayout::new(handle, stride))
113            .collect();
114
115        (base_handle, layouts)
116    }
117}
118
119pub(crate) fn contiguous_strides(shape: &Shape) -> Strides {
120    let rank = shape.len();
121    let mut strides = strides![1; rank];
122    for i in (0..rank - 1).rev() {
123        strides[i] = strides[i + 1] * shape[i + 1];
124    }
125    strides
126}
127
128/// Take a list of sub-slices of a buffer and create a list of offset handles.
129/// Sizes must be in bytes and handles will be aligned to the memory alignment.
130pub fn offset_handles(
131    base_handle: Handle,
132    sizes_bytes: &[usize],
133    buffer_align: usize,
134) -> Vec<Handle> {
135    let total_size = base_handle.size() as usize;
136    let mut offset = 0;
137    let mut out = Vec::new();
138
139    for size in sizes_bytes {
140        let handle = base_handle
141            .clone()
142            .offset_start(offset as u64)
143            .offset_end((total_size - offset - size) as u64);
144        out.push(handle);
145        offset += size.next_multiple_of(buffer_align);
146    }
147
148    out
149}
150
151/// The 2D geometry of a copy to or from a pitched allocation: how wide each
152/// row is, how many rows there are, and the stride between their starts.
153///
154/// [`of`](Self::of) answers `None` for an allocation that needs no 2D copy at
155/// all. A row stride equal to the row width means no padding, so the whole
156/// buffer is one contiguous span and the plain linear copy is both correct and
157/// faster — drivers also refuse the 2D form for very tall transfers, an
158/// embedding table's 128k rows say, which the linear path handles.
159///
160/// This is the read side of what [`PitchedMemoryLayoutPolicy`] produces, and
161/// it is the same arithmetic whichever driver performs the copy.
162#[derive(Debug, Clone, Copy, PartialEq, Eq)]
163pub struct Pitch {
164    /// The bytes in one row, which is what both sides of the copy transfer.
165    pub width_bytes: usize,
166    /// How many rows the copy moves.
167    pub height: usize,
168    /// The bytes between the starts of two rows on the pitched side.
169    pub stride_bytes: usize,
170}
171
172impl Pitch {
173    /// The pitch of an allocation with this layout, or `None` when its rows
174    /// are contiguous.
175    ///
176    /// The caller has already validated the strides as pitched row-major; this
177    /// only asks whether there is padding to step over.
178    pub fn of(shape: &[usize], strides: &[usize], elem_size: usize) -> Option<Self> {
179        let rank = shape.len();
180        let width = *shape.last().unwrap_or(&1);
181        if rank < 2 || strides[rank - 2] == width {
182            return None;
183        }
184        Some(Self {
185            width_bytes: width * elem_size,
186            height: shape.iter().rev().skip(1).product(),
187            stride_bytes: strides[rank - 2] * elem_size,
188        })
189    }
190}
191
192#[cfg(test)]
193mod pitch_tests {
194    use super::Pitch;
195
196    /// A contiguous buffer has no pitch, so a copy of it is one linear span.
197    ///
198    /// The distinction is not an optimization: drivers refuse the 2D form for
199    /// very tall transfers, and the linear path is what handles those.
200    #[test]
201    fn contiguous_rows_have_no_pitch() {
202        // A row stride equal to the row width is exactly no padding.
203        assert_eq!(Pitch::of(&[4, 8], &[8, 1], 4), None);
204        // Rank 0 and 1 have no second-to-last dimension to be padded.
205        assert_eq!(Pitch::of(&[8], &[1], 4), None);
206        assert_eq!(Pitch::of(&[], &[], 4), None);
207    }
208
209    /// A padded buffer reports the geometry the 2D copy needs, in bytes.
210    ///
211    /// Every one of the three is a byte count the driver indexes with; a wrong
212    /// one scrambles the rows rather than failing, which is why this is worth
213    /// checking away from a device.
214    #[test]
215    fn padded_rows_report_width_height_and_stride() {
216        // 4 rows of 8 f32, padded to a stride of 12 elements.
217        let pitch = Pitch::of(&[4, 8], &[12, 1], 4).expect("a padded row is a pitch");
218        assert_eq!(pitch.width_bytes, 8 * 4);
219        assert_eq!(pitch.stride_bytes, 12 * 4);
220        assert_eq!(pitch.height, 4);
221    }
222
223    /// The height is every dimension but the last, so a rank-3 buffer's rows
224    /// are counted across the leading dimensions rather than only the middle.
225    #[test]
226    fn height_counts_every_row_not_only_the_last_dimension() {
227        let pitch = Pitch::of(&[2, 3, 8], &[36, 12, 1], 4).expect("a padded row is a pitch");
228        assert_eq!(pitch.height, 6);
229        assert_eq!(pitch.width_bytes, 8 * 4);
230        assert_eq!(pitch.stride_bytes, 12 * 4);
231    }
232
233    /// The element size scales all three, so the same layout of a wider
234    /// element is the same geometry in more bytes.
235    #[test]
236    fn the_geometry_is_bytes_not_elements() {
237        let narrow = Pitch::of(&[4, 8], &[12, 1], 1).expect("a padded row is a pitch");
238        let wide = Pitch::of(&[4, 8], &[12, 1], 8).expect("a padded row is a pitch");
239        assert_eq!(wide.width_bytes, narrow.width_bytes * 8);
240        assert_eq!(wide.stride_bytes, narrow.stride_bytes * 8);
241        assert_eq!(wide.height, narrow.height);
242    }
243}