Skip to main content

ruda_kernel/dsl/compute/
launcher.rs

1use alloc::{boxed::Box, vec::Vec};
2use core::marker::PhantomData;
3
4use crate::dsl::Runtime;
5use crate::dsl::prelude::{ArrayArg, TensorArg, TensorMapArg, TensorMapKind};
6use crate::dsl::{InfoBuilder, KernelSettings, ScalarArgType};
7#[cfg(any(feature = "frontend-std", feature = "std"))]
8use core::cell::RefCell;
9use ruda_core::ir::{AddressType, Scope, StorageType, Type};
10use ruda::runtime::server::{Binding, RudaCount, TensorMapBinding};
11use ruda::runtime::{
12    client::ComputeClient,
13    compiler::RudaTask,
14    kernel::{RudaKernel, KernelTask},
15    server::KernelArguments,
16};
17
18#[cfg(any(feature = "frontend-std", feature = "std"))]
19std::thread_local! {
20    static INFO: RefCell<InfoBuilder> = RefCell::new(InfoBuilder::default());
21    // Only used for resolving types
22    static SCOPE: RefCell<Scope> = RefCell::new(Scope::root(false));
23}
24
25/// A fully registered kernel invocation, not compiled or submitted yet.
26///
27/// Buffers, scalars and shape metadata are owned. The execution queue is fixed
28/// at preparation time. This is an opt-in building block for native graph
29/// backends, not stream capture or a CPU implementation of a GPU kernel.
30pub struct PreparedKernel<R: Runtime> {
31    task: Box<dyn RudaTask<R::Compiler>>,
32    count: RudaCount,
33    arguments: KernelArguments,
34    client: ComputeClient<R>,
35    scalar_words: usize,
36}
37
38impl<R: Runtime> PreparedKernel<R> {
39    /// Number of aligned u64 words in the packed scalar prefix. Backend graph
40    /// updates must keep the remaining (shape/stride/length) metadata unchanged.
41    pub fn scalar_words(&self) -> usize { self.scalar_words }
42
43    /// Consume the prepared invocation. Backends must honor the client's device
44    /// and queue, retain arguments, and validate any restrictions before launch.
45    pub fn into_parts(self) -> (
46        Box<dyn RudaTask<R::Compiler>>, RudaCount, KernelArguments, ComputeClient<R>,
47    ) {
48        (self.task, self.count, self.arguments, self.client)
49    }
50}
51
52/// Prepare a kernel for [launch](KernelLauncher::launch).
53pub struct KernelLauncher<R: Runtime> {
54    buffers: Vec<Binding>,
55    tensor_maps: Vec<TensorMapBinding>,
56    address_type: AddressType,
57    pub settings: KernelSettings,
58    #[cfg(not(any(feature = "frontend-std", feature = "std")))]
59    info: InfoBuilder,
60    #[cfg(not(any(feature = "frontend-std", feature = "std")))]
61    pub scope: Scope,
62    _runtime: PhantomData<R>,
63}
64
65impl<R: Runtime> KernelLauncher<R> {
66    #[cfg(any(feature = "frontend-std", feature = "std"))]
67    pub fn with_scope<T>(&mut self, fun: impl FnMut(&mut Scope) -> T) -> T {
68        SCOPE.with_borrow_mut(fun)
69    }
70
71    #[cfg(not(any(feature = "frontend-std", feature = "std")))]
72    pub fn with_scope<T>(&mut self, mut fun: impl FnMut(&mut Scope) -> T) -> T {
73        fun(&mut self.scope)
74    }
75
76    #[cfg(any(feature = "frontend-std", feature = "std"))]
77    fn with_info<T>(&mut self, fun: impl FnMut(&mut InfoBuilder) -> T) -> T {
78        INFO.with_borrow_mut(fun)
79    }
80
81    #[cfg(not(any(feature = "frontend-std", feature = "std")))]
82    fn with_info<T>(&mut self, mut fun: impl FnMut(&mut InfoBuilder) -> T) -> T {
83        fun(&mut self.info)
84    }
85
86    /// Register a scalar to be launched.
87    pub fn register_scalar<C: ScalarArgType>(&mut self, scalar: C) {
88        self.with_info(|info| info.scalars.push(scalar));
89    }
90
91    /// Register a scalar to be launched from raw data.
92    pub fn register_scalar_raw(&mut self, bytes: &[u8], dtype: StorageType) {
93        self.with_info(|info| info.scalars.push_raw(bytes, dtype));
94    }
95
96    /// Finish argument registration without submitting any GPU computation.
97    /// Construct each launcher and consume it before preparing the next one:
98    /// scalar/metadata registration uses the existing thread-local builder.
99    pub fn prepare<K: RudaKernel>(
100        mut self, count: RudaCount, kernel: K, client: &ComputeClient<R>,
101    ) -> PreparedKernel<R> {
102        let scalar_words = self.with_info(|info| info.scalars.len_aligned());
103        PreparedKernel {
104            scalar_words,
105            arguments: self.into_bindings(),
106            task: Box::new(KernelTask::<R::Compiler, K>::new(kernel)),
107            count,
108            client: client.fixed_execution_queue(),
109        }
110    }
111
112    /// Launch the kernel.
113    #[track_caller]
114    pub fn launch<K: RudaKernel>(
115        self,
116        ruda_count: RudaCount,
117        kernel: K,
118        client: &ComputeClient<R>,
119    ) {
120        let bindings = self.into_bindings();
121        let kernel = Box::new(KernelTask::<R::Compiler, K>::new(kernel));
122
123        client.launch(kernel, ruda_count, bindings)
124    }
125
126    /// Launch the kernel without check bounds.
127    ///
128    /// # Safety
129    ///
130    /// The kernel must not:
131    /// - Contain any out of bounds reads or writes. Doing so is immediate UB.
132    /// - Contain any loops that never terminate. These may be optimized away entirely or cause
133    ///   other unpredictable behaviour.
134    #[track_caller]
135    pub unsafe fn launch_unchecked<K: RudaKernel>(
136        self,
137        ruda_count: RudaCount,
138        kernel: K,
139        client: &ComputeClient<R>,
140    ) {
141        unsafe {
142            let bindings = self.into_bindings();
143            let kernel = Box::new(KernelTask::<R::Compiler, K>::new(kernel));
144
145            client.launch_unchecked(kernel, ruda_count, bindings)
146        }
147    }
148
149    /// We need to create the bindings in the same order they are defined in the compilation step.
150    ///
151    /// The function [`crate::dsl::KernelIntegrator::integrate`] stars by registering the input tensors followed
152    /// by the output tensors. Then the tensor metadata, and the scalars at the end. The scalars
153    /// are registered in the same order they are added. This is why we store the scalar data type
154    /// in the `scalar_order` vector, so that we can register them in the same order.
155    ///
156    /// Also returns an ordered list of constant bindings. The ordering between constants and tensors
157    /// is up to the runtime.
158    fn into_bindings(mut self) -> KernelArguments {
159        let mut bindings = KernelArguments::new();
160        let address_type = self.address_type;
161        let info = self.with_info(|info| info.finish(address_type));
162
163        bindings.buffers = self.buffers;
164        bindings.tensor_maps = self.tensor_maps;
165        bindings.info = info;
166
167        bindings
168    }
169}
170
171// Tensors/arrays
172impl<R: Runtime> KernelLauncher<R> {
173    /// Push a new input tensor to the state.
174    pub fn register_tensor(&mut self, tensor: TensorArg<R>, ty: Type) {
175        if let Some(tensor) = self.process_tensor(tensor, ty) {
176            self.buffers.push(tensor);
177        }
178    }
179
180    fn process_tensor(&mut self, tensor: TensorArg<R>, ty: Type) -> Option<Binding> {
181        let tensor = match tensor {
182            TensorArg::Handle { handle, .. } => handle,
183            TensorArg::Alias { .. } => return None,
184        };
185
186        let elem_size = ty.size();
187        let vectorization = ty.vector_size();
188
189        let buffer_len = tensor.handle.size_in_used() / elem_size as u64;
190        let len = tensor.shape.iter().product::<usize>() / vectorization;
191        let address_type = self.address_type;
192        self.with_info(|info| {
193            info.metadata.register_tensor(
194                tensor.strides.len() as u64,
195                buffer_len,
196                len as u64,
197                tensor.shape.clone(),
198                tensor.strides.clone(),
199                address_type,
200            )
201        });
202        Some(tensor.handle)
203    }
204
205    /// Push a new input array to the state.
206    pub fn register_array(&mut self, array: ArrayArg<R>, ty: Type) {
207        if let Some(tensor) = self.process_array(array, ty) {
208            self.buffers.push(tensor);
209        }
210    }
211
212    fn process_array(&mut self, array: ArrayArg<R>, ty: Type) -> Option<Binding> {
213        let array = match array {
214            ArrayArg::Handle { handle, .. } => handle,
215            ArrayArg::Alias { .. } => return None,
216        };
217
218        let elem_size = ty.size();
219        let vectorization = ty.vector_size();
220
221        let buffer_len = array.handle.size_in_used() / elem_size as u64;
222        let address_type = self.address_type;
223        self.with_info(|info| {
224            info.metadata.register_array(
225                buffer_len,
226                array.length[0] as u64 / vectorization as u64,
227                address_type,
228            )
229        });
230        Some(array.handle)
231    }
232
233    /// Push a new tensor to the state.
234    pub fn register_tensor_map<K: TensorMapKind>(&mut self, map: TensorMapArg<R, K>, ty: Type) {
235        let binding = self
236            .process_tensor(map.tensor, ty)
237            .expect("Can't use alias for TensorMap");
238
239        let map = map.metadata.clone();
240        self.tensor_maps.push(TensorMapBinding { binding, map });
241    }
242}
243
244impl<R: Runtime> KernelLauncher<R> {
245    pub fn new(settings: KernelSettings) -> Self {
246        Self {
247            address_type: settings.address_type,
248            settings,
249            buffers: Vec::new(),
250            tensor_maps: Vec::new(),
251            _runtime: PhantomData,
252            #[cfg(not(any(feature = "frontend-std", feature = "std")))]
253            info: InfoBuilder::default(),
254            #[cfg(not(any(feature = "frontend-std", feature = "std")))]
255            scope: Scope::root(false),
256        }
257    }
258}