2026 年 9 月,NVIDIA 宣布将在 Rust 中采用原生 GPU 编程。CUDA C++ 和 CUDA Python 是成熟的企业级工具链,NVIDIA 将不断发展壮大并使 CUDA Rust 走向成熟,直至 2027 年及以后
AI 的系统层涵盖推理引擎,为基础设施、驱动和智能体运行时提供服务,并且随着模型和技术的变化,该层会不断变化。越来越多的代码使用 Rust 编写,它可以在编译时捕获所有类别的错误,而不会降低性能。
出于同样的原因,NVIDIA 也参与了这一转变。Nova Linux 驱动程序使用 Rust 编写。NVIDIA Dynamo 基于 Rust 核心构建。NVTX 具有 Rust 绑定。
GPU 内核是个例外。您可以从 Rust 启动内核,但内核本身通常必须使用另一种语言编写。
NVIDIA CUDA Rust 弥补了这一差距。GPU 内核可以使用 Rust 编写,并以原生方式编译为 PTX,而不是使用其他任何地方的代码包装器。
使用 Rust 有两个音轨,与 CUDA 本身的两个音轨相匹配。SIMT 是您已使用 CUDA C++ 或 numba-cuda 编写的模型。您可以指明某个线程的作用,然后启动数千个线程。Tile 是较新的编程模型,也可用于 C++ 和 Python。所有这些前端均可让您说出每块数据的作用,而 Tile IR 编译器则负责完成其余工作。
当您选择一个来构建时,首先要找到 Tile。编译器决定图块如何映射到每个架构上,因此源文件不会对特定架构的选择进行编码,当您需要控制或想自行管理内存和线程时,可以使用 SIMT。
您使用的是哪种语言,另一个问题是您使用的是哪种型号。使用最适合您现有堆栈的 CUDA 曝光。以下两个项目适用于 Rust 堆栈。我们计划支持多语言互操作,因此选择不会将您排除在其他语言之外。
以下是每个轨道上的相同内核,用于对 1024 个浮点运算执行元素加法。两者都是完整的程序,都在运行,并且都打印同一行,因此您可以并排阅读它们,看看有什么变化。
SIMT 追踪:cuda-oxide
cuda-oxide 是一个自定义 rustc 代码生成后端。它拦截编译,通过 Rust MIR、社区 Pliron IR 框架和 LLVM IR 将 #[kernel] 函数路由到 PTX,并将所有其他内容交给标准后端。Pliron 上的 GPU 方言是我们的。这些方言和每个转换都保留在 Rust 中,直到标准的 LLVM 后端接管。
您需要 Linux、具有计算能力 8.0 或更高版本的 GPU、CUDA 工具包 ( 12.x 或更高版本) 、包含 libclang 标头的 Clang 以及固定的 Nightly 工具链。cargo oxide doctor 会检查所有内容,包括可选的系统 LLVM。安装驱动构建的 Cargo 子命令 cargo-oxide:
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
cargo oxide run
第一个 cargo oxide run 构建了代码生成后端,预计这需要一段时间。稍后运行重用缓存。
它会打印 PASSED: all 1024 elements correct。这就是 cargo oxide new 编写的整个程序,并在此处添加了注释:
use cuda_device::{kernel, launch_bounds, launch_contract, thread, DisjointSlice};
use cuda_host::cuda_module;
use cuda_core::{CudaContext, DeviceBuffer, LaunchConfig1D};
// === DEVICE CODE - everything in here is compiled to PTX ===
// The macro also generates the host-side API used further down:
// `load`, `prepare_vecadd`, and the safe `vecadd` launch method.
#[cuda_module]
mod kernels {
use super::*;
#[kernel] // GPU entry point
#[launch_bounds(256)] // max threads per block; lets the compiler budget registers
#[launch_contract(domain = 1, block = (256, 1, 1))] // indexes in 1-D, 256-thread blocks
pub fn vecadd(a: &[f32], b: &[f32], mut c: DisjointSlice<f32>) {
let idx = thread::index_1d();
let idx_raw = idx.get(); // the plain usize, for reading the inputs
if let Some(c_elem) = c.get_mut(idx) {
*c_elem = a[idx_raw] + b[idx_raw];
}
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
// === HOST SETUP - device, stream, and buffers ===
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)?;
// === LOAD, PREPARE, LAUNCH ===
// SAFETY: this package owns the embedded device bundle produced for the
// kernels module above.
let module = unsafe { kernels::load(&ctx)? };
// 4 blocks of 256 threads, 0 bytes of dynamic shared memory. `prepare_vecadd`
// checks that against the contract above and against the live device limits.
// The safe `vecadd` below takes that token where a raw config would go.
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)?;
// === READ BACK AND VERIFY ===
// Copies down and synchronizes, so the launch has finished by the time
// `c_host` can be read.
let c_host = c_dev.to_host_vec(&stream)?;
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 {} elements correct", N);
} else {
eprintln!("FAILED: {} errors", errors);
std::process::exit(1);
}
Ok(())
}
主机和设备代码位于一个文件中,只需一条命令即可构建,无需单独的内核包。
首先阅读核函数签名,因为它承载了整个安全参数。a 和 b 是普通的共享切片,可被每个线程读取。c 是一种 DisjointSlice<f32> 类型,可将每个线程对其自身元素的独占访问权限赋予其他任何内容。它之所以存在,是因为 &mut [f32] 对作业来说是错误的形状。每个线程都需要相同的 &mut,但 Rust 正确地拒绝了这一点。DisjointSlice 将可变借用的部分拆分为每个线程部分。
thread::index_1d() 返回索引类型而非裸整数,且 c.get_mut(idx) 仅接受该类型。返回的是 Option,因此越界情况是您要处理的分支,而不是稍后发现的内存错误。
系统会检查启动,而非信任启动。#[launch_contract] 声明此内核会在一个维度中索引 256 线程块。prepare_vecadd 根据该声明和实时设备限制验证您的 LaunchConfig1D,并提供安全 vecadd 方法所需的证明。无合约的核函数仅会公开原始的不安全启动方法,因为裸 LaunchConfig 不会透露要启动的核函数。
The Tile track:cutile-rs
cutile-rs 的效果要高出一层。您可以在图块而不是标量上执行计算。每个图块块作为单个逻辑线程在一个子张量的数据上运行一次核函数体,然后编译器决定返回它的实际 GPU 线程数。#[cutile::module] 宏会将内核的 AST 嵌入主机二进制文件中,并在首次需要内核时通过 CUDA Tile IR ( NVIDIA Tile – Level Compiler IR) 进行 JIT 编译。
要求比 SIMT 轨道更轻。您需要具有计算能力 8.0 或更高版本的 GPU、CUDA 13.3、稳定的 Rust 1.89 或更高版本以及 Linux,但没有夜间工具链,也没有您自己的 LLVM。
cutile 已发布,因此无需克隆任何内容:
cargo new vecadd_demo
cd vecadd_demo
cargo add cutile
这是针对图块编写的相同元素加法。将其粘贴到 src/main.rs 和 cargo run 中:
use cutile::prelude::*;
// The macro captures this module's AST into the host binary. The kernel is
// JIT-compiled through CUDA Tile IR the first time it is actually launched.
#[cutile::module]
mod kernel {
use cutile::core::*;
#[cutile::entry()]
fn add<const B: i32>(
// B is the tile width, a static dimension. A different B produces a
// different specialization.
z: &mut Tensor<f32, { [B] }>, // exclusive output, one sub-tensor of B elements
x: &Tensor<f32, { [-1] }>, // shared input; -1 is a dynamic dimension, resolved at launch
y: &Tensor<f32, { [-1] }>,
) {
// This body runs once per mut sub-tensor, as a single logical thread.
// Tile kernels load tiles, not scalars, from x and y.
let tx = load_tile_like(x, z); // the slice of x lining up with this sub-tensor of z
let ty = load_tile_like(y, z);
z.store(tx + ty); // elementwise across the whole tile
}
}
fn main() -> Result<(), Error> {
let device = Device::new(0)?;
let stream = device.new_stream()?;
// These are lazy. Nothing has touched the GPU yet.
let x = api::ones::<f32>(&[1024]);
let y = api::ones::<f32>(&[1024]);
// Partitioning does three things at once: gives each tile exclusive
// ownership of its own 128-element chunk, fixes the grid at 1024/128 = 8
// tiles, and supplies B.
let z = api::zeros::<f32>(&[1024]).partition([128]);
let c: Vec<f32> = kernel::add(z, x, y) // takes ownership of all three tensors
.first() // ...and returns them; pick the output back out
.unpartition() // drop the host-side partition wrapper; no data moves
.to_host_vec() // record the copy back
.sync_on(&stream)?; // and only now does any of it run
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");
}
Ok(())
}
PASSED: all 1024 elements correct
在 Rust 稳定版 中,平铺轨道得到相同的答案,并且其签名发出相同的安全参数。这次没有 DisjointSlice。主机分区仅适用于可变张量,它会为每个图块传递一个其他图块无法重叠的可写子张量。这种排他性正是 &mut 所保证的。
输入形状中的 -1 是一个 sentinel 而非大小。该维度会在启动时读取张量,因此无需重新编译即可改变形状。
主机上有趣的行是 .partition([128]),它同时执行三项任务。它让排他性成为现实。每个图块都有自己的 128 个元素块,没有其他图块可以触碰它。它修复了启动几何图形,因为 1024 除以 128 是由 8 个图块组成的网格。
网格从分区开始计算,而不是单独计算,并根据核函数的索引进行检查。它还提供 B,由于启动程序会读取分区的图块宽度,因此调用站点永远不会写入此内容。因此,必须先对 &mut 输出进行分区,然后才能进行传递。
然后看看启动会带来什么结果。您在主机上调用的 add 是宏生成的启动器,而不是上述设备函数。它获取所有三个张量的所有权,并在 GPU 完成后将其作为元组返回。这就是 .first() 的作用,从中提取输出。
.sync_on(&stream) 之前什么都不会运行。之前的所有内容都是延迟描述,记录而不是提交。其中包括 ones、zeros、内核调用,甚至复制回主机。整个程序是一个具有单个同步点的链。
编译器捕获的内容
这两个内核都对内存作出了相同的声明。它们的输入是共享的,并且输出内容仅属于一个 Writer。它们的不同之处仅在于其制作级别,以及是否需要专门构建的类型才能制作。
这一点很重要,因为数千个线程会按无法保证的顺序到达相同的缓冲区。当两个用户找到同一个地址,一个正在写入时,顺序决定结果。这些错误很少按需重现,而且会在生产失败前通过测试。
将 SIMT 内核的输出缓冲区作为其自身的输入之一传递不会进行编译,无论该内核是否真的会发生竞争:
module.vecadd(&stream, &prepared, &c_dev, &b_dev, &mut c_dev)?;
error[E0502]: cannot borrow `c_dev` as mutable because it is also borrowed as immutable
图块侧的相同锯齿也不会编译:
let z = api::zeros::<f32>(&[1024]);
kernel::add(z.partition([128]), z, y)
error[E0382]: use of moved value: `z`
这两个示例都会在编译时发现典型的锯齿错误,并在不同的位置绘制线条。cuda-oxide 会检查每次启动调用。cutile-rs 的所有权遵循跨启动边界的张量,这是两种声明中更强的。
Tile 不会为您提供共享内存或线程索引,以免出错,因为编译器拥有两者。图块是单个逻辑线程,因此没有线程可供竞赛。这就是通过构建使其安全的原因,这也是你用来交换的东西。SIMT 会保持这种控制,而现在那里的共享内存需要 unsafe。共享内存是快速 SIMT 内核的基石,因此确保该路径安全的关键在于主动工作。
项目现状
这两个项目都处于早期阶段,尚未就绪生产。cuda-oxide 是早期的 Alpha。cutile-rs 发布在 crates.io 上,并且已经在 NVIDIA 外部的 HuggingFace 的 Grout 推理引擎和 mistral.rs 中使用。覆盖率不完整,API 会移动。如果你发现了粗糙的边缘,我们希望了解它们。
货物和板条箱让人们期望轻松上手。GPU 编程历来恰恰相反,而缩短这一距离是我们的工作。SIMT 轨道仍然需要固定的夜间工具链,这正是我们不想再向您提出的问题。
对 GPU 的 Rust 并不新鲜。在这个领域中,有一些出色的工作比我们的工作更早,并且还在继续。cuda-oxide 书中的生态系统附录映射了我们相对于 Rust-GPU、rust-cuda、CubeCL 和其他组件的位置,随着这两个项目的成熟,我们一直在与 rust-cuda 维护人员合作。
新功能是我们为其打造的工程,以及对其发展方向的清晰理解。
您现在可以做什么
- 运行 SIMT 示例。 cuda-oxide 中的
cargo oxide new和cargo oxide run。 - 运行图块示例。克隆cutile-rs,然后是
cargo run -p cutile-examples --example hello_world。 - 阅读文档。 cuda-oxide 手册和 cuTile Rust 文档。
- 阅读白皮书。 GPU 上的“无畏并发 ( Fearless Concurrency) ”。
- 文件问题。请告诉我们cuda-oxide或cutile-rs上有什么损坏和缺少了什么。
- 加入对话。在 GitHub 或 cuda-oxide Discord上讨论这两个代码库。
- 欢迎参加讲座。 Melih Elibol 将在 RustConf 2026 上展示“GPU 上的无畏并发”,9 月 8 日至 11 日于蒙特利尔。NVIDIA 也会有其他员工参加,如果您在那里,欢迎来找我们!
您可以修改现有内容,并与我们一起进行改进。时间还早,是开放的,您现在构建的内容将塑造未来。
Rust 社区
在我们提升原生 Rust GPU 编程的过程中,NVIDIA 很高兴能与 Rust 社区携手共进。Rust-cuda、rust-gpu 和 cudarc 等项目开创了 GPU 和 Rust 的结合,而包括 VectorWare 团队在内的相关人员在与 Rust 社区合作时,继续塑造我们对自身工作的看法。