Skip to main content

cubek_std/
matrix_layout.rs

1use cubecl::{
2    prelude::*,
3    quant::scheme::QuantScheme,
4    zspace::{Strides, strides},
5};
6
7use crate::InvalidConfigError;
8
9#[derive(CubeType, Copy, Clone, PartialEq, Eq, Hash, Debug, Default)]
10/// Layout of a 2D structure such as a tensor, shared memory or slice,
11/// used within any matmul kernel level
12pub enum MatrixLayout {
13    #[default]
14    RowMajor,
15    ColMajor,
16}
17
18impl MatrixLayout {
19    pub fn from_shape_and_strides(
20        shape: &[usize],
21        strides: &[usize],
22        scheme: Option<&QuantScheme>,
23    ) -> Result<Self, InvalidConfigError> {
24        assert!(
25            shape.len() >= 2 && shape.len() == strides.len(),
26            "Shape/stride mismatch or not a matrix"
27        );
28
29        if let Some(packing_dim) = scheme.and_then(|s| s.packing_dim()) {
30            if packing_dim == 0 {
31                return Ok(MatrixLayout::RowMajor);
32            }
33            if packing_dim == 1 {
34                return Ok(MatrixLayout::ColMajor);
35            }
36
37            return Err(Box::new(format!(
38                "Invalid or non-contiguous matrix layout: packing_dim={packing_dim:?}"
39            )));
40        }
41
42        let n = shape.len();
43
44        let outer = shape[n - 2];
45        let inner = shape[n - 1];
46
47        let stride_outer = strides[n - 2];
48        let stride_inner = strides[n - 1];
49
50        // These checks are actually broken for quantized inputs (and are not trivially fixable).
51        // For quantized tensors the quantized axis will probably need to be stored, since it can be
52        // hard to tell on which axis it is packed.
53        // The packed axis is always the contiguous one. One test case has a logical shape of [4, 4]
54        // for example, with strides of [1, 1]. It is not possible to determine the packing dimension
55        // accurately for this problem.
56
57        // A dimension of size 1 is only ever indexed at 0, so its stride is never
58        // added to an offset and cannot make the matrix non-contiguous. Reading it
59        // as evidence of the opposite layout would disagree with
60        // `matrix_batch_layout`, the classifier the matmul autotune key is built
61        // from: a `[1, k]` row vector carrying strides `[1, 1]` is contiguous there
62        // but would land here as col major, so a plan tuned for one layout gets
63        // replayed on the other.
64
65        // Row-major: inner dimension is contiguous
66        if (stride_inner == 1) && (outer == 1 || stride_outer >= inner) {
67            return Ok(MatrixLayout::RowMajor);
68        }
69
70        // Col-major: outer dimension is contiguous
71        if (stride_outer == 1) && (inner == 1 || stride_inner >= outer) {
72            return Ok(MatrixLayout::ColMajor);
73        }
74
75        Err(Box::new(format!(
76            "Invalid or non-contiguous matrix layout: shape={shape:?}, strides={strides:?}",
77        )))
78    }
79
80    pub fn to_strides(&self, shape: &[usize]) -> Strides {
81        assert!(shape.len() >= 2, "Shape must have at least 2 dimensions");
82
83        let n = shape.len();
84        let mut strides = strides![0; n];
85
86        // Start with contiguous layout for last two dims
87        match self {
88            MatrixLayout::RowMajor => {
89                strides[n - 1] = 1; // inner dim contiguous
90                strides[n - 2] = shape[n - 1]; // outer stride = inner size
91            }
92            MatrixLayout::ColMajor => {
93                strides[n - 2] = 1; // outer dim contiguous
94                strides[n - 1] = shape[n - 2]; // inner stride = outer size
95            }
96        }
97
98        // Batch dims: contiguous
99        for i in (0..n - 2).rev() {
100            strides[i] = strides[i + 1] * shape[i + 1];
101        }
102
103        strides
104    }
105}
106
107#[cfg(feature = "testing")]
108impl From<MatrixLayout> for cubek_test_utils::StridedLayout {
109    fn from(layout: MatrixLayout) -> Self {
110        match layout {
111            MatrixLayout::RowMajor => Self::RowMajor,
112            MatrixLayout::ColMajor => Self::ColMajor,
113        }
114    }
115}
116
117#[cfg(feature = "testing")]
118impl From<MatrixLayout> for cubek_test_utils::LayoutSpec {
119    fn from(layout: MatrixLayout) -> Self {
120        cubek_test_utils::StridedLayout::from(layout).into()
121    }
122}
123
124#[cube]
125/// Maps the matmul MatrixLayout to cmma's MatrixLayout, for use in Cmma API.
126pub fn as_cmma_layout(#[comptime] layout: MatrixLayout) -> cmma::MatrixLayout {
127    match layout {
128        MatrixLayout::RowMajor => cmma::MatrixLayout::RowMajor,
129        MatrixLayout::ColMajor => cmma::MatrixLayout::ColMajor,
130    }
131}
132
133#[cube]
134/// Maps the cmma's MatrixLayout to matmul MatrixLayout.
135pub fn from_cmma_layout(#[comptime] layout: cmma::MatrixLayout) -> comptime_type!(MatrixLayout) {
136    match layout {
137        cmma::MatrixLayout::RowMajor => MatrixLayout::RowMajor,
138        cmma::MatrixLayout::ColMajor => MatrixLayout::ColMajor,
139        cmma::MatrixLayout::Undefined => MatrixLayout::RowMajor,
140    }
141}
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146
147    fn layout(shape: &[usize], strides: &[usize]) -> Result<MatrixLayout, String> {
148        MatrixLayout::from_shape_and_strides(shape, strides, None).map_err(|err| err.to_string())
149    }
150
151    #[test]
152    fn contiguous_is_row_major() {
153        assert_eq!(layout(&[4, 8], &[8, 1]).unwrap(), MatrixLayout::RowMajor);
154    }
155
156    #[test]
157    fn transposed_is_col_major() {
158        assert_eq!(layout(&[4, 8], &[1, 4]).unwrap(), MatrixLayout::ColMajor);
159    }
160
161    #[test]
162    fn pitched_rows_are_row_major() {
163        // A padded row stride is still row major: rows never overlap.
164        assert_eq!(layout(&[4, 8], &[16, 1]).unwrap(), MatrixLayout::RowMajor);
165    }
166
167    #[test]
168    fn single_row_is_row_major() {
169        // `[k, 1]` transposed: the row stride is 1 because there is only ever one
170        // row, which must not be read as col major.
171        assert_eq!(layout(&[1, 8], &[1, 1]).unwrap(), MatrixLayout::RowMajor);
172    }
173
174    #[test]
175    fn single_column_is_col_major() {
176        // A column of a col-major matrix: the inner stride is below the row count
177        // only because the single column is never advanced past.
178        assert_eq!(layout(&[8, 1], &[1, 4]).unwrap(), MatrixLayout::ColMajor);
179    }
180
181    #[test]
182    fn overlapping_strides_are_rejected() {
183        assert!(layout(&[4, 8], &[2, 1]).is_err());
184    }
185
186    #[test]
187    fn batches_do_not_change_the_matrix_layout() {
188        assert_eq!(
189            layout(&[2, 4, 8], &[32, 8, 1]).unwrap(),
190            MatrixLayout::RowMajor
191        );
192        assert_eq!(
193            layout(&[2, 4, 8], &[32, 1, 4]).unwrap(),
194            MatrixLayout::ColMajor
195        );
196    }
197}