Skip to main content

cubecl_std/tensor/contiguous/
perpendicular.rs

1use cubecl::prelude::*;
2use cubecl_core::{
3    self as cubecl, calculate_cube_count_elemwise, ir::VectorSize, tensor_vector_size_parallel,
4    tensor_vector_size_perpendicular,
5};
6use std::cmp::min;
7
8/// Copies a tensor whose unit-stride axis is not the last one into an output whose last axis
9/// is, by transposing `vector_size × vector_size` blocks in registers.
10///
11/// For each block, a unit gathers `vector_size` vectors along the input's unit-stride axis,
12/// transposes them, and stores `vector_size` vectors along the output's last axis. A unit
13/// handles `blocks_per_unit` blocks in a row along the last axis.
14///
15/// A row is one block's worth of `axis` in one batch, where every axis but `axis` and the last
16/// is a batch. Units walk the last axis fastest, so stores are coalesced across a cube, while
17/// the loads of neighbouring rows fill the same cache lines.
18#[cube(launch_unchecked, address_type = "dynamic")]
19fn copy_perpendicular<T: Numeric, N: Size>(
20    input: &Tensor<Vector<T, N>>,
21    output: &mut Tensor<Vector<T, N>>,
22    blocks_last: usize,
23    blocks_axis: usize,
24    rows: usize,
25    blocks_per_unit: usize,
26    tiles_last: usize,
27    panel_rows: usize,
28    num_tiles: usize,
29    axis: usize,
30    #[comptime] rank: usize,
31    #[define(T)] _elem: ElemType,
32) {
33    let tile = CUBE_POS;
34    if tile >= num_tiles {
35        terminate!()
36    }
37
38    let vector_size = input.vector_size();
39    let last_axis = rank - 1;
40
41    // Tiles are walked a panel of `panel_rows` row tiles at a time, about one batch, so cubes
42    // running side by side read the same cache lines. Within a panel they are walked
43    // diagonally: with power-of-two strides, cubes on the same input rows would otherwise all
44    // hit the same memory channels.
45    let panel_tiles = panel_rows * tiles_last;
46    let tile_in_panel = tile % panel_tiles;
47    let tile_rows_in_panel = tile_in_panel % panel_rows;
48    let tile_rows = (tile / panel_tiles) * panel_rows + tile_rows_in_panel;
49    let tile_last = (tile_in_panel / panel_rows + tile_rows_in_panel) % tiles_last;
50    let row = tile_rows * CUBE_DIM_Y as usize + UNIT_POS_Y as usize;
51    let block_start = (tile_last * CUBE_DIM_X as usize + UNIT_POS_X as usize) * blocks_per_unit;
52    if row >= rows || block_start >= blocks_last {
53        terminate!()
54    }
55    let mut block_end = block_start + blocks_per_unit;
56    if block_end > blocks_last {
57        block_end = blocks_last;
58    }
59
60    let block_axis = row % blocks_axis;
61    let mut batch = row / blocks_axis;
62    let mut offset_input = 0;
63    let mut offset_output = 0;
64    #[unroll]
65    for i in 0..rank {
66        let dim = rank - 1 - i;
67        if dim != axis && dim != last_axis {
68            let coordinate = batch % output.shape(dim);
69            batch /= output.shape(dim);
70            offset_input += coordinate * input.stride(dim);
71            offset_output += coordinate * output.stride(dim);
72        }
73    }
74
75    let start_axis = block_axis * vector_size;
76    let input_stride = input.stride(last_axis);
77    let output_stride = output.stride(axis);
78    let index_input = (offset_input + start_axis) / vector_size;
79    let index_output = (offset_output + start_axis * output_stride) / vector_size;
80    let input_stride = input_stride / vector_size;
81    let output_stride = output_stride / vector_size;
82
83    let mut accumulators = Sequence::<Vector<T, N>>::new();
84
85    #[unroll]
86    for _ in 0..vector_size {
87        accumulators.push(Vector::empty());
88    }
89
90    for block in block_start..block_end {
91        // A block spans `vector_size` positions of the last axis: as many input strides, and a
92        // single output vector.
93        let index_input = index_input + block * vector_size * input_stride;
94        let index_output = index_output + block;
95
96        #[unroll]
97        for i in 0..vector_size {
98            let gathered = input[index_input + i * input_stride];
99
100            #[unroll]
101            for o in 0..vector_size {
102                let vector = accumulators.index_mut(o);
103                vector.insert(i, gathered.extract(o));
104            }
105        }
106
107        #[unroll]
108        for o in 0..vector_size {
109            output[index_output + o * output_stride] = accumulators[o];
110        }
111    }
112}
113
114/// Copies `input` into `output` with the perpendicular kernel when its blocks can be
115/// vectorized, returning whether it did.
116///
117/// Without vectorization a block is a single element and the kernel is a strided copy, so the
118/// caller keeps its general path for that case. `input` and `output` must have the same shape.
119pub fn try_launch_copy_perpendicular(
120    client: &Client,
121    input: &TensorBinding,
122    output: &TensorBinding,
123    dtype: ElemType,
124) -> bool {
125    // An empty tensor has nothing to copy, and no blocks to divide it into.
126    if output.shape.contains(&0) {
127        return false;
128    }
129    let (axis, vector_size) = perpendicular_vector_size(client, input, output, dtype);
130    if vector_size == 1 {
131        return false;
132    }
133
134    let rank = output.shape.len();
135    let blocks_last = output.shape[rank - 1] / vector_size;
136    let blocks_axis = output.shape[axis] / vector_size;
137    let batches =
138        output.shape.iter().product::<usize>() / (output.shape[rank - 1] * output.shape[axis]);
139    let rows = batches * blocks_axis;
140
141    let hardware = &client.properties().hardware;
142    let (blocks_per_unit, cube_dim) = if hardware.num_cpu_cores.is_some() {
143        // Cubes run one after another on a CPU, so a cube spreads rows over the threads, and
144        // each unit walks its whole row: a regular stride to prefetch, and one setup per row.
145        let cube_dim = CubeDim::new(client, rows);
146        (blocks_last, CubeDim::new_2d(1, cube_dim.num_elems()))
147    } else {
148        // A cube covers up to 8 blocks along the last axis and fills the rest of up to 256 units
149        // with rows, so its loads share cache lines while its stores stay coalesced. A side
150        // shorter than the cube shrinks it, and the other side takes the units it frees.
151        let units = prev_power_of_two(hardware.max_units_per_cube.min(256) as usize);
152        let (max_x, max_y, _) = hardware.max_cube_dim;
153        let max_x = prev_power_of_two(max_x as usize);
154        let max_y = prev_power_of_two(max_y as usize);
155        let blocks_last_pow2 = blocks_last.next_power_of_two().min(max_x);
156        let cube_x = blocks_last_pow2.min(8).min(units);
157        let cube_y = rows.next_power_of_two().min(units / cube_x).min(max_y);
158        let cube_x = blocks_last_pow2.min(units / cube_y);
159        (1, CubeDim::new_2d(cube_x as u32, cube_y as u32))
160    };
161
162    let tiles_last = blocks_last
163        .div_ceil(blocks_per_unit)
164        .div_ceil(cube_dim.x as usize);
165    let tiles_rows = rows.div_ceil(cube_dim.y as usize);
166    let panel_rows = blocks_axis.div_ceil(cube_dim.y as usize);
167    // The last panel may be short; its tiles past `rows` exit at once.
168    let num_tiles = tiles_rows.div_ceil(panel_rows) * panel_rows * tiles_last;
169    let cube_count = calculate_cube_count_elemwise(client, num_tiles, CubeDim::new_1d(1));
170    let address_type = input
171        .required_address_type(dtype.size())
172        .max(output.required_address_type(dtype.size()));
173
174    unsafe {
175        copy_perpendicular::launch_unchecked(
176            client,
177            cube_count,
178            cube_dim,
179            address_type,
180            vector_size,
181            input.clone().into_tensor_arg(),
182            output.clone().into_tensor_arg(),
183            blocks_last,
184            blocks_axis,
185            rows,
186            blocks_per_unit,
187            tiles_last,
188            panel_rows,
189            num_tiles,
190            axis,
191            rank,
192            dtype,
193        );
194    }
195
196    true
197}
198
199/// The input's unit-stride axis, and the vector size the perpendicular kernel can use.
200fn perpendicular_vector_size(
201    client: &Client,
202    input: &TensorBinding,
203    output: &TensorBinding,
204    dtype: ElemType,
205) -> (usize, VectorSize) {
206    // A unit-stride axis of size 1 has nothing to vectorize, while another unit-stride axis
207    // might.
208    let axis = input
209        .strides
210        .iter()
211        .zip(input.shape.iter())
212        .position(|(stride, shape)| *stride == 1 && *shape > 1)
213        .unwrap_or(0);
214    let rank = output.shape.len();
215
216    let vector_size_perpendicular = tensor_vector_size_perpendicular(
217        client.io_optimized_vector_sizes(dtype.size()),
218        &input.shape,
219        &input.strides,
220        rank - 1,
221    );
222    let vector_size_parallel = tensor_vector_size_parallel(
223        client.io_optimized_vector_sizes(dtype.size()),
224        &output.shape,
225        &output.strides,
226        rank - 1,
227    );
228    // The gather loads `vector_size` consecutive elements along the input's unit-stride axis,
229    // so that axis must be vectorizable on its own.
230    let vector_size_axis = tensor_vector_size_parallel(
231        client.io_optimized_vector_sizes(dtype.size()),
232        &input.shape,
233        &input.strides,
234        axis,
235    );
236    let vector_size = min(
237        min(vector_size_perpendicular, vector_size_parallel),
238        vector_size_axis,
239    );
240
241    (axis, vector_size)
242}
243
244/// The largest power of two at most `value`, which must not be 0.
245fn prev_power_of_two(value: usize) -> usize {
246    1 << value.ilog2()
247}