Skip to main content

gpu_kernel/
lib.rs

1//! Running Rust code on a GPU is not as hard as it might sound and here is how it’s done!
2//!
3//! Let us start with the code, it takes just a few lines:
4//! ```rust,no_run
5//! // main.rs
6//! // GPU code is no-std and requires the nightly gpu_kernel ABI
7//! #![cfg_attr(feature = "gpu", no_std, feature(abi_gpu_kernel))]
8//!
9//! // Macro to compile and include the GPU code
10//! gpu_kernel::kernel_lib!();
11//!
12//! // Define a kernel, this function runs on the GPU
13//! #[gpu_kernel::kernel]
14//! fn kernel(s: &str) {
15//!     let id = gpu_kernel::intrinsics::workitem_id_x();
16//!     println!("Hello {s} from thread #{}!", id);
17//! }
18//!
19//! #[cfg(not(feature = "gpu"))]
20//! fn main() {
21//!     let s = "World".to_string();
22//!     // Launch 10 threads on the GPU
23//!     kernel.launch(
24//!         gpu_kernel::LaunchConfig::new()
25//!             .threads_per_workgroup([10, 1, 1])
26//!             .workgroups([1, 1, 1]),
27//!         &s,
28//!     );
29//! }
30//! ```
31//!
32//! This is all Rust code, it prints hello world from the GPU for each started thread:
33//! ```bash
34//! $ cargo run
35//! Hello World from thread #0!
36//! Hello World from thread #1!
37//! Hello World from thread #2!
38//! Hello World from thread #3!
39//! Hello World from thread #4!
40//! Hello World from thread #5!
41//! Hello World from thread #6!
42//! Hello World from thread #7!
43//! Hello World from thread #8!
44//! Hello World from thread #9!
45//! ```
46//!
47//! In `Cargo.toml`, we add `gpu-kernel` as a dependency and that’s it:
48//! ```toml
49//! # Cargo.toml
50//! [package]
51//! name = "hello_world"
52//! version = "0.1.0"
53//! edition = "2024"
54//!
55//! # This gets defined when building for the gpu.
56//! # It can be omitted when using target_arch or similar for cfg conditions, it exists for convenience only.
57//! [features]
58//! gpu = []
59//!
60//! [dependencies]
61//! gpu-kernel = "0.1"
62//! ```
63//!
64//! For `cargo run` to work, the GPU compute runtime needs to be installed, see the next section.
65//!
66//! ## Setup
67//!
68//! Currently, AMD GPUs are supported.
69//! Contributions for other Rust GPU targets are welcome, adding support to `gpu-kernel` should be relatively straightforward.
70//!
71//! Nightly Rust is currently required for the gpu_kernel ABI and GPU intrinsics.
72//!
73//! 1. Install ROCm. On Ubuntu 26.04, this is a simple `apt install rocm-dev`
74//! 1. Add `rust-src` to rustup to support build-std: `rustup component add rust-src`
75//! 1. Configure your GPU in cargo’s config, find your version with `rocminfo | grep gfx`:
76//!    ```toml
77//!    # ~/.cargo/config.toml
78//!    [target.amdgcn-amd-amdhsa]
79//!    rustflags = ["-Ctarget-cpu=gfx<your version>"]
80//!    # If rocminfo shows xnack- for your GPU, add "-Ctarget-feature=-xnack-support" as well
81//!    ```
82//!    Alternatively, specify the flags through an environment variable: `CARGO_TARGET_AMDGCN_AMD_AMDHSA_RUSTFLAGS=-Ctarget-cpu=gfx<your version>`
83//! 1. Set `HIP_PATH=/usr` for `hip-runtime-sys` to find the hip headers
84//!
85//! On NixOS, skip step 4 and add `rocmPackages.clr` to your dev shell to automagically set `HIP_DEVICE_LIB_PATH` and `HIP_PATH` or manually set `HIP_DEVICE_LIB_PATH="${rocmPackages.rocm-device-libs}/amdgcn/bitcode"` and `HIP_PATH="${rocmPackages.clr}"`.
86//!
87//! ## Settings
88//!
89//! Configuration files like `.cargo/config.toml` and `~/.cargo/config.toml` can be used to specify compiler flags as described in the [setup](#setup) section.
90//!
91//! Additionally, a few of environment variables can be set:
92//!
93//! | Env variable                               | Default                                         | Example               | Description                                                              |
94//! |--------------------------------------------|-------------------------------------------------|-----------------------|--------------------------------------------------------------------------|
95//! | `HIP_PATH`                                 | `/opt/rocm/hip`                                 | `/usr`                | Path to the hip installation to find headers                             |
96//! | `HIP_DEVICE_LIB_PATH`                      | `$(hipconfig -l)/../lib/clang/*/amdgcn/bitcode` |                       | Path to device libs, ends with `amdgcn/bitcode` and contains `.bc` files |
97//! | `CARGO_TARGET_AMDGCN_AMD_AMDHSA_RUSTFLAGS` | empty                                           | `-Ctarget-cpu=gfx900` | RUSTFLAGS used to compile amdgpu GPU code                                |
98//! | `CARGO_TARGET_AMDGCN_AMD_AMDHSA_FLAGS`     | empty                                           | `-v`                  | Cargo flags used to compile amdgpu GPU code                              |
99//!
100//! Several flags are added automatically to the GPU compilation.
101//!
102//! - If a `gpu` feature is defined in `Cargo.toml`, `--features=gpu` is passed to cargo
103//! - The `crate-type` is set to `cdylib`
104//! - Device libs are added to `link-arg`s and `-Clinker-plugin-lto` is enabled
105//! - core and alloc are built with `-Zbuild-std=core,alloc`
106//! - In debug mode, `opt-level=2` is set, as no optimizations can lead to crashes or compilation failures in the backend
107//! - In release mode, `panic=immediate-abort` is set for performance, so no panic messages are available
108#![deny(missing_docs)]
109#![cfg_attr(any(target_arch = "amdgpu", target_arch = "nvptx64"), no_std)]
110// Allocators will potentially be stabilized before all the GPU necessary stuff.
111#![cfg_attr(
112    not(any(target_arch = "amdgpu", target_arch = "nvptx64")),
113    feature(allocator_api)
114)]
115
116#[cfg(all(
117    feature = "amd",
118    not(any(target_arch = "amdgpu", target_arch = "nvptx64"))
119))]
120use std::alloc::AllocError;
121#[cfg(all(
122    feature = "amd",
123    not(any(target_arch = "amdgpu", target_arch = "nvptx64"))
124))]
125use std::ptr::NonNull;
126
127#[cfg(all(
128    feature = "amd",
129    not(any(target_arch = "amdgpu", target_arch = "nvptx64"))
130))]
131use hip_runtime_sys::hipError_t::hipSuccess;
132
133mod safe_kernel_arg;
134pub use safe_kernel_arg::*;
135
136pub use gpu_kernel_proc_macros::kernel;
137#[doc(hidden)]
138pub use gpu_kernel_proc_macros::{kernel_lib_impl_dbg, kernel_lib_impl_rel};
139
140#[cfg(all(
141    feature = "amd",
142    not(any(target_arch = "amdgpu", target_arch = "nvptx64"))
143))]
144#[doc(hidden)]
145pub use hip_runtime_sys;
146
147/// Items automatically imported for kernels.
148///
149/// These don’t appear in the docs as they are only available in GPU code.
150#[cfg(any(doc, target_arch = "amdgpu", target_arch = "nvptx64"))]
151pub mod prelude {
152    #[cfg(target_arch = "amdgpu")]
153    pub use amdgpu_device_libs::prelude::{print, println};
154}
155
156/// Some basic, useful intrinsics for GPU kernels.
157///
158/// Once there is more support in `core`, this will be removed.
159///
160/// These don’t appear in the docs as they are only available in GPU code.
161#[cfg(any(doc, target_arch = "amdgpu"))]
162pub mod intrinsics {
163    #[cfg(target_arch = "amdgpu")]
164    pub use amdgpu_device_libs::dispatch_ptr;
165    #[cfg(target_arch = "amdgpu")]
166    pub use amdgpu_device_libs::prelude::{
167        s_barrier, workgroup_id_x, workgroup_id_y, workgroup_id_z, workitem_id_x, workitem_id_y,
168        workitem_id_z,
169    };
170}
171
172/// The `kernel_lib!()` macro declares a crate as a library of GPU kernels.
173///
174/// It compiles the crate for the GPU and includes the compiled binary on the CPU side.
175///
176/// # Example
177///
178/// ```
179/// // Somewhere at the top-level of your crate
180/// gpu_kernel::kernel_lib!();
181/// ```
182#[macro_export]
183macro_rules! kernel_lib {
184    () => {
185        // Different calls for debug and release mode, so the kernels are compiled appropriately
186        #[cfg(not(any(target_arch = "amdgpu", target_arch = "nvptx64")))]
187        #[cfg(debug_assertions)]
188        ::gpu_kernel::kernel_lib_impl_dbg!();
189        #[cfg(not(any(target_arch = "amdgpu", target_arch = "nvptx64")))]
190        #[cfg(not(debug_assertions))]
191        ::gpu_kernel::kernel_lib_impl_rel!();
192
193        #[cfg(any(target_arch = "amdgpu", target_arch = "nvptx64"))]
194        extern crate alloc;
195    };
196}
197
198/// A GPU kernel never comes alone, it is always groups of them that are launched together.
199///
200/// A number of launched threads together are called a workgroup.
201/// And a number of workgroups are launched together.
202/// This struct specifies how many workgroups are launched and how many threads are contained in each of them.
203///
204/// The total number of threads launched is number of workgroups times threads per workgroup.
205///
206/// # Example
207///
208/// ```
209/// # use gpu_kernel::LaunchConfig;
210/// let launch_config = LaunchConfig::new()
211///     .workgroups([1, 0, 0])
212///     .threads_per_workgroup([1, 0, 0]);
213/// ```
214#[non_exhaustive]
215#[derive(Clone, Default, Eq, Hash, PartialEq)]
216pub struct LaunchConfig {
217    /// The number of workgroups launched on the GPU.
218    ///
219    /// A three-dimensional size for x, y, z dimensions.
220    /// For a simple list of threads, this can be `[n, 1, 1]`.
221    pub workgroups: Option<[u32; 3]>,
222    /// The number of threads in each workgroup.
223    ///
224    /// A three-dimensional size for x, y, z dimensions.
225    /// For a simple list of threads, this can be `[n, 1, 1]`.
226    pub threads_per_workgroup: Option<[u32; 3]>,
227}
228
229/// Allocate managed memory on AMD that lives on the CPU and is visible to the GPU as well.
230///
231/// On GPUs that support it (mostly MI cards), managed memory can be automatically transferred
232/// between CPU and GPU.
233/// See the [unified memory management] documentation.
234///
235/// With the `amd-allocator` crate feature (enabled by default), this is the default allocator.
236///
237/// [unified memory management]: https://rocm.docs.amd.com/projects/HIP/en/latest/how-to/hip_runtime_api/memory_management/unified_memory.html
238#[cfg(all(
239    feature = "amd",
240    not(any(target_arch = "amdgpu", target_arch = "nvptx64"))
241))]
242pub struct ManagedMemAlloc;
243
244/// Define global allocator.
245#[cfg(all(
246    feature = "amd-allocator",
247    not(any(target_arch = "amdgpu", target_arch = "nvptx64"))
248))]
249#[global_allocator]
250static HEAP: ManagedMemAlloc = ManagedMemAlloc;
251
252/// Allocate memory on the GPU, visible to the CPU as well.
253///
254/// [`GpuBox`] is a convenient `Box` using this allocator.
255#[cfg(all(
256    feature = "amd",
257    not(any(target_arch = "amdgpu", target_arch = "nvptx64"))
258))]
259pub struct GpuAlloc;
260
261/// A `Box` allocated on the GPU, also accessible from the CPU.
262///
263/// # Example
264///
265/// ```
266/// # use gpu_kernel::GpuBox;
267/// // This integer is allocated in GPU memory,
268/// // so fast to access on the GPU and slow to access on the CPU.
269/// let gpu_int = GpuBox::new(42);
270/// ```
271#[cfg(all(
272    feature = "amd",
273    not(any(target_arch = "amdgpu", target_arch = "nvptx64"))
274))]
275pub type GpuBox<T, A = GpuAlloc> = Box<T, A>;
276
277/// A loaded, compiled GPU binary.
278#[cfg(not(any(target_arch = "amdgpu", target_arch = "nvptx64")))]
279#[doc(hidden)]
280pub struct Module {
281    #[cfg(feature = "amd")]
282    module: hip_runtime_sys::hipModule_t,
283}
284#[cfg(not(any(target_arch = "amdgpu", target_arch = "nvptx64")))]
285unsafe impl Send for Module {}
286#[cfg(not(any(target_arch = "amdgpu", target_arch = "nvptx64")))]
287unsafe impl Sync for Module {}
288
289/// A loaded, compiled GPU kernel.
290///
291/// Can be launched on the GPU.
292///
293/// The `#[kernel]` macro adds a `launch` function that takes a [`&LaunchConfig`](`LaunchConfig`)
294/// as first argument and all kernel arguments afterwards.
295#[cfg(not(any(target_arch = "amdgpu", target_arch = "nvptx64")))]
296pub struct Kernel {
297    #[cfg(feature = "amd")]
298    func: hip_runtime_sys::hipFunction_t,
299}
300#[cfg(not(any(target_arch = "amdgpu", target_arch = "nvptx64")))]
301unsafe impl Send for Kernel {}
302#[cfg(not(any(target_arch = "amdgpu", target_arch = "nvptx64")))]
303unsafe impl Sync for Kernel {}
304
305#[cfg(all(
306    feature = "amd",
307    not(any(target_arch = "amdgpu", target_arch = "nvptx64"))
308))]
309struct HipStream(hip_runtime_sys::hipStream_t);
310
311#[cfg(all(
312    feature = "amd",
313    not(any(target_arch = "amdgpu", target_arch = "nvptx64"))
314))]
315thread_local! {
316    /// A thread-local stream to launch and wait for kernels.
317    static STREAM: std::cell::RefCell<HipStream> = std::cell::RefCell::new(HipStream::new());
318}
319
320impl LaunchConfig {
321    /// Create an empty `LaunchConfig`.
322    ///
323    /// At least [`Self::workgroups`] and [`Self::threads_per_workgroup`] need to be filled out, otherwise launching panics.
324    pub fn new() -> Self {
325        Default::default()
326    }
327
328    /// The number of workgroups launched on the GPU.
329    ///
330    /// A three-dimensional size for x, y, z dimensions.
331    /// For a simple list of threads, this can be `[n, 1, 1]`.
332    pub fn workgroups(&mut self, workgroups: [u32; 3]) -> &mut Self {
333        self.workgroups = Some(workgroups);
334        self
335    }
336
337    /// The number of threads in each workgroup.
338    ///
339    /// A three-dimensional size for x, y, z dimensions.
340    /// For a simple list of threads, this can be `[n, 1, 1]`.
341    pub fn threads_per_workgroup(&mut self, threads_per_workgroup: [u32; 3]) -> &mut Self {
342        self.threads_per_workgroup = Some(threads_per_workgroup);
343        self
344    }
345}
346
347#[cfg(all(
348    feature = "amd",
349    not(any(target_arch = "amdgpu", target_arch = "nvptx64"))
350))]
351unsafe impl std::alloc::GlobalAlloc for ManagedMemAlloc {
352    #[inline]
353    unsafe fn alloc(&self, layout: std::alloc::Layout) -> *mut u8 {
354        use std::ffi;
355        unsafe {
356            let mut ptr: *mut ffi::c_void = std::ptr::null_mut();
357            let result = hip_runtime_sys::hipMallocManaged(
358                &mut ptr,
359                layout.size(),
360                hip_runtime_sys::hipMemAttachGlobal,
361            );
362            assert_eq!(result, hipSuccess);
363            ptr as *mut _
364        }
365    }
366
367    #[inline]
368    unsafe fn dealloc(&self, ptr: *mut u8, _: std::alloc::Layout) {
369        unsafe {
370            let result = hip_runtime_sys::hipFree(ptr as *mut _);
371            assert_eq!(result, hipSuccess);
372        };
373    }
374}
375
376#[cfg(all(
377    feature = "amd",
378    not(any(target_arch = "amdgpu", target_arch = "nvptx64"))
379))]
380unsafe impl std::alloc::Allocator for GpuAlloc {
381    #[inline]
382    fn allocate(&self, layout: std::alloc::Layout) -> Result<NonNull<[u8]>, AllocError> {
383        use std::ffi;
384        unsafe {
385            let mut ptr: *mut ffi::c_void = std::ptr::null_mut();
386            let result = hip_runtime_sys::hipMalloc(&mut ptr, layout.size());
387            assert_eq!(result, hipSuccess);
388            Ok(NonNull::slice_from_raw_parts(
389                NonNull::new(ptr as *mut _).ok_or(AllocError)?,
390                layout.size(),
391            ))
392        }
393    }
394
395    #[inline]
396    unsafe fn deallocate(&self, ptr: NonNull<u8>, _: std::alloc::Layout) {
397        unsafe {
398            let result = hip_runtime_sys::hipFree(ptr.as_ptr() as *mut _);
399            assert_eq!(result, hipSuccess);
400        };
401    }
402}
403
404#[cfg(all(
405    feature = "amd",
406    not(any(target_arch = "amdgpu", target_arch = "nvptx64"))
407))]
408impl HipStream {
409    fn new() -> Self {
410        unsafe {
411            let mut stream: hip_runtime_sys::hipStream_t = std::ptr::null_mut();
412            let result = hip_runtime_sys::hipStreamCreate(&mut stream);
413            assert_eq!(result, hipSuccess);
414            Self(stream)
415        }
416    }
417}
418
419#[cfg(all(
420    feature = "amd",
421    not(any(target_arch = "amdgpu", target_arch = "nvptx64"))
422))]
423impl Drop for HipStream {
424    fn drop(&mut self) {
425        unsafe {
426            let result = hip_runtime_sys::hipStreamDestroy(self.0);
427            assert_eq!(result, hipSuccess);
428        }
429    }
430}
431
432/// Get the thread-local stream.
433///
434/// Internally copies the reference to make access simpler.
435#[cfg(all(
436    feature = "amd",
437    not(any(target_arch = "amdgpu", target_arch = "nvptx64"))
438))]
439fn thread_local_stream() -> hip_runtime_sys::hipStream_t {
440    STREAM.with_borrow(|s| s.0)
441}
442
443#[cfg(not(any(target_arch = "amdgpu", target_arch = "nvptx64")))]
444impl Module {
445    /// Load a module from a binary.
446    pub fn new(data: &[u8]) -> Self {
447        #[cfg(feature = "amd")]
448        unsafe {
449            let mut module: hip_runtime_sys::hipModule_t = std::ptr::null_mut();
450            let result = hip_runtime_sys::hipModuleLoadData(
451                &mut module,
452                data.as_ptr() as *const std::ffi::c_void,
453            );
454            assert_eq!(result, hipSuccess);
455            Self { module }
456        }
457    }
458
459    /// Get the kernel with the specified name from the loaded binary.
460    pub fn get_kernel(&self, name: &str) -> Kernel {
461        #[cfg(feature = "amd")]
462        unsafe {
463            let mut function: hip_runtime_sys::hipFunction_t = std::ptr::null_mut();
464            let kernel_name = std::ffi::CString::new(name).expect("Invalid kernel name");
465            let result = hip_runtime_sys::hipModuleGetFunction(
466                &mut function,
467                self.module,
468                kernel_name.as_ptr(),
469            );
470            assert_eq!(
471                result, hipSuccess,
472                "Failed to find kernel {:?}",
473                kernel_name
474            );
475            Kernel { func: function }
476        }
477    }
478}
479
480#[cfg(not(any(target_arch = "amdgpu", target_arch = "nvptx64")))]
481impl Kernel {
482    /// Get the raw kernel function.
483    #[cfg(feature = "amd")]
484    pub fn func(&self) -> hip_runtime_sys::hipFunction_t {
485        self.func
486    }
487
488    /// Launch a kernel, passing the given type as arguments.
489    ///
490    /// # Safety
491    ///
492    /// `T` must be the actual arguments expected by the kernel.
493    #[doc(hidden)]
494    pub unsafe fn launch_impl<T: ?Sized>(&self, launch_config: &LaunchConfig, args: &mut T) {
495        #[cfg(feature = "amd")]
496        {
497            use std::ffi;
498
499            let mut size = std::mem::size_of_val(args);
500
501            #[allow(clippy::manual_dangling_ptr)]
502            let mut config = [
503                0x1 as *mut ffi::c_void,                          // Next come arguments
504                args as *mut _ as *mut ffi::c_void,               // Pointer to arguments
505                0x2 as *mut ffi::c_void,                          // Next comes size
506                std::ptr::addr_of_mut!(size) as *mut ffi::c_void, // Pointer to size of arguments
507                0x3 as *mut ffi::c_void,                          // End
508            ];
509
510            let workgroups = launch_config
511                .workgroups
512                .expect("Must set `workgroups` in LaunchConfig");
513            let threads_per_workgroup = launch_config
514                .threads_per_workgroup
515                .expect("Must set `threads_per_workgroup` in LaunchConfig");
516
517            unsafe {
518                let stream = thread_local_stream();
519                // Launch two workgroups (2x1x1), each of the size (LEN/2)x1x1
520                let result = hip_runtime_sys::hipModuleLaunchKernel(
521                    self.func,
522                    workgroups[0],
523                    workgroups[1],
524                    workgroups[2],
525                    threads_per_workgroup[0],
526                    threads_per_workgroup[1],
527                    threads_per_workgroup[2],
528                    0,                    // sharedMemBytes for extern shared variables
529                    stream,               // stream
530                    std::ptr::null_mut(), // params (unimplemented in hip)
531                    config.as_mut_ptr(),  // arguments
532                );
533                assert_eq!(result, hipSuccess, "Failed to launch kernel");
534
535                let result = hip_runtime_sys::hipStreamSynchronize(stream);
536                assert_eq!(result, hipSuccess, "Failed to wait for kernel to finish");
537            }
538        }
539    }
540}