Skip to main content

cubek_std/tile/variants/
unit.rs

1use cubecl::std::tensor::layout::Coords2d;
2use cubecl::{self, prelude::*};
3
4use crate::tile::{
5    LOGIT_MASKED, Plane, RowWise, StridedTile, Tile, TileKind, TileKindExpand,
6    mask::{Mask, MaskExpand},
7    scope::TileScope,
8};
9
10#[derive(CubeType)]
11pub struct UnitTile<E: Numeric> {
12    pub data: Array<E>,
13    #[cube(comptime)]
14    pub layout: UnitTileLayout,
15}
16
17#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
18// Assumes row-major. If loading from a col-major source, use transposed_load=true
19pub struct UnitTileLayout {
20    pub num_rows: u32,
21    pub num_cols: u32,
22    pub transposed_load: bool,
23}
24
25impl UnitTileLayout {
26    pub const fn new(num_rows: u32, num_cols: u32, transposed_load: bool) -> UnitTileLayout {
27        UnitTileLayout {
28            num_rows,
29            num_cols,
30            transposed_load,
31        }
32    }
33}
34
35#[cube]
36impl<E: Numeric> UnitTile<E> {
37    pub fn new(#[comptime] layout: UnitTileLayout) -> UnitTile<E> {
38        let data = Array::new(comptime!(layout.num_rows * layout.num_cols) as usize);
39        UnitTile::<E> { data, layout }
40    }
41
42    pub fn zero(&mut self) {
43        for i in 0..self.layout.num_rows * self.layout.num_cols {
44            self.data[i as usize] = E::from_int(0);
45        }
46    }
47
48    pub fn get(&self, row: u32, col: u32) -> E {
49        self.data[(row * self.layout.num_cols + col) as usize]
50    }
51
52    /// Reads the element at `local_pos` and casts to `bool`. Used by the
53    /// `Mask` trait dispatcher when this tile is acting as a materialized
54    /// mask fragment.
55    pub fn should_mask(&self, local_pos: Coords2d) -> bool {
56        bool::cast_from(self.data[(local_pos.0 * self.layout.num_cols + local_pos.1) as usize])
57    }
58
59    pub fn accumulate(&mut self, row: u32, col: u32, val: E) {
60        self.data[(row * self.layout.num_cols + col) as usize] += val;
61    }
62
63    pub fn rowwise_scale(&mut self, scale: &RowWise<E>) {
64        for r in 0..self.layout.num_rows as usize {
65            let row_offset = r as u32 * self.layout.num_cols;
66            for c in 0..self.layout.num_cols {
67                let index = row_offset + c;
68                self.data[index as usize] *= scale.vals[r];
69            }
70        }
71    }
72
73    pub fn rowwise_max(&self) -> RowWise<E> {
74        let num_rows = comptime!(self.layout.num_rows) as usize;
75        let num_cols = comptime!(self.layout.num_cols) as usize;
76        let mut vals = Array::new(num_rows);
77
78        for r in 0..num_rows {
79            let row_offset = r * num_cols;
80            let mut val = E::min_value();
81
82            for c in 0..num_cols {
83                let index = row_offset + c;
84                val = max(val, self.data[index]);
85            }
86
87            vals[r] = val;
88        }
89
90        RowWise::<E> { num_rows, vals }
91    }
92
93    pub fn rowwise_sum(&self) -> RowWise<E> {
94        let num_rows = comptime!(self.layout.num_rows) as usize;
95        let num_cols = comptime!(self.layout.num_cols) as usize;
96        let mut vals = Array::new(num_rows);
97
98        for r in 0..num_rows {
99            let row_offset = r * num_cols;
100            let mut val = E::from_int(0);
101
102            for c in 0..num_cols {
103                let index = row_offset + c;
104                val += self.data[index];
105            }
106
107            vals[r] = val;
108        }
109
110        RowWise::<E> { num_rows, vals }
111    }
112
113    // TODO find a way to have this not necessary if E == E2
114    // TODO even if E != E2 it could be written as output to UnitTile::exp_diff rather than exp_diff being inplace
115    pub fn copy_from<E2: Numeric>(&mut self, other: &UnitTile<E2>) {
116        // Assume layouts are the same
117
118        for r in 0..self.layout.num_rows as usize {
119            let row_offset = r as u32 * self.layout.num_cols;
120            for c in 0..self.layout.num_cols {
121                let index = row_offset + c;
122                self.data[index as usize] = E::cast_from(other.data[index as usize]);
123            }
124        }
125    }
126
127    pub fn load_from_strided_tile<E2: Numeric, ES: Size>(&mut self, tile: &StridedTile<E2, ES>) {
128        if comptime!(self.layout.transposed_load) {
129            strided_tile_to_transposed_unit_tile(tile, self)
130        } else {
131            strided_tile_to_unit_tile(tile, self)
132        }
133    }
134
135    pub fn scale_and_mask<M: Mask>(&mut self, scale: E, mask: &M) {
136        for r in 0..self.layout.num_rows {
137            let row_offset = r * self.layout.num_cols;
138            for c in 0..self.layout.num_cols {
139                let index = row_offset + c;
140                self.data[index as usize] = self.data[index as usize] * scale
141                    + E::cast_from(mask.should_mask((r, c))) * E::min_value();
142            }
143        }
144    }
145}
146
147#[cube]
148impl<E: Float> UnitTile<E> {
149    pub fn row_max(&self, acc: &mut RowWise<E>, base: &RowWise<E>) {
150        acc.copy_from(base);
151        acc.max_inplace(&self.rowwise_max());
152    }
153
154    pub fn row_sum(&self, acc: &mut RowWise<E>) {
155        acc.fill(E::from_int(0));
156        acc.add_inplace(&self.rowwise_sum());
157    }
158
159    pub fn fill_zero(&mut self) {
160        self.zero();
161    }
162
163    /// Cast-copies this unit tile into `dest`. Used by per-variant softmax
164    /// helpers when writing the post-softmax score into a same-storage
165    /// destination.
166    pub fn write_to<Lhs: Float>(&self, dest: &mut UnitTile<Lhs>) {
167        let total = comptime!(self.layout.num_rows * self.layout.num_cols);
168        for i in 0..total {
169            dest.data[i as usize] = Lhs::cast_from(self.data[i as usize]);
170        }
171    }
172
173    pub fn exp_diff(&mut self, rowwise: &RowWise<E>) {
174        let num_rows = comptime!(self.layout.num_rows) as usize;
175        let num_cols = comptime!(self.layout.num_cols) as usize;
176        let threshold = E::new(LOGIT_MASKED);
177
178        for r in 0..num_rows {
179            let row_offset = r * num_cols;
180
181            let val = rowwise.vals[r];
182
183            for c in 0..num_cols {
184                let index = row_offset + c;
185
186                let safe_val = clamp_min(val, threshold);
187                let not_masked = E::cast_from(val >= threshold);
188                self.data[index] = not_masked * (self.data[index] - safe_val).exp();
189            }
190        }
191    }
192}
193
194#[cube]
195/// Allocates a `Tile::Unit`. The variant is valid in any scope — each unit
196/// just holds its own row-major copy of the tile.
197pub fn allocate_unit_tile<E: Numeric, Sc: TileScope>(
198    #[comptime] layout: UnitTileLayout,
199) -> Tile<E, Sc> {
200    Tile::from_kind(TileKind::new_Unit(UnitTile::<E>::new(layout)))
201}
202
203#[cube]
204impl<Acc: Float> UnitTile<Acc> {
205    /// Online softmax for the per-unit-full Unit variant. Each unit holds the
206    /// whole tile in registers; reductions are register-local. Destination
207    /// must be another `UnitTile`.
208    pub fn softmax<Lhs: Float, M: Mask>(
209        &mut self,
210        mask: &M,
211        softmaxed: &mut Tile<Lhs, Plane>,
212        state: &mut (RowWise<Acc>, RowWise<Acc>),
213        head_dim_factor: Acc,
214    ) -> RowWise<Acc> {
215        let num_rows = comptime!(state.0.num_rows);
216        let mut max_buf = RowWise::<Acc>::new_min_value(num_rows);
217        let mut sum_buf = RowWise::<Acc>::new_zero(num_rows);
218
219        self.scale_and_mask::<M>(head_dim_factor, mask);
220        self.row_max(&mut max_buf, &state.0);
221        self.exp_diff(&max_buf);
222        self.row_sum(&mut sum_buf);
223
224        let exp_m_diff = state.0.exp_diff(&max_buf);
225        let new_l = exp_m_diff.mul(&state.1).add(&sum_buf);
226
227        match &mut softmaxed.kind {
228            TileKind::Unit(d) => self.write_to::<Lhs>(d),
229            TileKind::Bounce(_) => panic!("UnitTile::softmax: Bounce destination not supported"),
230            TileKind::WhiteboxFragment(_) => {
231                panic!("UnitTile::softmax: WhiteboxFragment destination not supported")
232            }
233            TileKind::Register(_) => {
234                panic!("UnitTile::softmax: Register destination not supported")
235            }
236            _ => panic!("UnitTile::softmax: unsupported softmaxed variant"),
237        }
238
239        RowWise::copy_from(&mut state.0, &max_buf);
240        RowWise::copy_from(&mut state.1, &new_l);
241
242        exp_m_diff
243    }
244}
245
246#[cube]
247fn strided_tile_to_unit_tile<E: Numeric, N: Size, E2: Numeric>(
248    strided_tile: &StridedTile<E, N>,
249    unit_tile: &mut UnitTile<E2>,
250) {
251    let vector_size = N::value().comptime() as u32;
252    assert!(unit_tile.layout.num_cols.is_multiple_of(vector_size));
253
254    let col_iterations = comptime!(unit_tile.layout.num_cols / vector_size);
255
256    for row in 0..unit_tile.layout.num_rows {
257        for col in 0..col_iterations {
258            let line_read = strided_tile.get_vector(row, col);
259            #[unroll]
260            for i in 0..vector_size {
261                unit_tile.data
262                    [(row * unit_tile.layout.num_cols + col * vector_size + i) as usize] =
263                    E2::cast_from(line_read.extract(i as usize));
264            }
265        }
266    }
267}
268
269#[cube]
270fn strided_tile_to_transposed_unit_tile<E: Numeric, N: Size, E2: Numeric>(
271    strided_tile: &StridedTile<E, N>,
272    unit_tile: &mut UnitTile<E2>,
273) {
274    let vector_size = N::value().comptime() as u32;
275    assert!(unit_tile.layout.num_cols.is_multiple_of(vector_size));
276
277    let input_num_rows = unit_tile.layout.num_cols.comptime();
278    let input_num_cols = unit_tile.layout.num_rows.comptime();
279    let vector_iterations = input_num_cols / vector_size;
280
281    for input_row in 0..input_num_rows {
282        for input_col_vector in 0..vector_iterations {
283            let vector_read = strided_tile.get_vector(input_row, input_col_vector);
284
285            #[unroll]
286            for i in 0..vector_size {
287                unit_tile.data[((input_col_vector + i) * input_num_rows + input_row) as usize] =
288                    E2::cast_from(vector_read.extract(i as usize));
289            }
290        }
291    }
292}