Skip to main content

rten_gemm/
im2col.rs

1use std::mem::MaybeUninit;
2use std::ops::Range;
3
4use rten_base::byte_cast::{AsBytes, cast_uninit_mut_slice};
5use rten_simd::ops::{BitOps, MaskOps, NumOps};
6use rten_simd::{Isa, Mask, Simd};
7use rten_tensor::{NdTensorView, Storage};
8
9use super::packing::int8::{PackedBMeta, shift_cast_i8_u8};
10
11/// Maps rows of an [`Im2Col`] matrix to locations in the source image.
12///
13/// For efficiency when packing the image, the locations are premultiplied by
14/// the corresponding stride.
15pub struct RowOffsets {
16    /// Map of row index to `channel * channel_stride`.
17    pub chan: Vec<i32>,
18
19    /// Map of row index to `kernel_y * dilation_y * row_stride`.
20    pub y: Vec<i32>,
21
22    /// Map of row index to `kernel_x * dilation_x * col_stride`.
23    pub x: Vec<i32>,
24}
25
26/// Maps columns of an [`Im2Col`] matrix to locations in the source image.
27///
28/// For efficiency when packing the image, the locations are premultiplied by
29/// the corresponding stride.
30pub struct ColOffsets {
31    /// Map of column index to `row * row_stride` where `row` is the top Y
32    /// coordinate of the patch in the source image.
33    pub y: Vec<i32>,
34
35    /// Map of column index to `col * col_stride` where `col` is the left X
36    /// coordinate of the patch in the source image.
37    pub x: Vec<i32>,
38}
39
40/// A matrix formed by unrolling patches of an image into columns.
41///
42/// Each column of the matrix corresponds to a different spatial patch of the
43/// image, and each row is a different location within the patch. The matrix
44/// can be used as the right-hand input of a matrix multiplication in order
45/// to perform a convolution.
46///
47/// The input image has shape [C, H, W] and is transformed into a matrix with
48/// shape [C * Kh * kW, Oh * Ow] where Kh/Kw are convolution kernel sizes and
49/// Oh/Ow are the number of patches in the Y and X directions. Given a weight
50/// matrix W of shape `[M, C * Kh * kW]` the matrix multiplication `W @
51/// im2col(image)` produces an output of shape `[M, Oh * Ow]` which can be
52/// reshaped into the convolution output `[M, Oh, Ow]`.
53///
54/// The matrix is _virtual_ as it is not materialized fully in memory. Instead
55/// blocks of the matrix are materialized during computation.
56pub struct Im2Col<'a, T> {
57    pub image: NdTensorView<'a, T, 3>,
58
59    /// Map of im2col row index to position within image patch (channel,
60    /// kernel_y, kernel_x) pre-multiplied by corresponding stride.
61    ///
62    /// The arrays may be padded to a multiple of a step size specified by the
63    /// GEMM kernel. `n_rows` contains the actual number of rows in the virtual
64    /// matrix.
65    pub row_offsets: RowOffsets,
66
67    /// Map of im2col column index to (y, x) coordinate of top-level corner of
68    /// patch in image, pre-multiplied by corresponding stride.
69    ///
70    /// The arrays may be padded to a multiple of a step size specified by the
71    /// GEMM kernel. `n_cols` contains the actual number of columns in the
72    /// virtual matrix.
73    pub col_offsets: ColOffsets,
74
75    /// Number of columns in the im2col matrix.
76    pub n_cols: usize,
77
78    /// Number of rows in the im2col matrix.
79    pub n_rows: usize,
80
81    /// Maximum valid sum of `row_offsets.y + col_offsets.y`. Values above this
82    /// correspond to the padding region.
83    pub max_y_offset: i32,
84
85    /// Maximum valid sum of `row_offsets.x + col_offsets.x`. Values above this
86    /// correspond to the padding region.
87    pub max_x_offset: i32,
88}
89
90impl<T: Copy + Default> Im2Col<'_, T> {
91    /// Return the number of rows in the im2col matrix.
92    pub fn rows(&self) -> usize {
93        self.n_rows
94    }
95
96    /// Return the number of columns in the im2col matrix.
97    pub fn cols(&self) -> usize {
98        self.n_cols
99    }
100
101    /// Pack part of an image into a packing buffer.
102    ///
103    /// This method is for use by kernels using the "standard" packing buffer
104    /// layout for the B / RHS input.
105    ///
106    /// `NR_REGS` specifies the width of each column panel as a multiple of
107    /// `S::LEN` elements. In other words, `panel_width` must exactly equal
108    /// `NR_REGS * S::LEN`.
109    #[inline(always)]
110    pub(super) fn pack_block<I: Isa, const NR_REGS: usize>(
111        &self,
112        isa: I,
113        out: &mut [MaybeUninit<T>],
114        panel_width: usize,
115        rows: Range<usize>,
116        cols: Range<usize>,
117    ) {
118        let ops = isa.i32();
119        let mask_ops = isa.m32();
120
121        assert_eq!(panel_width, ops.len() * NR_REGS);
122
123        let col_range = cols.start..cols.end.next_multiple_of(panel_width);
124        let used_size = rows.len() * col_range.len();
125        assert_eq!(out.len(), used_size);
126
127        let col_y_offsets = &self.col_offsets.y[col_range.clone()];
128        let col_x_offsets = &self.col_offsets.x[col_range.clone()];
129        let row_chan_offsets = &self.row_offsets.chan[rows.clone()];
130        let row_y_offsets = &self.row_offsets.y[rows.clone()];
131        let row_x_offsets = &self.row_offsets.x[rows.clone()];
132
133        let img_data = self.image.storage();
134
135        // Compute max valid image buffer offset. Used to clamp generated offsets
136        // as a form of bounds check.
137        let img_len = self.image.storage().len();
138        assert!(img_len > 0 && img_len <= i32::MAX as usize);
139        let max_img_offset = ops.splat(img_len as i32 - 1);
140
141        // Loop over column panels, then rows, then SIMD-wide column groups
142        // within each panel.
143        let mut out_offset = 0;
144
145        for start_col in (0..col_y_offsets.len()).step_by(ops.len() * NR_REGS) {
146            let col_y_offset: [I::I32; NR_REGS] =
147                std::array::from_fn(|i| ops.load(&col_y_offsets[start_col + ops.len() * i..]));
148            let col_x_offset: [I::I32; NR_REGS] =
149                std::array::from_fn(|i| ops.load(&col_x_offsets[start_col + ops.len() * i..]));
150            let max_x_offset = ops.splat(self.max_x_offset);
151            let max_y_offset = ops.splat(self.max_y_offset);
152
153            for ((&row_chan_offset, &row_y_offset), &row_x_offset) in row_chan_offsets
154                .iter()
155                .zip(row_y_offsets.iter())
156                .zip(row_x_offsets.iter())
157            {
158                let row_chan_offset = ops.splat(row_chan_offset);
159                let row_y_offset = ops.splat(row_y_offset);
160                let row_x_offset = ops.splat(row_x_offset);
161
162                for i in 0..NR_REGS {
163                    let y_offset = ops.add(col_y_offset[i], row_y_offset);
164                    let x_offset = ops.add(col_x_offset[i], row_x_offset);
165
166                    let offsets = ops.add(ops.add(row_chan_offset, y_offset), x_offset);
167
168                    // Ensure offsets cannot be out of bounds even if row /
169                    // column offsets were calculated incorrectly.
170                    let offsets = ops.min(ops.max(offsets, ops.zero()), max_img_offset);
171
172                    // Create mask to specify offsets which are valid. Others
173                    // correspond to the padding region.
174                    let zero = ops.zero();
175
176                    let y_valid =
177                        mask_ops.and(ops.ge(y_offset, zero), ops.le(y_offset, max_y_offset));
178                    let x_valid =
179                        mask_ops.and(ops.ge(x_offset, zero), ops.le(x_offset, max_x_offset));
180                    let pad_mask = mask_ops.and(y_valid, x_valid);
181
182                    // Set offsets to zero for padding elements. We require
183                    // this offset is always valid.
184                    let offsets_array = ops.select(offsets, zero, pad_mask).to_array();
185                    let pad_mask_array = pad_mask.to_array();
186
187                    // Gather elements and store in packing buffer.
188                    for idx in 0..ops.len() {
189                        // Safety: offsets_array[idx] is a valid offset.
190                        let src_elem =
191                            unsafe { *img_data.get_unchecked(offsets_array[idx] as usize) };
192
193                        // This should be compiled to a conditional move.
194                        let elem = if pad_mask_array[idx] {
195                            src_elem
196                        } else {
197                            T::default()
198                        };
199
200                        // Safety: `out_offset + i` is valid for `i < ops.len()`.
201                        let out_el = unsafe { out.get_unchecked_mut(out_offset + idx) };
202                        out_el.write(elem);
203                    }
204
205                    out_offset += ops.len();
206                }
207            }
208        }
209
210        // Check we initialized as many elements as used.
211        assert_eq!(out_offset, used_size);
212    }
213}
214
215impl Im2Col<'_, i8> {
216    /// Pack part of an image into a packing buffer.
217    ///
218    /// The packing buffer contains panels of size `KC * NR`, where `NR` is
219    /// `NR_REGS` times the i32 SIMD width.
220    ///
221    /// This method is for use by kernels using int8 dot product instructions
222    /// to compute `S::LEN x i32` dot products from two input vectors each
223    /// containing `S::LEN x 4 x i8` (or u8) inputs.
224    #[inline(always)]
225    #[allow(unused)] // Some architectures only
226    pub(super) fn pack_block_i8_dot<
227        I: Isa,
228        const NR: usize,
229        const NR_REGS: usize,
230        const K_TILE: usize,
231    >(
232        &self,
233        isa: I,
234        out: &mut [MaybeUninit<i8>],
235        rows: Range<usize>,
236        cols: Range<usize>,
237        zero_point: i8,
238    ) {
239        self.pack_block_int8::<_, NR, NR_REGS, K_TILE, false>(isa, out, rows, cols, zero_point);
240    }
241
242    /// Variant of [`pack_block_i8_dot`](Self::pack_block_i8_dot) which shifts
243    /// i8 values to u8 by adding 128.
244    #[inline(always)]
245    #[allow(unused)] // Some architectures only
246    pub(super) fn pack_block_i8_dot_cast_u8<
247        I: Isa,
248        const NR: usize,
249        const NR_REGS: usize,
250        const K_TILE: usize,
251    >(
252        &self,
253        isa: I,
254        out: &mut [MaybeUninit<u8>],
255        rows: Range<usize>,
256        cols: Range<usize>,
257        zero_point: i8,
258    ) {
259        let out = cast_uninit_mut_slice(out).unwrap();
260        self.pack_block_int8::<_, NR, NR_REGS, K_TILE, true>(isa, out, rows, cols, zero_point);
261    }
262
263    #[inline(always)]
264    fn pack_block_int8<
265        I: Isa,
266        const NR: usize,
267        const NR_REGS: usize,
268        const K_TILE: usize,
269        const CAST_B_U8: bool,
270    >(
271        &self,
272        isa: I,
273        out: &mut [MaybeUninit<i8>],
274        rows: Range<usize>,
275        cols: Range<usize>,
276        zero_point: i8,
277    ) {
278        let ops = isa.i32();
279        assert_eq!(ops.len() * NR_REGS, NR);
280
281        let mask_ops = isa.m32();
282
283        debug_assert!(rows.end <= self.rows());
284        debug_assert!(cols.end <= self.cols());
285
286        let max_x_offset = ops.splat(self.max_x_offset);
287        let max_y_offset = ops.splat(self.max_y_offset);
288
289        let col_x_offsets = &self.col_offsets.x;
290        debug_assert_eq!(col_x_offsets.len() % ops.len(), 0);
291
292        let col_y_offsets = &self.col_offsets.y;
293        debug_assert_eq!(col_y_offsets.len() % ops.len(), 0);
294
295        let row_x_offsets = &self.row_offsets.x;
296        debug_assert_eq!(row_x_offsets.len() % K_TILE, 0);
297
298        let row_y_offsets = &self.row_offsets.y;
299        debug_assert_eq!(row_y_offsets.len() % K_TILE, 0);
300
301        let row_chan_offsets = &self.row_offsets.chan;
302        debug_assert_eq!(row_chan_offsets.len() % K_TILE, 0);
303
304        let img_data = self.image.storage();
305
306        let mut out_offset = 0;
307
308        for start_col in cols.step_by(ops.len() * NR_REGS) {
309            let col_y_offset: [I::I32; NR_REGS] =
310                std::array::from_fn(|i| ops.load(&col_y_offsets[start_col + i * ops.len()..]));
311            let col_x_offset: [I::I32; NR_REGS] =
312                std::array::from_fn(|i| ops.load(&col_x_offsets[start_col + i * ops.len()..]));
313            let zero = ops.zero();
314
315            let mut col_sums = [ops.zero().to_array(); NR_REGS];
316
317            for start_row in rows.clone().step_by(K_TILE) {
318                for i in 0..K_TILE {
319                    let k = start_row + i;
320                    let row_x_offset = ops.splat(unsafe { *row_x_offsets.get_unchecked(k) });
321                    let row_y_offset = ops.splat(unsafe { *row_y_offsets.get_unchecked(k) });
322                    let row_chan_offset = ops.splat(unsafe { *row_chan_offsets.get_unchecked(k) });
323
324                    for c_block in 0..NR_REGS {
325                        let x_offsets = ops.add(row_x_offset, col_x_offset[c_block]);
326                        let y_offsets = ops.add(row_y_offset, col_y_offset[c_block]);
327                        let offsets = ops.add(ops.add(x_offsets, y_offsets), row_chan_offset);
328
329                        let y_valid =
330                            mask_ops.and(ops.ge(y_offsets, zero), ops.le(y_offsets, max_y_offset));
331                        let x_valid =
332                            mask_ops.and(ops.ge(x_offsets, zero), ops.le(x_offsets, max_x_offset));
333                        let pad_mask = mask_ops.and(y_valid, x_valid);
334                        let pad_mask_array = pad_mask.to_array();
335
336                        // Set offsets to zero for padding elements. We require
337                        // this offset is always valid.
338                        let offsets_array = ops.select(offsets, zero, pad_mask).to_array();
339
340                        for idx in 0..ops.len() {
341                            let out_elem = unsafe {
342                                out.get_unchecked_mut(
343                                    out_offset + (c_block * ops.len() + idx) * K_TILE + i,
344                                )
345                            };
346                            let src_elem =
347                                unsafe { *img_data.get_unchecked(offsets_array[idx] as usize) };
348
349                            if CAST_B_U8 {
350                                let src_elem = shift_cast_i8_u8(src_elem);
351                                let elem = if pad_mask_array[idx] { src_elem } else { 0 };
352                                col_sums[c_block][idx] += elem as i32;
353                                out_elem.write(elem as i8);
354                            } else {
355                                let elem = if pad_mask_array[idx] { src_elem } else { 0 };
356                                col_sums[c_block][idx] += elem as i32;
357                                out_elem.write(elem);
358                            }
359                        }
360                    }
361                }
362                out_offset += ops.len() * NR_REGS * K_TILE;
363            }
364
365            // Store column sums and zero points at end of each panel.
366            let meta = PackedBMeta::<NR> {
367                // Safety: col_sums is `[i32_vec_len; NR_REGS]` and we checked
368                // `i32_vec_len * NR_REGS == NR`.
369                col_sums: *unsafe {
370                    std::mem::transmute::<&[<I::I32 as Simd>::Array; NR_REGS], &[i32; NR]>(
371                        &col_sums,
372                    )
373                },
374                zero_points: if CAST_B_U8 {
375                    [shift_cast_i8_u8(zero_point) as i32; NR]
376                } else {
377                    [zero_point as i32; NR]
378                },
379            };
380            let meta_bytes = meta.as_bytes();
381            for (i, byte) in meta_bytes.iter().enumerate() {
382                out[out_offset + i].write(*byte as i8);
383            }
384            out_offset += meta_bytes.len();
385        }
386
387        // Sanity check
388        assert_eq!(out_offset, out.len());
389    }
390}