2026年9月,NVIDIA 宣布将着力推进 Rust 原生 GPU 编程。CUDA C++ 和 CUDA Python 是成熟的企业级工具链,NVIDIA 将在 2027 年及以后持续发展和完善 CUDA Rust
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)数据做什么,其余交给 Tile IR 编译器。
在选择基于哪一个构建时,优先考虑 Tile。编译器决定瓦片如何映射到每种架构,因此你的源代码不会编码特定于架构的选择;当你需要那种控制权或想自己管理内存和线程时,再降到 SIMT。
选择哪种语言与选择哪种模型是不同的问题。使用最适合你现有技术栈的 CUDA 暴露方式。下面两个项目适用于你的技术栈是 Rust 的情况。我们计划支持跨语言互操作,因此这一选择不会把你锁定在其他选项之外。
下面是每条路径上的同一个内核,它对 1,024 个浮点数执行逐元素加法。两者都是完整程序,都能运行,都打印同一行,因此你可以并排阅读它们,看看有什么变化。
SIMT 路径:cuda-oxide
cuda-oxide 是一个自定义的 rustc
代码生成后端。它拦截编译过程,将 #[kernel]
函数经由 Rust MIR、社区 Pliron IR 框架和 LLVM IR 一路路由到 PTX,其余一切交给标准后端。Pliron 之上的 GPU 方言是我们自己的。方言和每一个变换都留在 Rust 中,直到标准 LLVM 后端接手。
你需要 Linux、计算能力 8.0 或更高的 GPU、CUDA 工具包(12.x 或更新)、带 libclang 头文件的 clang,以及固定版本的 nightly 工具链。cargo oxide doctor
会检查所有这些,包括可选的系统 LLVM。安装 cargo-oxide
,即驱动构建的 Cargo 子命令:
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(())
}
主机代码和设备代码位于同一个文件中,用一条命令即可构建,无需单独的内核 crate。
先读内核签名,因为它承载了整个安全性论证。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
对它所启动的内核没有任何说明。
Tile 路线:cutile-rs
cutile-rs 在更高一层上工作。你在 tile 上而不是标量上进行计算。每个 tile 块将内核主体作为单个逻辑线程在一个数据子张量上运行一次,而编译器决定由多少个真实 GPU 线程来支撑它。#[cutile::module]
宏将内核的 AST 嵌入主机二进制文件,并在内核首次需要时通过 CUDA Tile IR(NVIDIA 的 tile 级编译器 IR)对其进行 JIT 编译。
要求比 SIMT 路线更轻。你需要一块计算能力为 8.0 或更高的 GPU、CUDA 13.3、稳定版 Rust 1.89 或更新版本,以及 Linux,但不需要 nightly 工具链,也不需要你自己的 LLVM。
cutile
已发布,因此无需克隆:
cargo new vecadd_demo
cd vecadd_demo
cargo add cutile
以下是同样的逐元素加法,针对 tile 编写。将其粘贴到 src/main.rs
并运行 cargo run
:
use cutile::prelude::*;
// 该宏将此模块的 AST 捕获到宿主二进制文件中。内核在首次实际启动时
// 通过 CUDA Tile IR 进行 JIT 编译。
#[cutile::module]
mod kernel {
use cutile::core::*;
#[cutile::entry()]
fn add<const B: i32>(
// B 是 tile 宽度,一个静态维度。不同的 B 会产生
// 不同的特化版本。
z: &mut Tensor<f32, { [B] }>, // 独占输出,一个包含 B 个元素的子张量
x: &Tensor<f32, { [-1] }>, // 共享输入;-1 是动态维度,在启动时解析
y: &Tensor<f32, { [-1] }>,
) {
// 此函数体对每个 mut 子张量运行一次,作为单个逻辑线程。
// Tile 内核从 x 和 y 中加载 tile,而非标量。
let tx = load_tile_like(x, z); // x 中与 z 的这个子张量对齐的切片
let ty = load_tile_like(y, z);
z.store(tx + ty); // 在整个 tile 上逐元素操作
}
}
fn main() -> Result<(), Error> {
let device = Device::new(0)?;
let stream = device.new_stream()?;
// 这些是惰性的。尚未触及 GPU。
let x = api::ones::<f32>(&[1024]);
let y = api::ones::<f32>(&[1024]);
// 分区同时完成三件事:赋予每个 tile 对其自身 128 元素块的独占
// 所有权,将网格固定为 1024/128 = 8 个 tile,并提供 B。
let z = api::zeros::<f32>(&[1024]).partition([128]);
let c: Vec<f32> = kernel::add(z, x, y) // 取得全部三个张量的所有权
.first() // ...并返回它们;将输出重新取出
.unpartition() // 丢弃宿主侧的分区包装;不移动数据
.to_host_vec() // 记录回拷贝
.sync_on(&stream)?; // 只有到这时才会真正运行
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
Tile 轨道在稳定的 Rust 上得出相同的答案,其签名也做出了相同的安全性论证。这次没有 DisjointSlice。
在主机上进行分区仅对可变张量是必要的,它为每个 tile 块分配一个可写的子张量,其他 tile 块无法重叠。这种排他性正是 &mut 已经保证的。
输入形状中的 -1 是一个哨兵值而非尺寸。该维度在启动时从张量读取,因此形状可以变化而无需重新编译。
主机上有趣的一行是 .partition([128]),它同时完成三项工作。它使排他性成为现实。每个 tile 拥有其 128 元素的块,其他 tile 无法触及。它固定了启动几何结构,因为 1,024 除以 128 是 8 个 tile 的网格。
网格由分区决定,而不是单独计算并与内核的索引进行核对。它还提供了 B,在调用点从未写入,因为启动器从分区读取 tile 宽度。这就是为什么 &mut 输出在传递之前必须进行分区。
然后看看启动返回什么。你在主机上调用的 add 是宏生成的启动器,而不是上面的设备函数。它获取所有三个张量的所有权,并在 GPU 完成后将它们作为元组交还。这就是 .first() 的用途,从中取出输出。
直到 .sync_on(&stream) 之前什么都不会运行。它之前的一切都是惰性描述,记录而非提交。这包括 ones、zeros、内核调用,甚至复制回主机。整个程序是一个链,只有一个同步点。
编译器捕获什么
两个内核都对内存做出相同的声明。它们的输入是共享的,输出仅属于一个写入者。它们仅在做出声明的层级上不同,以及是否需要专门构建的类型来做出声明。
这很重要,因为数千个线程以不保证的顺序访问相同的缓冲区。当其中两个命中同一地址且其中一个正在写入时,顺序决定结果。这些错误很少按需重现,它们通过测试后才在生产中失败。
将 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
Tile 侧的相同别名同样无法编译:
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 不给你任何可能出错的共享内存或线程索引,因为编译器同时掌控这两者。一个 tile 块就是一个逻辑线程,因此没有线程可供你产生竞态。这正是它天生安全的原因,也正是你为此放弃的东西。SIMT 保留了那种控制权,而如今那里的共享内存需要 unsafe
。共享内存是快速 SIMT 内核的基石,因此让这条路径变得安全是一项正在进行的工作。
项目现状
两个项目都处于早期阶段,都尚未达到生产可用。cuda-oxide 处于早期 alpha 阶段。cutile-rs 走得更远,已发布到 crates.io,并且已在 NVIDIA 之外被使用,用于 HuggingFace 的 Grout 推理引擎以及 mistral.rs。覆盖范围尚不完整,API 也会变动。如果你发现粗糙之处,我们希望听到反馈。
Cargo 和 crates 设定了一种期望:上手很容易。GPU 编程历来恰恰相反,而缩小这一差距正是工作的一部分。SIMT 路线仍然需要固定版本的 nightly 工具链,而这正是我们希望不再向你索要的东西。
Rust 在 GPU 上并非新事物。这个领域有早于我们的优秀工作,并且与我们的工作并行持续发展。cuda-oxide 手册中的生态系统附录描绘了我们相对于 Rust-GPU、rust-cuda、CubeCL 等所处的位置,并且随着两个项目的成熟,我们一直在与 rust-cuda 的维护者合作。
新的是我们为此投入的工程力量,以及对它走向何方的清晰认识。
你今天可以做什么
运行 SIMT 示例。在 cuda-oxide 中执行 cargo oxide new
,然后执行 cargo oxide run
。运行 Tile 示例。克隆 cutile-rs,然后执行 cargo run -p cutile-examples --example hello_world
。阅读文档。cuda-oxide 手册和 cuTile Rust 文档。阅读论文。。GPU 上的无畏并发提交 issue。在 cuda-oxide 或 cutile-rs 上告诉我们哪里出了问题、缺少了什么。加入讨论。两个仓库的 GitHub Discussions,或 cuda-oxide Discord。来听演讲。Melih Elibol 将在 RustConf 2026 上演讲“GPU 上的无畏并发*”*,大会于 9 月 8 日至 11 日在蒙特利尔举行。NVIDIA 还会有其他员工参加,如果你也在场,欢迎来找我们!
摆弄一下这里已有的东西,然后来和我们一起做。它还很早期,它是开放的,而你现在构建的东西将塑造接下来的走向。
Rust 社区
NVIDIA 很高兴能与 Rust 社区携手共进,共同提升原生 Rust GPU 编程。rust-cuda、rust-gpu 和 cudarc 等项目开创了 GPU 与 Rust 的结合,而它们背后的人,包括 VectorWare 的团队,在我们与 Rust 社区一起构建的过程中,继续塑造着我们对自己工作的思考。
