Skip to main content

ruda_kernel/dsl/frontend/
barrier.rs

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