CUDA Rust Skill

Prompt

CUDA Rust Skill

Creator:

About this prompt

Expert guidance for writing native NVIDIA GPU kernels in Rust with CUDA Rust, the SIMT track (cuda-oxide, a rustc codegen backend using #[kernel], DisjointSlice, launch contracts, cargo oxide) and the Tile track (cutile-rs / the cutile crate, #[cutile::module], #[cutile::entry], Tensor partitioning, CUDA Tile IR JIT). Use this skill whenever the user wants to write, port, debug, review, or choose between GPU kernels in Rust; mentions cuda-oxide, cutile, cuTile, CUDA Tile, Tile IR, PTX from Rust, cargo oxide, DisjointSlice, launch_contract, or partition(); asks how to move a CUDA C++, numba-cuda, or Triton kernel into Rust; or compares these with rust-cuda, Rust-GPU, CubeCL, or cudarc. Trigger even when the user just says "GPU code in Rust" or "CUDA in Rust".

Characters19,807
Words2,889
~Tokens4,952
Size19.4 KB

CUDA Rust

NVIDIA's CUDA Rust (announced Sep 8, 2026) compiles GPU kernels written in Rust natively to PTX, instead of writing kernels in C++ and only launching them from Rust. Two tracks mirror CUDA's two models. Both are early-stage and not production-ready; APIs will move.

SIMT: cuda-oxideTile: cutile-rs (crate cutile)
ModelWrite what one thread doesWrite what one tile (sub-tensor) does; compiler picks threads
Compilerustc codegen backend: MIR → Pliron IR (NVIDIA GPU dialects) → LLVM IR → PTX, ahead of timeMacro embeds kernel AST; JIT via CUDA Tile IR on first launch per specialization
ToolchainPinned nightly (nightly-2026-04-03), clang + libclang headers, CUDA toolkit 12.x+Stable Rust ≥ 1.89, CUDA 13.3, no LLVM
PlatformLinux, compute capability ≥ 8.0Linux, compute capability ≥ 8.0
Output exclusivityDisjointSlice<T>: one element per threadhost .partition() + &mut Tensor: one sub-tensor per tile
Geometry safety#[launch_contract] + prepare_<k>() tokenderived from partition; can't mismatch
Aliasing checkper launch call (E0502)ownership moves across launch (E0382) — stronger
You controlindexing, block size, shared memory (unsafe today)nothing about threads/shared memory
Maturityearly alphafurther along; on crates.io; used by HuggingFace Grout, mistral.rs

Rules for using this skill

  • Confidence discipline. Public material covers 1-D elementwise kernels only. Prefer API marked Confirmed below. When using anything Inferred (2-D/3-D indexing, scalar args, reductions, math functions, tuple accessors beyond .first()), tell the user in one line which identifiers to verify in the cuda-oxide book / cuTile Rust docs. Never invent versions, flags, or features. With web access, check the repos for anything version-sensitive (nightly pin, CUDA version).
  • Name collision: an unrelated, apparently unmaintained cuda-oxide crate exists on crates.io. Install from NVlabs git; never cargo add cuda-oxide.
  • Response shapes. Write a kernel: one-line track choice (if not given) → complete single-file program (imports, host + device, verification) → explanation of safety-relevant lines → Inferred identifiers to verify. Port: compact concept map → Rust code → behavioral differences. Debug: identify track, quote key error, explain the invariant, give the fix. "Which one?": recommend, don't survey.

Choosing a track

Default to Tile (NVIDIA's recommendation): the compiler maps tiles to each architecture, so source doesn't encode architecture choices; stable Rust; no threads to race.

Choose SIMT when the user needs explicit per-thread indexing (irregular access, custom stencils), block shared memory, block-geometry control, a structure-preserving port of CUDA C++/numba-cuda, or AOT kernels. Also: CUDA stuck at 12.x → only cuda-oxide; locked to stable Rust → cutile-rs; need maturity → cutile-rs.

Language and model are separate choices: CUDA Tile also exists in C++ and Python; NVIDIA plans cross-language interop.

Rules both tracks share

  1. Inputs shared, output has one writer. Read-only: &[T] / &Tensor. Writable: DisjointSlice<T> / partitioned &mut Tensor. &mut [T] can't work for SIMT: every thread would hold the same mutable borrow.
  2. Never pass one buffer as input and output of a launch — rejected at compile time whether or not the kernel would race. Use a separate output, or (SIMT) make it the DisjointSlice and read through your own element. Don't defeat the check with unsafe.
  3. Bounds are a branch. SIMT get_mut(idx)Option. Tile load_tile_like aligns input with the output tile.
  4. SIMT indices are typed; .get() gives the raw usize for reading inputs only.
  5. Always add #[launch_contract] (SIMT); without it only raw unsafe launchers exist.
  6. Tile is lazy until .sync_on(&stream); SIMT to_host_vec(&stream) copies and syncs. Time only after sync and warm-up (Tile JIT).
  7. Tile launchers consume all tensors and return them as a tuple.

SIMT track: cuda-oxide

Setup

rustup toolchain install nightly-2026-04-03 cargo +nightly-2026-04-03 install --git https://github.com/NVlabs/cuda-oxide.git cargo-oxide cargo oxide new vecadd_demo && cd vecadd_demo cargo oxide doctor # checks GPU, CUDA, clang/libclang, nightly, optional system LLVM cargo oxide run # first run builds the codegen backend: slow; cached after

Host and device code live in one file, one crate, one build command. Expected template output: PASSED: all 1024 elements correct.

Confirmed API

ItemCrateRole
#[cuda_module] mod m {..}cuda_hostDevice module; generates m::load, prepare_<k>, safe <k> launcher
#[kernel]cuda_deviceGPU entry point
#[launch_bounds(N)]cuda_deviceMax threads per block (register budget)
#[launch_contract(domain = 1, block = (256, 1, 1))]cuda_deviceIndexing dims + block shape
thread::index_1d(), idx.get()cuda_deviceTyped thread index; raw usize
DisjointSlice<T>, .get_mut(idx) -> Option<&mut T>cuda_devicePer-thread exclusive output
CudaContext::new(0)?, ctx.default_stream()cuda_coreContext, stream
DeviceBuffer::from_host(&stream, &v)?, DeviceBuffer::<T>::zeroed(&stream, n)?cuda_coreUpload / zeroed alloc
buf.to_host_vec(&stream)?cuda_coreCopy down + synchronize
unsafe { m::load(&ctx)? }generatedLoad embedded device bundle
LaunchConfig1D::new(blocks, threads_per_block, shared_mem_bytes)cuda_coreGeometry
module.prepare_<k>(cfg)?generatedValidates vs contract + live device limits → proof token
module.<k>(&stream, &prepared, &in.., &mut out)?generatedSafe launch

Template

use cuda_device::{kernel, launch_bounds, launch_contract, thread, DisjointSlice}; use cuda_host::cuda_module; use cuda_core::{CudaContext, DeviceBuffer, LaunchConfig1D}; #[cuda_module] mod kernels { use super::*; #[kernel] #[launch_bounds(256)] #[launch_contract(domain = 1, block = (256, 1, 1))] pub fn vecadd(a: &[f32], b: &[f32], mut c: DisjointSlice<f32>) { let idx = thread::index_1d(); let i = idx.get(); if let Some(c_elem) = c.get_mut(idx) { // None for over-launched tail threads *c_elem = a[i] + b[i]; } } } fn main() -> Result<(), Box<dyn std::error::Error>> { let ctx = CudaContext::new(0)?; let stream = ctx.default_stream(); const N: usize = 1024; let a_host: Vec<f32> = (0..N).map(|i| i as f32).collect(); let b_host: Vec<f32> = (0..N).map(|i| (i * 2) as f32).collect(); let a_dev = DeviceBuffer::from_host(&stream, &a_host)?; let b_dev = DeviceBuffer::from_host(&stream, &b_host)?; let mut c_dev = DeviceBuffer::<f32>::zeroed(&stream, N)?; // SAFETY: this package owns the embedded device bundle produced for `kernels`. let module = unsafe { kernels::load(&ctx)? }; let prepared = module.prepare_vecadd(LaunchConfig1D::new((N as u32).div_ceil(256), 256, 0))?; module.vecadd(&stream, &prepared, &a_dev, &b_dev, &mut c_dev)?; let c_host = c_dev.to_host_vec(&stream)?; // syncs let errors = (0..N).filter(|&i| (c_host[i] - (a_host[i] + b_host[i])).abs() > 1e-5).count(); if errors == 0 { println!("PASSED: all {N} elements correct"); } else { eprintln!("FAILED: {errors} errors"); std::process::exit(1); } Ok(()) }

Notes: keep launch_bounds, contract block size, and threads-per-block equal. div_ceil over-launches the tail; get_mut's Option makes that safe. Guard i < a.len() if inputs can be shorter than the output. Prepare once and reuse the token for the same geometry (Inferred). load is unsafe — keep a // SAFETY: comment.

Patterns

  • Elementwise map (Confirmed shape): change the body, e.g. ReLU *o = if x[i] > 0.0 { x[i] } else { 0.0 };.
  • In-place update: make the array the DisjointSlice and read your own element: *yi = alpha[0] * x[i] + *yi;. Race-free. Passing a from_host buffer as &mut is Inferred allowed.
  • Scalars (Inferred): try alpha: f32; confirmed-API fallback is a 1-element DeviceBuffer read as alpha[0].
  • 2-D/3-D (Inferred: domain = 2, index_2d, LaunchConfig2D): safe fallback is 1-D over w*h with (i % w, i / w).
  • Gather out[i] = src[idx[i] as usize]: fine (only out written); bounds-check idx[i].
  • Scatter out[idx[i]] = ..: not expressible safely by design. Use sort-then-segment, per-thread slots, or documented unsafe.
  • Reductions: multi-pass halving (out[i] = in[2i] + in[2i+1] into a half-size buffer), or prefer Tile. Block-local accumulation needs shared memory.
  • Shared memory: sized by the third LaunchConfig1D arg; access is unsafe today (safe path is active work). Don't invent the accessor; point to the book.
  • Perf: keep data on device across launches; set launch_bounds to the real max; benchmark after sync + warm-up.

Tile track: cutile-rs

Setup

cargo new vecadd_demo && cd vecadd_demo cargo add cutile cargo run # repo examples: clone cutile-rs, then cargo run -p cutile-examples --example hello_world

Confirmed API

ItemRole
use cutile::prelude::*; (host), use cutile::core::*; (inside kernel module)Imports
#[cutile::module] mod m {..}Captures AST; generates host launchers
#[cutile::entry()]Kernel entry
Tensor<T, { [B] }> with const B: i32Static dim; each B is a specialization
Tensor<T, { [-1] }>Dynamic dim read at launch; no recompile
load_tile_like(x, z)Tile of x aligned with current sub-tensor of z
z.store(expr); tx + tyWrite tile; elementwise tile arithmetic
Device::new(0)?, device.new_stream()?Device, stream
api::ones::<T>(&[n]), api::zeros::<T>(&[n])Lazy constructors
.partition([w])Exclusivity + grid (n/w tiles) + supplies B
m::entry(z, x, y)Launcher: takes ownership, returns tuple lazily
.first(), .unpartition(), .to_host_vec(), .sync_on(&stream)?Select output, drop wrapper (no data moves), record copy, execute
ErrorResult error type

Template

use cutile::prelude::*; #[cutile::module] mod kernel { use cutile::core::*; #[cutile::entry()] fn add<const B: i32>( z: &mut Tensor<f32, { [B] }>, // exclusive output tile x: &Tensor<f32, { [-1] }>, // shared, dynamic length y: &Tensor<f32, { [-1] }>, ) { let tx = load_tile_like(x, z); // runs once per tile, one logical thread let ty = load_tile_like(y, z); z.store(tx + ty); } } fn main() -> Result<(), Error> { let device = Device::new(0)?; let stream = device.new_stream()?; let x = api::ones::<f32>(&[1024]); // lazy let y = api::ones::<f32>(&[1024]); let z = api::zeros::<f32>(&[1024]).partition([128]); // 8 tiles, B = 128 let c: Vec<f32> = kernel::add(z, x, y) .first() .unpartition() .to_host_vec() .sync_on(&stream)?; // everything runs here let errors = c.iter().filter(|&&v| (v - 2.0).abs() > 1e-5).count(); if errors == 0 { println!("PASSED: all {} elements correct", c.len()); } else { eprintln!("FAILED: {errors} errors"); std::process::exit(1); } Ok(()) }

Notes: only mutable tensors are partitioned, and a &mut output must be. Shapes are literals in the confirmed code; the integer type for variable shapes is undocumented. Ragged tails (length not divisible by width) are Inferred/unknown: pad, or choose a dividing width. Errors surface at JIT/sync_on, so insert an earlier sync_on to isolate. Warm up before benchmarking.

Patterns

  • Elementwise: change the stored expression. -, *, / likely; math functions (exp, sqrt, maximum) Inferred.
  • More inputs: extra &Tensor<T, {[-1]}> params, each load_tile_like(_, z).
  • Scalars (Inferred): plain alpha: f32, 1-element tensor, or const generic (recompiles per value — avoid if it varies).
  • In-place: not with the same tensor (E0382); allocate a new partitioned output and rebind (lazy, so cheap to record).
  • Chaining: pass .first().unpartition() output into the next launcher; one sync_on at the end. Other tuple accessors are Inferred.
  • Multi-dim, reductions, attention-style: Tile IR targets these, but the Rust surface isn't shown; study cutile-examples, mistral.rs, and Grout first.

Safety model and compile errors

Thousands of threads hit the same buffers in no fixed order; races rarely reproduce and pass tests before failing in production. Both tracks encode "shared inputs, single-writer output" in types (paper and RustConf 2026 talk: Fearless Concurrency on the GPU).

SIMT E0502: module.vecadd(&stream, &prepared, &c_dev, &b_dev, &mut c_dev)?cannot borrow c_dev as mutable because it is also borrowed as immutable. Fix: distinct output, or read-through-DisjointSlice for true in-place.

Tile E0382: kernel::add(z.partition([128]), z, y)use of moved value: z. Fix: separate output. Don't .clone() a handle as a workaround unless docs confirm clone semantics — it may re-alias.

Tile's check is stronger because ownership follows tensors across the asynchronous launch; oxide's borrows end when the call returns.

Not guaranteed: numerics/overflow/NaN, OOB reads of shorter inputs (SIMT), correctness of unsafe shared memory, bundle match in load, performance, API stability.

Reviewing unsafe (oxide): require // SAFETY: stating bundle ownership (load); why a raw launch skips contract/prepare; for shared memory, which threads write which regions and why none overlap. Unsafe used only to alias input and output → remove it.


Porting

CUDA C++ / numba-cuda → cuda-oxide

Sourcecuda-oxide
__global__ void k / @cuda.jit#[kernel] pub fn k in #[cuda_module]
const float* in&[f32]
float* outmut out: DisjointSlice<f32>
int nusually drop; slices carry length
blockIdx.x*blockDim.x+threadIdx.x / cuda.grid(1)thread::index_1d() + .get()
if (i < n)if let Some(o) = out.get_mut(idx)
cudaMalloc+memcpy / cuda.to_deviceDeviceBuffer::from_host / zeroed
k<<<b, t, shm>>> / k[b, t]prepare_k(LaunchConfig1D::new(b, t, shm)) + k(&stream, &p, ..)
D2H + synchronize / copy_to_hostto_host_vec(&stream)
cudaFree / error codesDrop / Result
cuda.shared.array, per-dtype JITunsafe shared memory; static types (generic kernels Inferred)

Triton / CUDA Tile C++/Python → cutile-rs

Tritoncutile-rs
@triton.jit#[cutile::entry()] in #[cutile::module]
BLOCK_SIZE: tl.constexprconst B: i32 on output shape
program_id, offsets, maskimplicit per tile / handled by load + partition
tl.load(ptr+offs, mask)load_tile_like(x, z)
tl.store(out+offs, v, mask)z.store(v)
grid lambda cdiv(n, BLOCK).partition([BLOCK])
eager launch, raw pointerslazy until sync_on; ownership checks

Simple elementwise SIMT kernels with no shared memory can be "upgraded" to Tile: mention it.

Checklist: classify each array (read / write / read-write at same index) → map writes to exclusive outputs → drop guards types now handle (keep them for shorter inputs) → replace geometry with contract+prepare or partition → replace alloc/copy/free → redesign shared memory, scatter, reductions → verify against a CPU reference with tolerance → benchmark after warm-up + sync → list Inferred identifiers.


Troubleshooting

Ask for cargo oxide doctor output (oxide) plus: uname -s, nvidia-smi --query-gpu=name,compute_cap --format=csv,noheader, the CUDA Version in nvidia-smi (driver max), nvcc --version (toolkit), rustup toolchain list, rustc +stable -V, clang --version, and ldconfig -p | grep libclang.

SymptomCause → fix
Compute capability < 8.0 (T4 7.5, V100 7.0)Unsupported on both → cudarc / rust-cuda, or newer GPU
Not LinuxUnsupported; WSL2 untested
cutile fails on CUDA 12.x, or driver max < 13.3cutile needs CUDA 13.3 toolkit and driver; else use cuda-oxide
Container sees no GPU--gpus all / NVIDIA Container Toolkit; CUDA_VISIBLE_DEVICES
First cargo oxide run very slowBuilding the backend; normal, cached after
rustc_private errors / ICEs (oxide)Wrong nightly; install with the pin; check rustup override isn't shadowing the project toolchain
libclang not foundInstall clang + libclang dev package (Debian/Ubuntu clang libclang-dev); set LIBCLANG_PATH
cargo add cuda-oxide pulled the wrong crateRemove it; use NVlabs git + cargo oxide new
Valid Rust fails in #[kernel]Backend coverage incomplete: remove dyn, heap (Vec/Box/String), panics/formatting, recursion, iterator chains, std calls; file a minimal issue
Only unsafe launchers generatedAdd #[launch_contract]
prepare_* returns ErrThreads/block must match contract, be ≤ launch_bounds and device max (1024)
Unpartitioned &mut output error (Tile).partition([w]) every mutable tensor
Wrong tuple element / type (Tile)Launcher returns all args; select the output's position
Nothing happens / zero timings (Tile)Lazy until sync_on
Wrong values, no errorRagged partition, missing input guard, float tolerance; shrink N to 16 and compare with CPU
Slow first Tile launchJIT per specialization; changing width/static shapes recompiles

Minimal repro: start from the template, change one thing at a time, shrink N. Report GPU + compute capability, driver, toolkit, rustc -Vv, doctor output, full error.


Ecosystem and status

Rust already runs much of the AI systems layer; NVIDIA uses it in the Nova driver, Dynamo's core, and NVTX bindings. CUDA Rust fills the missing piece: the kernel itself. Non-NVIDIA project descriptions reflect early-to-mid 2026; the cuda-oxide book's ecosystem appendix is authoritative.

ProjectWhatPick when
cutile-rsTile frontend, JIT, NVIDIADefault for new NVIDIA-only Rust kernels on stable
cuda-oxideSIMT codegen backend, NVIDIANeed thread-level control; accept nightly/alpha
cudarcSafe host bindings: driver, cuBLAS, cuDNN, NVRTC (kernels usually C via NVRTC)Host-side libraries, older GPUs, stability; complements CUDA Rust
rust-cudaCommunity Rust → NVIDIA via NVVM (NVIDIA collaborating)Existing projects, broader GPU generations
Rust-GPURust → SPIR-VVulkan, cross-vendor
CubeCLEmbedded GPU compute language (Burn), multi-backend JITPortability across CUDA/ROCm/WGPU

Portability needed → CubeCL / Rust-GPU. Pre-Ampere → cudarc / rust-cuda. Production today → cudarc + C++ kernels, evaluating cutile-rs in parallel. Buffer interop between these and CUDA Rust is undocumented; flag it.

Roadmap signals: NVIDIA is growing CUDA Rust into 2027+. Stated goals are removing the nightly pin, safe SIMT shared memory, and cross-language interop.

Resources (don't fabricate other URLs): github.com/NVlabs/cuda-oxide and the cuda-oxide book; the cutile-rs repo, cuTile Rust docs, and cutile on crates.io; GitHub Discussions on both; the cuda-oxide Discord; the paper Fearless Concurrency on the GPU; NVIDIA Technical Blog, "Introducing CUDA Rust: Two Tracks for Writing GPU Kernels" (Sep 8, 2026).

Comments & Discussion

Scroll to load comments...

Tags

rust
gpu
cuda
rust-gpu
coding-assistant
coding-agent
kernel-development
ptx
simt
tile-ir
cuda-oxide
cutile
cuda-rust
systems-programming
performance-optimization
code-review

Share

Chat

Chat
Related Links
Tokenization

This item is not available for tokenization.

Loading recommendations...

Yuki

Your Marketplace Companion

Prompt

Hey, I'm Yuki 👋

Ask me about specific products, customer support, or anything about the Swarms Marketplace.