Skip to main content

cubecl_core/compute/
launcher.rs

1use alloc::{boxed::Box, vec::Vec};
2
3use crate::prelude::{BufferArg, PeerArg, TensorArg, TensorMapArg, TensorMapKind};
4use crate::{InfoBuilder, ScalarArgType};
5#[cfg(feature = "std")]
6use core::cell::RefCell;
7use cubecl_ir::{AddressType, ElemType, Scope, settings::KernelSettings};
8use cubecl_runtime::kernel::BufferIOAttr;
9use cubecl_runtime::server::{
10    BufferBinding, CubeCount, KernelResource, PeerBinding, TensorMapBinding,
11};
12use cubecl_runtime::{client::Client, kernel::CubeKernel, server::KernelArguments};
13
14#[cfg(feature = "std")]
15std::thread_local! {
16    static INFO: RefCell<InfoBuilder> = RefCell::new(InfoBuilder::default());
17    // Only used for resolving types
18    static SCOPE: RefCell<Scope> = RefCell::new(Scope::dummy());
19}
20
21/// Prepare a kernel for [launch](KernelLauncher::launch).
22pub struct KernelLauncher {
23    resources: Vec<KernelResource>,
24    /// What the caller declared each resource is for, indexed like
25    /// `resources` — see [`declare_io`](Self::declare_io).
26    declared_io: Vec<BufferIOAttr>,
27    /// The declaration the next registered resources fall under.
28    declaring: BufferIOAttr,
29    address_type: AddressType,
30    pub settings: KernelSettings,
31    #[cfg(not(feature = "std"))]
32    info: InfoBuilder,
33    #[cfg(not(feature = "std"))]
34    pub scope: Scope,
35}
36
37impl KernelLauncher {
38    #[cfg(feature = "std")]
39    pub fn with_scope<T>(&mut self, fun: impl FnMut(&Scope) -> T) -> T {
40        SCOPE.with_borrow(fun)
41    }
42
43    #[cfg(not(feature = "std"))]
44    pub fn with_scope<T>(&mut self, mut fun: impl FnMut(&Scope) -> T) -> T {
45        fun(&self.scope)
46    }
47
48    #[cfg(feature = "std")]
49    fn with_info<T>(&mut self, fun: impl FnMut(&mut InfoBuilder) -> T) -> T {
50        INFO.with_borrow_mut(fun)
51    }
52
53    #[cfg(not(feature = "std"))]
54    fn with_info<T>(&mut self, mut fun: impl FnMut(&mut InfoBuilder) -> T) -> T {
55        fun(&mut self.info)
56    }
57
58    /// Register a scalar to be launched.
59    pub fn register_scalar<C: ScalarArgType>(&mut self, scalar: C) {
60        self.with_info(|info| info.scalars.push(scalar));
61    }
62
63    /// Register a scalar to be launched from raw data.
64    pub fn register_scalar_raw(&mut self, bytes: &[u8], dtype: ElemType) {
65        self.with_info(|info| info.scalars.push_raw(bytes, dtype));
66    }
67
68    /// Launch the kernel.
69    #[track_caller]
70    pub fn launch<K: CubeKernel>(self, cube_count: CubeCount, kernel: K, client: &Client) {
71        let bindings = self.into_bindings();
72        let kernel = Box::new(kernel);
73
74        client.launch(kernel, cube_count, bindings)
75    }
76
77    /// Drop a launcher that will never launch, releasing what it registered.
78    ///
79    /// With `std` a launcher's scalars and metadata accumulate in a
80    /// thread-local [`InfoBuilder`] that only building the bindings drains, so
81    /// a launcher built to register arguments and then dropped — what the
82    /// `create_dummy_kernel` launch variant does — would leave that state
83    /// behind for the next real launch on the same thread to pick up as extra
84    /// arguments. Discarding drains it instead.
85    pub fn discard(self) {
86        let _ = self.into_bindings();
87    }
88
89    /// We need to create the bindings in the same order they are defined in the compilation step.
90    ///
91    /// The function [`crate::KernelIntegrator::integrate`] stars by registering the input tensors followed
92    /// by the output tensors. Then the tensor metadata, and the scalars at the end. The scalars
93    /// are registered in the same order they are added. This is why we store the scalar data type
94    /// in the `scalar_order` vector, so that we can register them in the same order.
95    ///
96    /// Also returns an ordered list of constant bindings. The ordering between constants and tensors
97    /// is up to the runtime.
98    fn into_bindings(mut self) -> KernelArguments {
99        let mut bindings = KernelArguments::new();
100        let address_type = self.address_type;
101        let info = self.with_info(|info| info.finish(address_type));
102
103        bindings.resources = self.resources;
104        bindings.declared_io = self.declared_io;
105        bindings.info = info;
106
107        bindings
108    }
109}
110
111// Tensors/arrays
112impl KernelLauncher {
113    /// Declare what the kernel does with the buffers registered from here on,
114    /// until the next declaration.
115    ///
116    /// The generated launch functions call this before each argument with
117    /// what the signature proves — `&Tensor` cannot be written, `&mut Tensor`
118    /// may be read — so a launch that fails before running, a kernel that
119    /// does not compile above all, taints only the buffers the kernel could
120    /// have written. The compiled kernel's own answer still wins once it
121    /// exists; this one is the answer that survives compilation failing. A
122    /// launcher that never declares leaves every resource
123    /// [`ReadWrite`](BufferIOAttr::ReadWrite), the loud fallback.
124    pub fn declare_io(&mut self, io: BufferIOAttr) {
125        self.declaring = io;
126    }
127
128    /// An aliasing argument writes the buffer it aliases in place, however
129    /// that buffer's own argument was declared — the aliased buffer usually
130    /// arrives through a `&Tensor`, and it is the one buffer an in-place
131    /// kernel exists to produce. The alias registers no resource of its own,
132    /// so its declaration lands on the buffer at `input_pos` instead: a
133    /// declaration built from each signature position alone would call that
134    /// buffer read-only and leave the in-place output unnamed by a failure,
135    /// which is silent garbage on a read.
136    fn alias_io(&mut self, input_pos: usize) {
137        if self.declaring.is_writable()
138            && let Some(io) = self.declared_io.get_mut(input_pos)
139        {
140            *io = BufferIOAttr::ReadWrite;
141        }
142    }
143
144    /// Record a resource.
145    fn push_resource(&mut self, resource: KernelResource) {
146        let io = match &resource {
147            // A tensor map's global side is written through TMA operations no
148            // signature shows — a map registered from a `&TensorMap` can
149            // still be a store's destination — so the declaration is clamped
150            // to the same answer the visibility analysis gives it.
151            KernelResource::TensorMap(_) => BufferIOAttr::ReadWrite,
152            KernelResource::Buffer(_) | KernelResource::Peer(_) => self.declaring,
153        };
154        self.declared_io.push(io);
155        self.resources.push(resource);
156    }
157
158    /// Push a new input tensor to the state.
159    pub fn register_tensor(&mut self, tensor: TensorArg, elem_size: usize) {
160        if let Some(tensor) = self.process_tensor(tensor, elem_size) {
161            self.push_resource(KernelResource::Buffer(tensor));
162        }
163    }
164
165    fn process_tensor(&mut self, tensor: TensorArg, elem_size: usize) -> Option<BufferBinding> {
166        let tensor = match tensor {
167            TensorArg::Handle { handle, .. } => handle,
168            TensorArg::Alias { input_pos, .. } => {
169                self.alias_io(input_pos);
170                return None;
171            }
172        };
173
174        let buffer_len = tensor.handle.size_in_used() / elem_size as u64;
175        let address_type = self.address_type;
176
177        self.with_info(|info| {
178            info.metadata.register_tensor(
179                buffer_len,
180                tensor.shape.clone(),
181                tensor.strides.clone(),
182                address_type,
183            )
184        });
185        Some(tensor.handle)
186    }
187
188    /// Push a new input array to the state.
189    pub fn register_buffer(&mut self, array: BufferArg, elem_size: usize) {
190        if let Some(tensor) = self.process_buffer(array, elem_size) {
191            self.push_resource(KernelResource::Buffer(tensor));
192        }
193    }
194
195    fn process_buffer(&mut self, array: BufferArg, elem_size: usize) -> Option<BufferBinding> {
196        let array = match array {
197            BufferArg::Handle { handle, .. } => handle,
198            BufferArg::Alias { input_pos, .. } => {
199                self.alias_io(input_pos);
200                return None;
201            }
202        };
203
204        let buffer_len = array.handle.size_in_used() / elem_size as u64;
205        let address_type = self.address_type;
206        self.with_info(|info| info.metadata.register_buffer(buffer_len, address_type));
207        Some(array.handle)
208    }
209
210    /// Push a buffer in memory shared with peers to the state. It is bound as a peer binding
211    /// whichever device it lives on, which is what tells the client the kernel declared it
212    /// shared; on its own device, the client binds it as an ordinary buffer.
213    pub fn register_peer(&mut self, buffer: PeerArg, elem_size: usize) {
214        let binding = self
215            .process_buffer(
216                BufferArg::Handle {
217                    handle: buffer.handle,
218                },
219                elem_size,
220            )
221            .expect("a handle is no alias");
222        let peer = PeerBinding::new(binding)
223            .expect("a peer argument is built from a handle shared with its peers");
224        self.push_resource(KernelResource::Peer(peer));
225    }
226
227    /// Push a new tensor to the state.
228    pub fn register_tensor_map<K: TensorMapKind>(
229        &mut self,
230        map: TensorMapArg<K>,
231        elem_size: usize,
232    ) {
233        let binding = self
234            .process_tensor(map.tensor, elem_size)
235            .expect("Can't use alias for TensorMap");
236
237        let map = map.metadata.clone();
238        self.push_resource(KernelResource::TensorMap(TensorMapBinding { binding, map }));
239    }
240}
241
242impl KernelLauncher {
243    pub fn new(settings: KernelSettings) -> Self {
244        Self {
245            address_type: settings.address_type,
246            settings,
247            resources: Vec::new(),
248            declared_io: Vec::new(),
249            declaring: BufferIOAttr::ReadWrite,
250            #[cfg(not(feature = "std"))]
251            info: InfoBuilder::default(),
252            #[cfg(not(feature = "std"))]
253            scope: Scope::dummy(),
254        }
255    }
256}
257
258#[cfg(test)]
259mod tests {
260    use super::*;
261    use cubecl_ir::settings::{Dim3, ExecutionMode};
262
263    fn settings() -> KernelSettings {
264        KernelSettings::new(Dim3::new_single(), ExecutionMode::Checked, AddressType::U32)
265    }
266
267    fn info_of(launcher: KernelLauncher) -> Vec<u64> {
268        launcher.into_bindings().info.data
269    }
270
271    /// `create_dummy_kernel` registers arguments into a launcher it never
272    /// launches. With `std` those registrations land in a thread-local that
273    /// only building the bindings drains, so the launcher has to be discarded
274    /// rather than dropped — otherwise the next real launch on the same
275    /// thread inherits them as extra arguments.
276    #[test]
277    fn a_discarded_launcher_leaves_nothing_for_the_next_launch() {
278        let empty = info_of(KernelLauncher::new(settings()));
279
280        // A registered scalar is visible in the info a launcher produces, so
281        // the equality below is a real claim about the thread-local, not a
282        // comparison of two things that could never differ.
283        let mut registered = KernelLauncher::new(settings());
284        registered.register_scalar(1u32);
285        assert_ne!(info_of(registered), empty);
286
287        let mut dummy = KernelLauncher::new(settings());
288        dummy.register_scalar(1u32);
289        dummy.discard();
290
291        assert_eq!(
292            info_of(KernelLauncher::new(settings())),
293            empty,
294            "a discarded launcher left its scalars behind for the next launch"
295        );
296    }
297}