Skip to main content

cubecl_std/tensor/contiguous/
perpendicular.rs

1use crate::tensor::{TensorHandle, into_contiguous};
2use cubecl::prelude::*;
3use cubecl_core::{
4    self as cubecl, calculate_cube_count_elemwise, tensor_vector_size_parallel,
5    tensor_vector_size_perpendicular,
6};
7use std::cmp::min;
8
9/// Kernel for converting a non-contiguous tensor into a contiguous one when
10/// the vectorization axis is perpendicular to the last dimension.
11///
12/// This kernel handles the case where memory is laid out such that the unit-stride
13/// is not on the last dimension, requiring a "gather-and-transpose" pattern
14/// to write out contiguous vectors.
15#[cube(launch_unchecked, address_type = "dynamic")]
16fn copy_perpendicular<T: Numeric, N: Size>(
17    input: &Tensor<Vector<T, N>>,
18    output: &mut Tensor<Vector<T, N>>,
19    axis_vectorized: usize,
20    #[define(T)] _elem: ElemType,
21) {
22    let vector_size = input.vector_size();
23    let last_axis = input.rank() - 1;
24
25    // Calculate how many vectorized vectors fit into the last dimension's shape.
26    let num_batch = output.shape(last_axis) / vector_size;
27
28    // Local registers to perform a small in-register transpose.
29    let mut accumulators = Sequence::<Vector<T, N>>::new();
30
31    #[unroll]
32    for _ in 0..vector_size {
33        accumulators.push(Vector::empty());
34    }
35
36    let channel_input_stride_elem = input.stride(last_axis);
37    let channel_output_stride_elem = output.stride(axis_vectorized);
38
39    // Strides adjusted for vectorization (vector_size).
40    let channel_input_stride = channel_input_stride_elem / vector_size;
41    let channel_output_stride = channel_output_stride_elem / vector_size;
42
43    // Total parallel units needed to cover the output space.
44    let num_runs = output.len() / (num_batch * vector_size);
45
46    if ABSOLUTE_POS >= num_runs {
47        terminate!()
48    }
49
50    // Mapping the global worker ID to the specific tensor coordinates.
51    let batch_index = ABSOLUTE_POS * num_batch;
52    let skip_interval = batch_index / channel_output_stride;
53    let skip_index = batch_index % channel_output_stride;
54    let skip_size = channel_output_stride_elem;
55    let global_index = (skip_interval * skip_size) + skip_index;
56
57    for b in 0..num_batch {
58        let offset_output = global_index + b;
59
60        // Calculate the physical offset in the input tensor for the current output coordinate.
61        let mut batch_offset = 0;
62        for axis in 0..input.rank() {
63            let coordinate = output.coordinate(offset_output * vector_size, axis);
64            batch_offset += coordinate * input.stride(axis);
65        }
66        let batch_offset = batch_offset / vector_size;
67
68        // --- STEP 1: GATHER ---
69        // Load data from the input tensor. Since the data is "perpendicular",
70        // we read across the stride-1 axis to fill the accumulators.
71        #[unroll]
72        for i in 0..vector_size {
73            let index = batch_offset + i * channel_input_stride;
74            let batched = input[index];
75
76            // --- STEP 2: TRANSPOSE ---
77            // Rearrange the loaded vector components into the accumulators.
78            #[unroll]
79            for o in 0..vector_size {
80                let vector = accumulators.index_mut(o);
81                vector.insert(i, batched.extract(o));
82            }
83        }
84
85        // --- STEP 3: STORE ---
86        // Write the transposed vectors to the output in a contiguous fashion.
87        #[unroll]
88        for o in 0..vector_size {
89            let index_out = offset_output + o * channel_output_stride;
90            let batched = accumulators[o];
91
92            output[index_out] = batched;
93        }
94    }
95}
96
97/// Launches the perpendicular contiguous kernel.
98///
99/// This is used when the input tensor's memory layout is such that the last dimension
100/// is not the one with a stride of 1 (the vectorized dimension). It optimizes
101/// the copy by using hardware vectorization (Vectors) and an in-register transpose.
102pub fn launch_into_contiguous_perpendicular<R: Runtime>(
103    client: &ComputeClient<R>,
104    input: TensorBinding<R>,
105    dtype: ElemType,
106) -> TensorHandle<R> {
107    // Fallback for 1D tensors where perpendicularity doesn't apply.
108    if input.shape.len() <= 1 {
109        return into_contiguous(client, input, dtype);
110    }
111
112    let output = TensorHandle::empty(client, input.shape.to_vec(), dtype);
113    launch_copy_perpendicular_ref(client, input, output.clone().binding(), dtype);
114
115    output
116}
117
118/// Launches the perpendicular contiguous kernel.
119///
120/// This is used when the input tensor's memory layout is such that the last dimension
121/// is not the one with a stride of 1 (the vectorized dimension). It optimizes
122/// the copy by using hardware vectorization (Vectors) and an in-register transpose.
123pub fn launch_copy_perpendicular_ref<R: Runtime>(
124    client: &ComputeClient<R>,
125    input: TensorBinding<R>,
126    output: TensorBinding<R>,
127    dtype: ElemType,
128) {
129    let mut axis = 0;
130
131    for (i, stride) in input.strides.iter().enumerate() {
132        if *stride == 1 {
133            axis = i;
134            break;
135        }
136    }
137    let rank = output.shape.len();
138
139    let vector_size_perpendicular = tensor_vector_size_perpendicular(
140        client.io_optimized_vector_sizes(dtype.size()),
141        &input.shape,
142        &input.strides,
143        rank - 1,
144    );
145    let vector_size_parallel = tensor_vector_size_parallel(
146        client.io_optimized_vector_sizes(dtype.size()),
147        &output.shape,
148        &output.strides,
149        rank - 1,
150    );
151    // The gather loads `vector_size` consecutive elements along the input's unit-stride axis,
152    // so that axis must be vectorizable on its own.
153    let vector_size_axis = tensor_vector_size_parallel(
154        client.io_optimized_vector_sizes(dtype.size()),
155        &input.shape,
156        &input.strides,
157        axis,
158    );
159    let vector_size = min(
160        min(vector_size_perpendicular, vector_size_parallel),
161        vector_size_axis,
162    );
163
164    let num_elems = output.shape.iter().product::<usize>();
165    let working_units = num_elems / (vector_size as usize * output.shape[rank - 1]);
166    let cube_dim = CubeDim::new(client, working_units);
167    let cube_count = calculate_cube_count_elemwise(client, working_units, cube_dim);
168    let address_type = input
169        .required_address_type(dtype.size())
170        .max(output.required_address_type(dtype.size()));
171
172    unsafe {
173        copy_perpendicular::launch_unchecked::<R>(
174            client,
175            cube_count,
176            cube_dim,
177            address_type,
178            vector_size,
179            input.into_tensor_arg(),
180            output.into_tensor_arg(),
181            axis,
182            dtype,
183        );
184    }
185}