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}