Write and run a Rust GPU kernel in under 30 minutes — no CUDA experience required.
This guide walks you through setup, running a pre-built example, writing a
compute kernel from scratch, using transparent data with GpuArray<T>, and
exploring the patched standard library for full Rust on GPU.
Prerequisites: Linux x86_64 (or WSL2), an NVIDIA GPU (SM 70+, i.e.,
GTX 1660 / RTX 2060 or newer), working nvidia-smi, and
rustup installed.
Clone the repository and run the one-command setup script:
git clone https://github.com/DaLaw2/async-gpu.git
cd async-gpu
./scripts/setup.sh --quicksetup.sh --quick installs the pinned nightly toolchain (currently
nightly-2026-06-03), the nvptx64-nvidia-cuda target, required components
(rust-src, llvm-tools, llvm-bitcode-linker), and compiles a smoke-test
PTX kernel to verify everything works.
Expected output (last few lines):
✓ Nightly toolchain installed
✓ nvptx64-nvidia-cuda target added
✓ Components installed (rust-src, llvm-tools, llvm-bitcode-linker)
✓ Smoke test PTX compiled successfully
Verify the setup:
./scripts/setup.sh --checkThe pinned toolchain version is defined in rust-toolchain.toml at the
repository root. All workspace crates use this toolchain automatically.
Before writing any code, run a pre-built example to see GPU output:
cargo run --manifest-path examples/hostcall/hello-gpu/host/Cargo.tomlThe first build takes 60-90 seconds (compiling dependencies). You should see:
=== Hello GPU Example ===
--- Demo 1: GPU println ---
[GPU] Hello from GPU!
[host] hostcall_print_hello: PASSED
--- Demo 2: GPU file I/O ---
[host] File created and written from GPU
[host] hostcall_file_test: PASSED
--- Demo 3: GPU threading ---
[host] Thread 1 computed: 42 (expected 42)
[host] Thread 2 computed: 99 (expected 99)
[host] thread_spawn_test: PASSED
This example uses the one-liner API — each demo is a single function call:
use async_gpu::gpu;
fn main() -> async_gpu::Result<()> {
// Launch a hostcall-enabled kernel (supports println!, file I/O)
gpu::run("my_kernel")?;
// Pure compute with output buffer
let result: Vec<u32> = gpu::launch("compute_kernel", 1024, 256)?;
Ok(())
}Key concept: every async-gpu project has two crates — a kernel crate
(compiled for nvptx64-nvidia-cuda, runs on GPU) and a host crate
(compiled for your CPU, drives the GPU).
We will implement SAXPY — the canonical GPU compute operation:
y[i] = a * x[i] + y[i].
Create the kernel crate directory structure. From the repo root:
mkdir -p examples/getting-started/kernel/src
mkdir -p examples/getting-started/kernel/.cargo[workspace]
[package]
name = "getting-started-kernel"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
[profile.release]
panic = "abort"
opt-level = 3
lto = "fat"The kernel has no dependencies — it uses only core (no standard library on
GPU by default). The cdylib crate type tells the compiler to produce a
single output artifact (PTX in this case).
[build]
target = "nvptx64-nvidia-cuda"
[target.nvptx64-nvidia-cuda]
linker = "llvm-bitcode-linker"
rustflags = ["-C", "target-cpu=sm_75", "-C", "target-feature=+ptx78"]
[unstable]
build-std = ["core"]
build-std-features = ["compiler-builtins-mem"]This tells Cargo to cross-compile for NVIDIA GPUs using -Zbuild-std=core.
Adjust target-cpu if your GPU uses a different SM version (e.g., sm_70
for Tesla V100, sm_86 for RTX 3060).
#![no_std]
#![feature(abi_gpu_kernel)]
#![feature(stdarch_nvptx)]
#![feature(asm_experimental_arch)]
use core::arch::nvptx;
#[panic_handler]
fn panic(_info: &core::panic::PanicInfo) -> ! {
unsafe {
core::arch::asm!("trap;");
}
loop {}
}
/// SAXPY: y[i] = a * x[i] + y[i]
///
/// Each GPU thread computes one element. Thread index is derived from
/// the block/thread hierarchy: idx = blockIdx.x * blockDim.x + threadIdx.x
#[no_mangle]
pub unsafe extern "gpu-kernel" fn saxpy(
x: *const f32,
y: *mut f32,
a: f32,
n: u32,
) {
let idx = nvptx::_block_idx_x() as u32
* nvptx::_block_dim_x() as u32
+ nvptx::_thread_idx_x() as u32;
if idx < n {
let val = *x.add(idx as usize);
let cur = *y.add(idx as usize);
*y.add(idx as usize) = a * val + cur;
}
}What each part does:
#![no_std]— GPU kernels do not have access to the standard library by default.#![feature(abi_gpu_kernel)]— enables theextern "gpu-kernel"calling convention.#![feature(stdarch_nvptx)]— enablescore::arch::nvptxintrinsics for thread indexing.#[panic_handler]— required forno_std; traps on panic.#[no_mangle]— preserves the function name so the host can find it by name.extern "gpu-kernel"— marks this as a GPU entry point (like__global__in CUDA C).- Thread indexing —
blockIdx.x * blockDim.x + threadIdx.xgives each thread a unique index. - Bounds check —
if idx < nprevents out-of-bounds access when the total thread count exceeds the data size.
The host program uploads data to the GPU, launches the kernel, and downloads results.
mkdir -p examples/getting-started/host/src[workspace]
[package]
name = "getting-started-host"
version = "0.1.0"
edition = "2021"
[dependencies]
async-gpu = { path = "../../../crates/async-gpu" }
cudarc = { version = "0.12", features = ["cuda-12060"] }The build script compiles the kernel crate to PTX so the host can embed it at compile time. Copy from an existing example — it handles toolchain detection and PTX patching automatically:
cp examples/hostcall/vector-math/host/build.rs examples/getting-started/host/build.rsThen update the PTX filename to match your kernel crate name (hyphens become underscores):
// In build.rs, change the PTX source path:
let ptx_src = kernel_dir
.join("target")
.join("nvptx64-nvidia-cuda")
.join("release")
.join("getting_started_kernel.ptx"); // matches the crate nameuse async_gpu::gpu;
/// Embed the compiled PTX at compile time.
const KERNEL_PTX: &str = include_str!(concat!(env!("OUT_DIR"), "/kernel.ptx"));
fn main() -> async_gpu::Result<()> {
const N: usize = 1024;
// Input data
let x: Vec<f32> = (0..N).map(|i| i as f32).collect();
let y: Vec<f32> = (0..N).map(|i| (i * 2) as f32).collect();
let a = 2.0f32;
// 1. Create a launch context: load PTX, set 256 threads/block,
// auto-compute grid to cover N elements
let ctx = gpu::custom("saxpy")
.ptx(KERNEL_PTX)
.threads(256)
.elements(N as u32)
.prepare()?;
// 2. Upload data to GPU (host-to-device copy)
let x_dev = ctx.upload(&x)?;
let mut y_dev = ctx.upload(&y)?;
// 3. Launch the kernel — arguments must match the kernel signature
let result = unsafe { ctx.launch((&x_dev, &mut y_dev, a, N as u32))? };
// 4. Download results (device-to-host copy)
let y_result = result.download(&y_dev)?;
// Verify: y[i] should equal a * x[i] + y_original[i]
for i in 0..N {
let expected = a * x[i] + y[i];
assert!(
(y_result[i] - expected).abs() < 0.001,
"Mismatch at index {i}: got {}, expected {expected}",
y_result[i]
);
}
println!("SAXPY computed {N} elements correctly!");
Ok(())
}The builder API flow:
gpu::custom("saxpy")— start building a launch for the"saxpy"kernel..ptx(KERNEL_PTX)— load your compiled PTX (embedded at compile time)..threads(256)— 256 threads per block (a common default)..elements(N as u32)— auto-compute grid dimensions to cover N elements..prepare()?— initialize the CUDA device and load the kernel.ctx.upload(&x)?— copy a host slice to GPU memory.ctx.launch(args)?— launch the kernel and wait for completion.result.download(&y_dev)?— copy GPU memory back to the host.
Build and run your SAXPY program:
cargo run --manifest-path examples/getting-started/host/Cargo.toml --releaseThe first build compiles the kernel to PTX (~30-60 seconds) and the host binary. Expected output:
SAXPY computed 1024 elements correctly!
Congratulations — you just ran Rust on a GPU.
GpuArray<T> is a host-device container that manages data residency
automatically. Instead of explicit upload() and download() calls, data
transfers happen lazily when needed.
use async_gpu::{gpu, GpuArray};
fn main() -> async_gpu::Result<()> {
// Create a GpuArray from host data — starts in HostOnly state
let input = GpuArray::from_vec(vec![1.0f32, 2.0, 3.0, 4.0]);
let ctx = gpu::custom("my_kernel")
.ptx(KERNEL_PTX)
.threads(256)
.elements(4)
.prepare()?;
// bind_gpu_array() lazily uploads to device on first use
let input_ptr = ctx.bind_gpu_array(&input)?;
// ... launch kernel with input_ptr as a u64 argument ...
Ok(())
}Residency states: HostOnly (initial), DeviceOnly (after GPU write),
Both (synced), Modified (device-side changes pending download).
AutoTuner searches for the best block size for a kernel by running multiple
configurations and measuring execution time:
use async_gpu::gpu;
use gpu_host::auto_tune::{AutoTuner, TuningCache, TuningKey};
let tuner = AutoTuner::new();
let cache = TuningCache::new();
let key = TuningKey::new("saxpy", N as u64, 0);
// Check cache first, then auto-tune if needed
let best_threads = match cache.get_config(&key) {
Some(threads) => threads,
None => {
// Generate candidate block sizes (32, 64, 128, 256, 512, 1024)
let candidates = tuner.generate_candidates(None);
// Run each candidate and pick the fastest — your benchmarking code here
let best = 256u32; // placeholder
cache.insert_config(key, best);
best
}
};The tuning cache persists results so subsequent runs skip the search phase.
Write GPU tests using the #[gpu_test] attribute macro, then run them with
cargo test:
// In your kernel crate (with patched std):
#[gpu_test]
fn test_addition() {
let result = 2 + 2;
assert!(result == 4, "Math is broken on this GPU");
}GPU assertions report the block ID, warp ID, and lane ID where the failure
occurred. Tests integrate with the standard cargo test runner.
The examples above use #![no_std] kernels. For a richer experience,
async-gpu supports a patched standard library that enables println!(),
File I/O, Vec, String, HashMap, and std::thread::spawn directly
on the GPU.
./scripts/setup.sh --stdThis patches the Rust standard library's platform abstraction layer (PAL) to route system calls through the hostcall protocol — the GPU sends requests to the host CPU, which executes them and returns results.
With the patched std, your kernel can use familiar Rust:
// No more #![no_std] — use the full standard library
#![feature(abi_gpu_kernel)]
#[no_mangle]
pub unsafe extern "gpu-kernel" fn my_kernel(result: *mut u32) {
println!("[GPU] Hello from Rust std on GPU!");
// Use Vec, String, format! — all work
let data: Vec<f32> = (0..100).map(|i| i as f32).collect();
let sum: f32 = data.iter().sum();
println!("[GPU] Sum of 0..100 = {sum}");
// File I/O — reads/writes real files on the host filesystem
use std::io::Write;
let mut f = std::fs::File::create("/tmp/gpu_output.txt").unwrap();
writeln!(f, "GPU computed sum = {sum}").unwrap();
// Thread spawning — each warp acts as a thread
let handle = std::thread::spawn(|| 42u32);
let value = handle.join().unwrap();
*result = value;
}The kernel's .cargo/config.toml changes to build with std:
[unstable]
build-std = ["std"]
build-std-features = []println!, format!, Vec, String, Box, HashMap, Mutex,
std::fs::File (create/read/write), std::io::stdin().read_line(),
std::thread::spawn + JoinHandle::join(), Result<T, E> with ?,
assert! with GPU metadata (block/warp/lane).
See examples/std/thread-demo/ for a complete working example.
async-gpu/
├── crates/
│ ├── async-gpu/ # Facade crate — what users depend on
│ ├── core/
│ │ ├── gpu-host/ # Host-side runtime, launch API, nn module
│ │ ├── gpu-runtime/ # GPU-side runtime (iterators, scopes, channels)
│ │ ├── gpu-protocol/ # Shared constants (hostcall services, buffer layout)
│ │ ├── gpu-atomics/ # GPU-compatible atomic operations
│ │ └── gpu-libc/ # C library shim for GPU std
│ ├── kernel/
│ │ ├── gpu-kernel-core/ # Core kernel functions
│ │ ├── gpu-kernel-compute/ # Compute kernels (GEMM, attention, conv, etc.)
│ │ ├── gpu-kernel-io/ # I/O kernels
│ │ └── gpu-kernel-test/ # Test kernels
│ └── test/ # Test harness crates
├── examples/
│ ├── hostcall/ # Examples using hostcall protocol
│ │ ├── hello-gpu/ # Minimal GPU hello world
│ │ ├── vector-math/ # Dot product and softmax
│ │ └── ... # 10 total
│ └── std/ # Examples using patched standard library
│ ├── thread-demo/ # GPU threading
│ ├── gpt2-inference/ # GPT-2 text generation
│ ├── mnist-train/ # MNIST MLP training
│ └── ... # 14 total
├── patched-std/ # Patches for Rust standard library
├── patched-rustc/ # Patches for rustc (warp-cooperative MIR pass)
└── scripts/ # Build and setup scripts
- More compute patterns: See
examples/hostcall/vector-math/for dot product and softmax kernels using the same builder API. - One-liner API: For simpler cases,
gpu::run("kernel")andgpu::launch("kernel", n, threads)skip the builder pattern entirely. Seeexamples/hostcall/hello-gpu/. - GPU I/O and threading: Run
./scripts/setup.sh --stdto enableprintln!(),File::read/write, andstd::thread::spawnon GPU. Seeexamples/std/thread-demo/. - Neural networks: The
nnmodule provides PyTorch-style layers and autograd. Seeexamples/std/gpt2-inference/andexamples/std/mnist-train/. Enable withfeatures = ["nn"]. - Async/tokio integration: Use
features = ["async"]forAsyncGpuRuntimeandGpuTask. Seeexamples/hostcall/tokio-offload/. - API reference:
cargo doc --open -p async-gpu
Your nightly toolchain is not set up correctly. Run:
./scripts/setup.sh --checkThe project requires a specific nightly pinned in rust-toolchain.toml. Run
./scripts/setup.sh --quick to install it.
Check that the nvptx64 target and llvm-bitcode-linker are installed:
rustup target list --toolchain nightly-2026-06-03 | grep nvptx
rustup component list --toolchain nightly-2026-06-03 | grep llvm-bitcodeIf missing, re-run ./scripts/setup.sh --quick.
The kernel crate failed to compile. Check the build output above the error for the actual compilation failure. Common causes:
- Missing
.cargo/config.tomlin the kernel directory - Wrong crate name in
build.rsPTX path (must match the crate name with hyphens replaced by underscores)
No GPU detected. Verify your NVIDIA driver:
nvidia-smiIf this fails, your driver is not installed or the GPU is not available.
The PTX was compiled for a different GPU architecture. Check that the
target-cpu in kernel/.cargo/config.toml matches your GPU. Use sm_75
for most RTX 16xx/20xx/30xx/40xx cards, or sm_70 for Tesla V100.