Skip to main content

cubek_std/tile/variants/
whitebox_fragment.rs

1use cubecl;
2use cubecl::{prelude::*, std::tensor::layout::Coords2d};
3
4use crate::tile::LOGIT_MASKED;
5use crate::tile::{
6    Plane, RowWise, StridedTile, Tile, TileKind, TileKindExpand,
7    mask::{Mask, MaskExpand},
8    scope::{TileScope, assert_plane_scope},
9};
10
11#[derive(CubeType)]
12/// Assumes:
13/// - unit_size * plane_dim = total_size (not dim wise but in total count)
14pub struct WhiteboxFragment<E: Numeric> {
15    pub array: Array<E>,
16    #[cube(comptime)]
17    pub layout: WhiteboxFragmentLayout,
18}
19
20#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)]
21pub enum InnerLayout {
22    /// Each unit has all its elements contiguous inside the same row
23    ///
24    ///  0,  0,  1,  1,  2,  2,  3,  3,
25    ///  4,  4,  5,  5,  6,  6,  7,  7,
26    ///  8,  8,  9,  9, 10, 10, 11, 11,
27    /// 12, 12, 13, 13, 14, 14, 15, 15,
28    /// 16, 16, 17, 17, 18, 18, 19, 19,
29    /// 20, 20, 21, 21, 22, 22, 23, 23,
30    /// 24, 24, 25, 25, 26, 26, 27, 27,
31    /// 28, 28, 29, 29, 30, 30, 31, 31,
32    Contiguous,
33    /// Each unit spreads its elements along two rows
34    ///
35    ///  0,  1,  2,  3,  4,  5,  6,  7,
36    ///  8,  9, 10, 11, 12, 13, 14, 15,
37    /// 16, 17, 18, 19, 20, 21, 22, 23,
38    /// 24, 25, 26, 27, 28, 29, 30, 31,
39    ///  0,  1,  2,  3,  4,  5,  6,  7,
40    ///  8,  9, 10, 11, 12, 13, 14, 15,
41    /// 16, 17, 18, 19, 20, 21, 22, 23,
42    /// 24, 25, 26, 27, 28, 29, 30, 31,
43    SplitRows,
44}
45
46#[cube]
47impl<E: Numeric> WhiteboxFragment<E> {
48    pub fn new(#[comptime] layout: WhiteboxFragmentLayout) -> WhiteboxFragment<E> {
49        let array = Array::new(comptime!(layout.unit_size.0 * layout.unit_size.1) as usize);
50
51        WhiteboxFragment::<E> { array, layout }
52    }
53
54    pub fn zero(&mut self) {
55        for i in 0..self.layout.unit_size.0 * self.layout.unit_size.1 {
56            self.array[i as usize] = E::from_int(0);
57        }
58    }
59
60    pub fn load_from_slice(&mut self, smem_slice: &[E]) {
61        for r in 0..self.layout.unit_size.0 {
62            for c in 0..self.layout.unit_size.1 {
63                let (row, col) = whitebox_fragment_absolute_pos(self.layout, (r, c));
64                let index = row * self.layout.total_size.1 + col;
65
66                self.array[(r * self.layout.unit_size.1 + c) as usize] = smem_slice[index as usize];
67            }
68        }
69    }
70
71    pub fn load_from_strided_tile<E2: Numeric, N: Size>(
72        &mut self,
73        strided_tile: &StridedTile<E2, N>,
74    ) {
75        // Assumes vector size == 1
76        for r in 0..self.layout.unit_size.0 {
77            for c in 0..self.layout.unit_size.1 {
78                let (row, col) = whitebox_fragment_absolute_pos(self.layout, (r, c));
79                self.array[(r * self.layout.unit_size.1 + c) as usize] =
80                    E::cast_from(strided_tile.get_vector(row, col))
81            }
82        }
83    }
84
85    pub fn store_to<F: Float>(&self, smem_slice: &mut [F]) {
86        for r in 0..self.layout.unit_size.0 {
87            for c in 0..self.layout.unit_size.1 {
88                let (row, col) = whitebox_fragment_absolute_pos(self.layout, (r, c));
89                let index = row * self.layout.total_size.1 + col;
90
91                smem_slice[index as usize] =
92                    F::cast_from(self.array[(r * self.layout.unit_size.1 + c) as usize]);
93            }
94        }
95    }
96
97    /// Reads the element at `local_pos` and casts to `bool`. Used by the
98    /// `Mask` trait dispatcher when this fragment is acting as a
99    /// materialized mask fragment.
100    pub fn should_mask(&self, local_pos: Coords2d) -> bool {
101        bool::cast_from(self.array[(local_pos.0 * self.layout.unit_size.1 + local_pos.1) as usize])
102    }
103
104    pub fn rowwise_scale(&mut self, scale: &RowWise<E>) {
105        for r in 0..self.layout.unit_size.0 as usize {
106            let row_offset = r as u32 * self.layout.unit_size.1;
107            for c in 0..self.layout.unit_size.1 {
108                let index = row_offset + c;
109                self.array[index as usize] *= scale.vals[r];
110            }
111        }
112    }
113
114    pub fn rowwise_max(&self) -> RowWise<E> {
115        let num_rows = comptime!(self.layout.unit_size.0) as usize;
116        let num_cols = comptime!(self.layout.unit_size.1) as usize;
117        let mut vals = Array::new(num_rows);
118
119        for r in 0..num_rows {
120            let row_offset = r * num_cols;
121            let mut val = E::min_value();
122
123            for c in 0..num_cols {
124                let index = row_offset + c;
125                val = max(val, self.array[index]);
126            }
127
128            vals[r] = val;
129        }
130
131        RowWise::<E> { num_rows, vals }
132    }
133
134    pub fn rowwise_sum(&self) -> RowWise<E> {
135        let num_rows = comptime!(self.layout.unit_size.0) as usize;
136        let num_cols = comptime!(self.layout.unit_size.1) as usize;
137        let mut vals = Array::new(num_rows);
138
139        for r in 0..num_rows {
140            let row_offset = r * num_cols;
141            let mut val = E::from_int(0);
142
143            for c in 0..num_cols {
144                let index = row_offset + c;
145                val += self.array[index];
146            }
147
148            vals[r] = val;
149        }
150
151        RowWise::<E> { num_rows, vals }
152    }
153
154    pub fn num_units_per_row(&self) -> comptime_type!(u32) {
155        comptime!(self.layout.total_size.1 / self.layout.unit_size.1)
156    }
157
158    pub fn scale_and_mask<M: Mask>(&mut self, scale: E, mask: &M) {
159        for r in 0..self.layout.unit_size.0 {
160            let row_offset = r * self.layout.unit_size.1;
161            for c in 0..self.layout.unit_size.1 {
162                let index = row_offset + c;
163                self.array[index as usize] = self.array[index as usize] * scale
164                    + E::cast_from(mask.should_mask((r, c))) * E::min_value();
165            }
166        }
167    }
168}
169
170#[cube]
171impl<E: Float> WhiteboxFragment<E> {
172    pub fn exp_diff(&mut self, rowwise: &RowWise<E>) {
173        let num_rows = comptime!(self.layout.unit_size.0) as usize;
174        let num_cols = comptime!(self.layout.unit_size.1) as usize;
175        let threshold = E::new(LOGIT_MASKED);
176
177        for r in 0..num_rows {
178            let row_offset = r * num_cols;
179
180            let val = rowwise.vals[r];
181            let safe_val = clamp_min(val, threshold);
182            let not_masked = E::cast_from(val >= threshold);
183
184            for c in 0..num_cols {
185                let index = row_offset + c;
186
187                self.array[index] = not_masked * (self.array[index] - safe_val).exp();
188            }
189        }
190    }
191}
192
193#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
194pub struct WhiteboxFragmentLayout {
195    pub total_size: Coords2d,
196    pub unit_size: Coords2d,
197    pub num_units_per_row: u32,
198    pub plane_dim: u32,
199}
200
201impl WhiteboxFragmentLayout {
202    pub const fn new(
203        total_size: Coords2d,
204        plane_dim: u32,
205        inner_layout: InnerLayout,
206    ) -> WhiteboxFragmentLayout {
207        let total_elements = total_size.0 * total_size.1;
208        let elements_per_unit = total_elements.div_ceil(plane_dim);
209
210        let (num_rows_per_unit, num_cols_per_unit) = match inner_layout {
211            InnerLayout::Contiguous => (1u32, elements_per_unit),
212            InnerLayout::SplitRows => (2u32, elements_per_unit / 2u32),
213        };
214        let unit_size = (num_rows_per_unit, num_cols_per_unit);
215        let num_units_per_row = total_size.1 / unit_size.1;
216
217        WhiteboxFragmentLayout {
218            total_size,
219            unit_size,
220            num_units_per_row,
221            plane_dim,
222        }
223    }
224
225    pub const fn num_units_per_row(&self) -> u32 {
226        self.total_size.1 / self.unit_size.1
227    }
228}
229
230#[cube]
231/// Allocates a `Tile::WhiteboxFragment` for the given scope. Panics at expansion
232/// time unless `Sc = Plane`.
233pub fn allocate_whitebox_fragment<E: Numeric, Sc: TileScope>(
234    #[comptime] layout: WhiteboxFragmentLayout,
235) -> Tile<E, Sc> {
236    comptime!(assert_plane_scope(Sc::KIND));
237    Tile::from_kind(TileKind::new_WhiteboxFragment(WhiteboxFragment::<E>::new(
238        layout,
239    )))
240}
241
242/// Maps a per-unit `(row, col)` to its absolute position within the tile
243/// described by `layout`.
244#[cube]
245pub fn whitebox_fragment_absolute_pos(
246    #[comptime] layout: WhiteboxFragmentLayout,
247    local_pos: Coords2d,
248) -> Coords2d {
249    let abs_row_index = {
250        let row_0 = UNIT_POS_X / layout.num_units_per_row;
251        let row_jump = comptime!(layout.plane_dim / layout.num_units_per_row);
252        local_pos.0 * row_jump + row_0
253    };
254    let abs_col_index = layout.unit_size.1 * (UNIT_POS_X % layout.num_units_per_row) + local_pos.1;
255    (abs_row_index, abs_col_index)
256}
257
258/// Zeroes a slice giving responsibility to units following `layout`.
259#[cube]
260pub fn whitebox_fragment_zero_slice<E: Numeric>(
261    #[comptime] layout: WhiteboxFragmentLayout,
262    slice: &mut [E],
263) {
264    for r in 0..layout.unit_size.0 {
265        for c in 0..layout.unit_size.1 {
266            let (row, col) = whitebox_fragment_absolute_pos(layout, (r, c));
267            let index = row * layout.total_size.1 + col;
268
269            slice[index as usize] = E::from_int(0);
270        }
271    }
272}
273
274// ===========================================================================
275// Cross-plane row reduction
276//
277// Reduces row-wise quantities across plane units that share a row, masking
278// out off-row peers. Restricted to plane scope (uses `plane_shuffle` and
279// `UNIT_POS_X`); callers enforce that.
280// ===========================================================================
281
282#[cube]
283impl<E: Float> WhiteboxFragment<E> {
284    pub fn row_max(&self, acc: &mut RowWise<E>, base: &RowWise<E>) {
285        acc.copy_from(base);
286        reduce::<E, FragmentRowMax>(acc, self);
287    }
288
289    pub fn row_sum(&self, acc: &mut RowWise<E>) {
290        acc.fill(E::from_int(0));
291        reduce::<E, FragmentRowSum>(acc, self);
292    }
293}
294
295#[cube]
296fn reduce<E: Float, RO: ReduceOp<E>>(vals: &mut RowWise<E>, data: &WhiteboxFragment<E>) {
297    let num_units_per_row = data.num_units_per_row().comptime();
298    let num_shares_within_plane = num_units_per_row.next_power_of_two().ilog2();
299
300    let unit_pos = UNIT_POS_X;
301    let unit_pos_in_row = unit_pos % num_units_per_row;
302
303    RO::reduce_local(data, vals);
304
305    for i in 0..num_shares_within_plane {
306        let offset = num_units_per_row >> (i + 1);
307        let source_unit = unit_pos + offset;
308
309        let value_from_source = rowwise_plane_broadcast(&*vals, source_unit);
310
311        // Mask if outside the row
312        let mask = unit_pos_in_row + offset >= num_units_per_row;
313        RO::reduce_from_peer(vals, &value_from_source, mask);
314    }
315
316    // Broadcast back to subgroup
317    let result = rowwise_plane_broadcast(&*vals, unit_pos - unit_pos_in_row);
318    vals.copy_from(&result);
319}
320
321#[cube]
322fn rowwise_plane_broadcast<E: Float>(rowwise: &RowWise<E>, source_unit: u32) -> RowWise<E> {
323    let mut result = Array::new(rowwise.num_rows);
324
325    for r in 0..rowwise.num_rows {
326        result[r] = plane_shuffle(rowwise.vals[r], source_unit);
327    }
328
329    RowWise::<E> {
330        num_rows: rowwise.num_rows,
331        vals: result,
332    }
333}
334
335#[cube]
336trait ReduceOp<E: Float> {
337    fn reduce_local(data: &WhiteboxFragment<E>, acc: &mut RowWise<E>);
338    fn reduce_from_peer(acc: &mut RowWise<E>, elem: &RowWise<E>, mask: bool);
339}
340
341#[derive(CubeType)]
342struct FragmentRowMax {}
343
344#[derive(CubeType)]
345struct FragmentRowSum {}
346
347#[cube]
348impl<E: Float> ReduceOp<E> for FragmentRowMax {
349    fn reduce_local(data: &WhiteboxFragment<E>, acc: &mut RowWise<E>) {
350        acc.max_inplace(&data.rowwise_max())
351    }
352
353    fn reduce_from_peer(acc: &mut RowWise<E>, elem: &RowWise<E>, mask: bool) {
354        let mut masked = RowWise::new_filled(elem.num_rows, E::cast_from(mask) * E::min_value());
355        masked.add_inplace(elem);
356
357        acc.max_inplace(&masked)
358    }
359}
360
361#[cube]
362impl<E: Float> ReduceOp<E> for FragmentRowSum {
363    fn reduce_local(data: &WhiteboxFragment<E>, acc: &mut RowWise<E>) {
364        acc.add_inplace(&data.rowwise_sum())
365    }
366
367    fn reduce_from_peer(acc: &mut RowWise<E>, elem: &RowWise<E>, mask: bool) {
368        let mut masked = RowWise::new_filled(elem.num_rows, E::cast_from(!mask));
369        masked.mul_inplace(elem);
370
371        acc.add_inplace(&masked)
372    }
373}
374
375// ===========================================================================
376// Online softmax over a free-standing WhiteboxFragment score.
377// ===========================================================================
378
379#[cube]
380impl<Acc: Float> WhiteboxFragment<Acc> {
381    /// Online softmax for a free-standing WhiteboxFragment score (the
382    /// register-only variant of attention's softmax). Writes the post-softmax
383    /// values into `softmaxed` (which may be `Bounce` — routed through its
384    /// smem into its cmma fragment — or another `WhiteboxFragment`).
385    pub fn softmax<Lhs: Float, M: Mask>(
386        &mut self,
387        mask: &M,
388        softmaxed: &mut Tile<Lhs, Plane>,
389        state: &mut (RowWise<Acc>, RowWise<Acc>),
390        head_dim_factor: Acc,
391    ) -> RowWise<Acc> {
392        let num_rows = comptime!(state.0.num_rows);
393        let mut max_buf = RowWise::<Acc>::new_min_value(num_rows);
394        let mut sum_buf = RowWise::<Acc>::new_zero(num_rows);
395
396        self.scale_and_mask::<M>(head_dim_factor, mask);
397        self.row_max(&mut max_buf, &state.0);
398        self.exp_diff(&max_buf);
399        self.row_sum(&mut sum_buf);
400
401        let exp_m_diff = state.0.exp_diff(&max_buf);
402        let new_l = exp_m_diff.mul(&state.1).add(&sum_buf);
403
404        write_fragment_into::<Acc, Lhs>(&*self, softmaxed);
405
406        RowWise::copy_from(&mut state.0, &max_buf);
407        RowWise::copy_from(&mut state.1, &new_l);
408
409        exp_m_diff
410    }
411}
412
413/// Write post-softmax `WhiteboxFragment` values into `softmaxed`.
414#[cube]
415fn write_fragment_into<Acc: Float, Lhs: Float>(
416    src: &WhiteboxFragment<Acc>,
417    softmaxed: &mut Tile<Lhs, Plane>,
418) {
419    match &mut softmaxed.kind {
420        TileKind::Bounce(d) => {
421            let stride = comptime!(d.cmma.tile_size.n());
422            src.store_to(&mut d.smem);
423            sync_cube();
424            cubecl::cmma::load(&mut d.cmma.matrix, &d.smem, stride);
425        }
426        TileKind::WhiteboxFragment(d) => {
427            let total = comptime!(src.layout.unit_size.0 * src.layout.unit_size.1);
428            for i in 0..total {
429                d.array[i as usize] = Lhs::cast_from(src.array[i as usize]);
430            }
431        }
432        _ => panic!("write_fragment_into: unsupported softmaxed variant"),
433    }
434}