Skip to main content

cubecl_core/frontend/
barrier.rs

1//! This module exposes barrier for asynchronous data transfer
2
3use alloc::vec;
4use pliron::{r#type::TypeHandle, value::Value};
5
6use crate as cubecl;
7use cubecl_ir::{
8    ExpandValue,
9    dialect::tma::*,
10    types::barrier::{BarrierLevel, BarrierType},
11};
12use cubecl_macros::intrinsic;
13use paste::paste;
14
15use crate::{
16    ir::{Scope, dialect::barrier::*},
17    prelude::*,
18    unexpanded,
19};
20
21use super::{CubePrimitive, CubeType, NativeExpand, SliceExpand, TensorMap};
22
23/// A mechanism for awaiting on asynchronous data transfers
24/// Behavior is defined by its ``BarrierLevel``.
25#[derive(Clone, Copy, PartialEq, Eq)]
26pub struct Barrier;
27pub type BarrierExpand = NativeExpand<Barrier>;
28
29#[derive(Clone, Copy, PartialEq)]
30pub struct BarrierToken;
31
32impl CubeType for Barrier {
33    type ExpandType = NativeExpand<Barrier>;
34}
35
36impl CubeDebug for Barrier {}
37
38impl CubePrimitive for Barrier {
39    type Scalar = u32; // Dummy, maybe we need another trait for non-standard primitives
40    type Size = Const<1>;
41    type WithScalar<S: Scalar> = S;
42    fn from_const_value(_value: cubecl_ir::ConstantValue) -> Self {
43        unreachable!("Can't create from const value")
44    }
45}
46
47impl ConstantTyped for Barrier {
48    fn __expand_as_type(scope: &Scope) -> TypeHandle {
49        BarrierType::get(scope.ctx(), BarrierLevel::Cube).into()
50    }
51}
52
53impl NativeAssign for Barrier {
54    fn elem_init_mut(_scope: &Scope, elem: ExpandValue) -> ExpandValue {
55        elem
56    }
57}
58
59impl CubeType for BarrierToken {
60    type ExpandType = NativeExpand<BarrierToken>;
61}
62
63impl ReadValue for NativeExpand<BarrierToken> {
64    fn read_value(&self, scope: &Scope) -> Value {
65        self.expand.read_value(scope)
66    }
67}
68
69impl NativeAssign for BarrierToken {
70    fn elem_init_mut(_scope: &Scope, elem: ExpandValue) -> ExpandValue {
71        elem
72    }
73}
74
75impl AsMutExpand for NativeExpand<BarrierToken> {
76    fn __expand_ref_mut_method(&mut self, _: &Scope) -> &mut Self {
77        self
78    }
79}
80
81macro_rules! tensor_map_load {
82    ($dim: literal, $($arg: expr),*) => {
83        paste! {
84            impl Barrier {
85                /// Copy a tile from a global memory `source` to a shared memory `destination`, with
86                /// the provided offsets.
87                #[allow(unused, clippy::too_many_arguments)]
88                pub fn [<tma_load_ $dim d>]<C1: CubePrimitive, C2: CubePrimitive<Scalar = C1::Scalar>>(
89                    &self,
90                    source: &TensorMap<C1, Tiled>,
91                    destination: &mut [C2],
92                    $($arg: i32),*
93                ) {
94                    unexpanded!()
95                }
96
97                #[allow(clippy::too_many_arguments)]
98                pub fn [<__expand_tma_load_ $dim d>]<C1: CubePrimitive, C2: CubePrimitive<Scalar = C1::Scalar>>(
99                    scope: &Scope,
100                    expand: &NativeExpand<Barrier>,
101                    source: &NativeExpand<TensorMap<C1, Tiled>>,
102                    destination: &mut SliceExpand<C2>,
103                    $($arg: NativeExpand<i32>),*
104                ) {
105                    expand.[<__expand_tma_load_ $dim d_method>](scope, source, destination, $($arg),*);
106                }
107            }
108
109            impl NativeExpand<Barrier> {
110                #[allow(clippy::too_many_arguments)]
111                pub fn [<__expand_tma_load_ $dim d_method>]<C1: CubePrimitive, C2: CubePrimitive<Scalar = C1::Scalar>>(
112                    &self,
113                    scope: &Scope,
114                    source: &NativeExpand<TensorMap<C1, Tiled>>,
115                    destination: &mut SliceExpand<C2>,
116                    $($arg: NativeExpand<i32>),*
117                ) {
118                    let barrier = self.value(scope);
119                    let source = source.value(scope);
120                    let destination = unsafe { *destination.__expand_as_ptr_method(scope) }.value(scope);
121                    let indices = vec![$($arg.read_value(scope)),*];
122
123                    let mem_copy = TmaLoadOp::new(scope.ctx_mut(), barrier, source, destination, indices);
124                    scope.register(&mem_copy);
125                }
126            }
127        }
128    };
129}
130
131macro_rules! tensor_map_load_im2col {
132    ($dim: literal, $($arg: expr),*; $($offset: expr),*) => {
133        paste! {
134            impl Barrier {
135                /// Copy a tile from a global memory `source` to a shared memory `destination`, with
136                /// the provided offsets.
137                #[allow(unused, clippy::too_many_arguments)]
138                pub fn [<tma_load_im2col_ $dim d>]<C1: CubePrimitive, C2: CubePrimitive<Scalar = C1::Scalar>>(
139                    &self,
140                    source: &TensorMap<C1, Im2col>,
141                    destination: &mut [C2],
142                    $($arg: i32,)*
143                    $($offset: u16),*
144                ) {
145                    unexpanded!()
146                }
147
148                #[allow(clippy::too_many_arguments)]
149                pub fn [<__expand_tma_load_im2col_ $dim d>]<C1: CubePrimitive, C2: CubePrimitive<Scalar = C1::Scalar>>(
150                    scope: &Scope,
151                    expand: &NativeExpand<Barrier>,
152                    source: &NativeExpand<TensorMap<C1, Im2col>>,
153                    destination: &mut SliceExpand<C2>,
154                    $($arg: NativeExpand<i32>,)*
155                    $($offset: NativeExpand<u16>),*
156                ) {
157                    expand.[<__expand_tma_load_im2col_ $dim d_method>](scope, source, destination, $($arg),*, $($offset),*);
158                }
159            }
160
161            impl NativeExpand<Barrier> {
162                #[allow(clippy::too_many_arguments)]
163                pub fn [<__expand_tma_load_im2col_ $dim d_method>]<C1: CubePrimitive, C2: CubePrimitive<Scalar = C1::Scalar>>(
164                    &self,
165                    scope: &Scope,
166                    source: &NativeExpand<TensorMap<C1, Im2col>>,
167                    destination: &mut SliceExpand<C2>,
168                    $($arg: NativeExpand<i32>,)*
169                    $($offset: NativeExpand<u16>),*
170                ) {
171                    let barrier = self.value(scope);
172                    let source = source.value(scope);
173                    let destination = unsafe { *destination.__expand_as_ptr_method(scope) }.value(scope);
174                    let indices = vec![$($arg.read_value(scope)),*];
175                    let offsets = vec![$($offset.read_value(scope)),*];
176
177                    let mem_copy = TmaLoadIm2colOp::new(scope.ctx_mut(), barrier, source, destination, indices, offsets);
178                    scope.register(&mem_copy);
179                }
180            }
181        }
182    };
183}
184
185tensor_map_load!(1, x);
186tensor_map_load!(2, y, x);
187tensor_map_load!(3, z, y, x);
188tensor_map_load!(4, w, z, y, x);
189tensor_map_load!(5, v, w, z, y, x);
190
191tensor_map_load_im2col!(3, n, w, c; w_offset);
192tensor_map_load_im2col!(4, n, h, w, c; h_offset, w_offset);
193tensor_map_load_im2col!(5, n, d, h, w, c; d_offset, h_offset, w_offset);
194
195#[cube]
196impl Barrier {
197    /// Create a local barrier object for the current unit. Automatically initialized with an
198    /// arrival count of `1`.
199    pub fn local() -> Self {
200        intrinsic!(|scope| {
201            let value =
202                scope.create_local_mut(BarrierType::get(scope.ctx(), BarrierLevel::Unit), None);
203            let arrival_count: ExpandValue = 1u32.into();
204            let arrival_count = arrival_count.read_value(scope);
205            let op = InitOp::new(scope.ctx_mut(), value, arrival_count);
206            scope.register(&op);
207            value.into()
208        })
209    }
210
211    /// Create a shared memory barrier that can be accessed by all units in the cube. Initialized
212    /// by the `is_elected` unit with an arrival count of `arrival_count`. This is the number of
213    /// times `arrive` or one of its variants needs to be called before the barrier advances.
214    ///
215    /// Exactly one unit must be elected; use `UNIT_POS == 0` for the usual case. A constant
216    /// election is logged as a warning when the kernel is built, because `true` makes every unit
217    /// initialize the same barrier and `false` leaves it uninitialized.
218    ///
219    /// If all units in the cube arrive on the barrier, use `CUBE_DIM` as the arrival count. For
220    /// other purposes, only a subset may need to arrive.
221    pub fn shared(arrival_count: u32, is_elected: bool) -> Shared<Barrier> {
222        intrinsic!(|scope| {
223            if is_elected.expand.as_const().is_some() {
224                scope.push_warning(
225                    "Barrier::shared: election must select exactly one unit; use `UNIT_POS == 0` instead of a constant",
226                );
227            }
228            let value =
229                scope.create_shared(BarrierType::get(scope.ctx(), BarrierLevel::Cube), None);
230            if_expand(scope, is_elected, |scope| {
231                let arrival_count = arrival_count.read_value(scope);
232                let op = InitOp::new(scope.ctx_mut(), value, arrival_count);
233                scope.register(&op);
234            });
235            sync_cube::expand(scope);
236            value.into()
237        })
238    }
239
240    /// Create a shared memory barrier that can be accessed by all units in the cube. Only declared,
241    /// but not initialized.
242    pub fn shared_uninit() -> Shared<Barrier> {
243        intrinsic!(|scope| {
244            let value =
245                scope.create_shared(BarrierType::get(scope.ctx(), BarrierLevel::Cube), None);
246            value.into()
247        })
248    }
249
250    /// Initializes a barrier with a given `arrival_count`. This is the number of
251    /// times `arrive` or one of its variants needs to be called before the barrier advances.
252    ///
253    /// If all units in the cube arrive on the barrier, use `CUBE_DIM` as the arrival count. For
254    /// other purposes, only a subset may need to arrive.
255    ///
256    /// # Note
257    ///
258    /// No synchronization or election is performed, this is raw initialization. For shared barriers
259    /// ensure only one unit performs the initialization, and synchronize the cube afterwards. There
260    /// may also be additional synchronization requirements for bulk copy operations, like
261    /// [`sync_async_proxy_shared()`].
262    pub fn init_manual(&self, arrival_count: u32) {
263        intrinsic!(|scope| {
264            let barrier = self.value(scope);
265            let arrival_count = arrival_count.read_value(scope);
266            let op = InitOp::new(scope.ctx_mut(), barrier, arrival_count);
267            scope.register(&op);
268        })
269    }
270}
271
272// MemcpyAsync
273
274#[cube]
275impl Barrier {
276    /// Copy the source slice to destination
277    ///
278    /// # Safety
279    ///
280    /// This will try to copy the whole source slice, so
281    /// make sure source length <= destination length
282    pub fn memcpy_async<C: CubePrimitive>(&self, source: &[C], destination: &mut [C]) {
283        intrinsic!(|scope| {
284            let barrier = self.value(scope);
285            let source_length = source.__extract_length(scope).value(scope);
286            let source = unsafe { *source.__expand_as_ptr_method(scope) }.value(scope);
287            let destination = unsafe { *destination.__expand_as_ptr_method(scope) }.value(scope);
288
289            let mem_copy = MemCopyAsyncOp::new(
290                scope.ctx_mut(),
291                barrier,
292                source,
293                destination,
294                source_length,
295                false,
296            );
297
298            scope.register(&mem_copy);
299        })
300    }
301
302    /// Copy the source slice to destination
303    ///
304    /// # Safety
305    ///
306    /// This will try to copy the whole source slice, so
307    /// make sure source length <= destination length
308    pub fn memcpy_async_cooperative<C: CubePrimitive>(&self, source: &[C], destination: &mut [C]) {
309        intrinsic!(|scope| {
310            let barrier = self.value(scope);
311            let source_length = source.__extract_length(scope).value(scope);
312            let source = unsafe { *source.__expand_as_ptr_method(scope) }.value(scope);
313            let destination = unsafe { *destination.__expand_as_ptr_method(scope) }.value(scope);
314
315            let mem_copy = MemCopyAsyncOp::new(
316                scope.ctx_mut(),
317                barrier,
318                source,
319                destination,
320                source_length,
321                true,
322            );
323
324            scope.register(&mem_copy);
325        })
326    }
327
328    /// Copy the source slice to destination. Uses transaction count like TMA, so use with
329    /// `expect_tx` or `arrive_and_expect_tx`.
330    ///
331    /// # Safety
332    ///
333    /// This will try to copy the whole source slice, so
334    /// make sure source length <= destination length
335    pub fn memcpy_async_tx<C: CubePrimitive>(&self, source: &[C], destination: &mut [C]) {
336        intrinsic!(|scope| {
337            let barrier = self.value(scope);
338            let source_length = source.__extract_length(scope).value(scope);
339            let source = unsafe { *source.__expand_as_ptr_method(scope) }.value(scope);
340            let destination = unsafe { *destination.__expand_as_ptr_method(scope) }.value(scope);
341
342            let mem_copy =
343                MemCopyAsyncTxOp::new(scope.ctx_mut(), barrier, source, destination, source_length);
344
345            scope.register(&mem_copy);
346        })
347    }
348}
349
350// Arrival and Wait
351
352#[cube]
353impl Barrier {
354    /// Arrive at the barrier, decrementing arrival count
355    pub fn arrive(&self) -> BarrierToken {
356        intrinsic!(|scope| {
357            let barrier = self.value(scope);
358            let arrive = ArriveOp::new(scope.ctx_mut(), barrier);
359            scope.register_with_result(&arrive).into()
360        })
361    }
362
363    /// Arrive at the barrier, decrementing arrival count. Additionally increments expected count.
364    pub fn arrive_and_expect_tx(&self, arrival_count: u32, transaction_count: u32) -> BarrierToken {
365        intrinsic!(|scope| {
366            let barrier = self.value(scope);
367            let arrival_count = arrival_count.read_value(scope);
368            let transaction_count = transaction_count.read_value(scope);
369            let op = ArriveAndExpectTxOp::new(
370                scope.ctx_mut(),
371                barrier,
372                arrival_count,
373                transaction_count,
374            );
375            scope.register_with_result(&op).into()
376        })
377    }
378
379    /// Increments the expected count of the barrier.
380    pub fn expect_tx(&self, transaction_count_update: u32) {
381        intrinsic!(|scope| {
382            let barrier = self.value(scope);
383            let transaction_count_update = transaction_count_update.value(scope);
384            scope.register(&ExpectTxOp::new(
385                scope.ctx_mut(),
386                barrier,
387                transaction_count_update,
388            ));
389        })
390    }
391
392    /// Wait until all data is loaded
393    pub fn arrive_and_wait(&self) {
394        intrinsic!(|scope| {
395            let barrier = self.value(scope);
396            scope.register(&ArriveAndWaitOp::new(scope.ctx_mut(), barrier));
397        })
398    }
399
400    /// Wait at the barrier until all arrivals are done
401    pub fn wait(&self, token: BarrierToken) {
402        intrinsic!(|scope| {
403            let barrier = self.value(scope);
404            let token = token.value(scope);
405            scope.register(&WaitOp::new(scope.ctx_mut(), barrier, token));
406        })
407    }
408
409    /// Wait at the barrier until the `phase` is completed. Doesn't require a token, but needs phase
410    /// to be managed manually.
411    pub fn wait_parity(&self, phase: u32) {
412        intrinsic!(|scope| {
413            let barrier = self.value(scope);
414            let phase = phase.read_value(scope);
415            scope.register(&WaitParityOp::new(scope.ctx_mut(), barrier, phase));
416        })
417    }
418}
419
420// Copy async
421
422/// Copy the source slice in global memory to destination in shared memory with a low level async
423/// copy. This only copies up to 128 bits/16 bytes, and does not synchronize. Use
424/// `barrier.copy_async_arrive` to make the reads visible.
425/// `copy_size` is in terms of elements to simplify copying between different vector sizes.
426///
427/// # Safety
428///
429/// This will try to copy the entire `copy_size`, so make sure the full width is in bounds.
430/// Starting address must be aligned to the full copy size.
431pub fn copy_async<C: CubePrimitive>(_source: &[C], _destination: &mut [C], _copy_size: u32) {
432    unexpanded!()
433}
434
435pub mod copy_async {
436    use super::*;
437
438    pub fn expand<C: CubePrimitive>(
439        scope: &Scope,
440        source: &SliceExpand<C>,
441        destination: &mut SliceExpand<C>,
442        copy_length: u32,
443    ) {
444        let source = unsafe { *source.__expand_as_ptr_method(scope) }.value(scope);
445        let destination = unsafe { *destination.__expand_as_ptr_method(scope) }.value(scope);
446        let scalar_size = C::Scalar::__expand_size(scope);
447        let copy_length_bytes = copy_length as usize * scalar_size;
448        let source_length = ExpandValue::from(copy_length_bytes).read_value(scope);
449
450        let mem_copy = CopyAsyncOp::new(
451            scope.ctx_mut(),
452            source,
453            destination,
454            source_length,
455            copy_length_bytes,
456            false,
457        );
458
459        scope.register(&mem_copy);
460    }
461}
462
463/// Copy the source slice in global memory to destination in shared memory with a low level async
464/// copy. This only copies up to 128 bits/16 bytes, and does not synchronize. Use
465/// `barrier.copy_async_arrive` to make the reads visible.
466/// `copy_size` is in terms of elements to simplify copying between different vector sizes.
467///
468/// Will only copy the length of the source slice, and zero fill the rest. Source length must be
469/// <= copy size.
470///
471/// # Safety
472/// Starting address must be aligned to the full copy size.
473/// **This will silently fail if the address is only aligned to the source length and not the copy size!**
474pub fn copy_async_checked<C: CubePrimitive>(
475    _source: &[C],
476    _destination: &mut [C],
477    _copy_size: u32,
478) {
479    unexpanded!();
480}
481
482pub mod copy_async_checked {
483    use super::*;
484
485    pub fn expand<C: CubePrimitive>(
486        scope: &Scope,
487        source: &SliceExpand<C>,
488        destination: &mut SliceExpand<C>,
489        copy_length: u32,
490    ) {
491        let source_length = source.__extract_length(scope);
492
493        let source = unsafe { *source.__expand_as_ptr_unchecked_method(scope) }.value(scope);
494        let destination =
495            unsafe { *destination.__expand_as_ptr_unchecked_method(scope) }.value(scope);
496        let scalar_size = C::Scalar::__expand_size(scope);
497        let vector_size = C::__expand_size(scope).__expand_runtime_method(scope);
498        let source_length_bytes = source_length.__expand_mul_method(scope, vector_size);
499
500        let mem_copy = CopyAsyncOp::new(
501            scope.ctx_mut(),
502            source,
503            destination,
504            source_length_bytes.read_value(scope),
505            copy_length as usize * scalar_size,
506            true,
507        );
508
509        scope.register(&mem_copy);
510    }
511}
512
513#[cube]
514impl Barrier {
515    /// Makes all previous `copy_async` operations visible on the barrier.
516    /// Should be called once after all copies have been dispatched, before reading from the shared
517    /// memory.
518    ///
519    /// Does *not* count as an arrive in terms of the barrier arrival count. So `arrive` or
520    /// `arrive_and_wait` should still be called afterwards.
521    pub fn commit_copy_async(&self) {
522        intrinsic!(|scope| {
523            let barrier = self.value(scope);
524            scope.register(&CommitCopyAsyncOp::new(scope.ctx_mut(), barrier));
525        })
526    }
527}
528
529impl From<SharedExpand<Barrier>> for BarrierExpand {
530    fn from(value: SharedExpand<Barrier>) -> Self {
531        value.expand.into()
532    }
533}
534
535#[cfg(test)]
536mod tests {
537    use super::*;
538    use cubecl_ir::{
539        AddressType,
540        settings::{Dim3, ExecutionMode, KernelSettings},
541        types::scalar::BoolType,
542    };
543
544    #[test]
545    fn shared_barrier_warns_on_a_constant_election() {
546        let scope = Scope::root(KernelSettings::new(
547            Dim3::new_1d(64),
548            ExecutionMode::Unchecked,
549            AddressType::U32,
550        ));
551        let _ = Barrier::__expand_shared(&scope, 64u32.into(), true.into());
552        let warnings = scope.pop_warnings();
553        assert_eq!(warnings.len(), 1, "{warnings:?}");
554        assert!(warnings[0].contains("UNIT_POS == 0"), "{warnings:?}");
555    }
556
557    #[test]
558    fn shared_barrier_is_quiet_for_a_runtime_election() {
559        let scope = Scope::root(KernelSettings::new(
560            Dim3::new_1d(64),
561            ExecutionMode::Unchecked,
562            AddressType::U32,
563        ));
564        let elected: NativeExpand<bool> =
565            ExpandValue::from(scope.create_local_mut(BoolType::get(scope.ctx()), None)).into();
566        let _ = Barrier::__expand_shared(&scope, 64u32.into(), elected);
567        assert!(scope.pop_warnings().is_empty());
568    }
569}