Kernel 和设备函数#
Kernel 是在 GPU 上运行的函数——是主机在数千个线程上启动的入口点。 设备函数是在 GPU 上运行的辅助函数,但只能从另一个设备函数或 kernel 调用,不能从主机调用。本章涵盖两者,以及设备代码中 支持(和不支持)的 Rust 模式。
参见
CUDA 编程指南 -- Kernel 关于 kernel 和设备函数的权威 CUDA C++ 参考。
#[kernel] -- GPU 入口点#
用 #[kernel] 注解一个函数告诉 cuda-oxide 将其编译为 GPU
入口点。该函数必须返回 ()——kernel 通过写入输出缓冲区来传递结果,
而不是通过返回值。
use cuda_device::{kernel, thread, DisjointSlice};
#[kernel]
pub fn vecadd(a: &[f32], b: &[f32], mut c: DisjointSlice<f32>) {
let idx = thread::index_1d();
if let Some(c_elem) = c.get_mut(idx) {
*c_elem = a[idx.get()] + b[idx.get()];
}
}
在底层,#[kernel] 做了三件事:
重命名函数到保留的
cuda_oxide_kernel_<hash>_<name>命名空间,以便编译器的收集器可以将其识别为设备入口点。 确切的前缀由工作区内部的reserved-oxide-symbolscrate 控制;<hash>后缀使命名空间对用户代码不可猜测。添加
#[no_mangle]以在生成的 PTX 中保留符号名称。生成一个标记结构体,实现
CudaKernel(或针对泛型 kernel 的GenericCudaKernel),以便主机启动代码可以在编译时查找 正确的 PTX 入口点。
在生成的 PTX 中,kernel 成为一个 .entry 指令——GPU 版本的
main:
.entry vecadd(.param .u64 a, .param .u64 a_len, ...) { ... }
参数约束#
Kernel 参数通过参数标量化(详见 内存与数据移动章节)跨越主机/设备 ABI 边界。 关键规则:
切片(
&[T]、DisjointSlice<T>)变为指针 + 长度对。标量(
u32、f32等)直接传递。结构体和闭包按值传递作为单个
.parambyval 传递。逐字段展开 仍然适用于内部的设备到设备调用,但 kernel 边界本身接收 整个聚合体作为一个值,以匹配主机启动器推送的单数据包槽位。不允许堆分配类型(
Vec、String、Box)——alloccrate 可以通过编译器,但目前没有配置#[global_allocator]。 即便有,设备端malloc也极其缓慢。
设备辅助函数#
并非所有 GPU 代码都需要写在 kernel 本身中。你可以将逻辑分解到辅助函数中, 编译器同样会为 GPU 编译这些函数。
自动发现的辅助函数#
最简单的方法:编写一个普通的 Rust 函数并从你的 kernel 中调用它。
编译器的收集器遍历每个 #[kernel] 入口点的调用图,
并自动为 GPU 编译每个可到达的函数——无需注解:
fn clamp(x: f32, lo: f32, hi: f32) -> f32 {
if x < lo { lo } else if x > hi { hi } else { x }
}
#[kernel]
pub fn apply_clamp(input: &[f32], mut out: DisjointSlice<f32>) {
let idx = thread::index_1d();
if let Some(out_elem) = out.get_mut(idx) {
*out_elem = clamp(input[idx.get()], 0.0, 1.0);
}
}
clamp 函数被编译为 PTX .func(设备函数),并且
通常被编译器内联,因此没有调用开销。
何时需要 #[device]#
#[device] 属性在三种自动发现不充分的特定场景下需要:
场景 |
为什么需要 |
|---|---|
独立设备库 |
Crate 中没有 |
跨 crate 设备函数 |
函数位于与 kernel 不同的 crate 中 |
设备 FFI |
函数暴露为 |
use cuda_device::device;
#[device]
pub fn magnitude(x: f32, y: f32) -> f32 {
(x * x + y * y).sqrt()
}
#[kernel] vs #[device]#
特性 |
|
|
自动发现 |
|---|---|---|---|
PTX 指令 |
|
|
|
可从主机启动 |
是,通过类型化模块 |
否 |
否 |
可以有返回值 |
否(必须是 |
是 |
是 |
可从设备代码调用 |
是 |
是 |
是 |
需要注解 |
总是需要 |
仅独立/跨 crate/FFI 时需要 |
永远不需要 |
哪些 Rust 特性可在 GPU 上使用#
cuda-oxide 通过 rustc 编译标准 Rust——它不是子集语言。
但 GPU 代码运行在 no_std 环境中,且目前没有配置设备端堆分配器,
因此某些 Rust 特性当前不可用。以下是当前的支持矩阵:
支持的#
特性 |
说明 |
|---|---|
基本类型( |
完全支持 |
结构体和元组 |
在 ABI 边界分解 |
枚举( |
包括 |
|
多路分支 |
|
基于范围和迭代器的 |
迭代器( |
通过 MIR 脱糖 |
|
在循环内部 |
数组( |
读取、写入、索引 |
切片( |
只读;可变写入通过 |
闭包(在设备代码内部) |
正常 Rust 语义 |
泛型函数 |
每个调用点单态化 |
|
用于高级模式 |
不支持的#
特性 |
原因 |
替代方案 |
|---|---|---|
|
需要堆分配器(目前没有设备端 |
使用固定大小数组或切片 |
|
需要格式化机制 + I/O |
使用 |
|
GPU 上没有 OS |
通过缓冲区通信 |
trait 对象( |
需要虚表派发 |
使用泛型(单态化) |
带消息的 |
格式化 + 分配 |
使用 |
小技巧
如果你不小心使用了不支持的特性,编译器会产生一个明确的错误:
"CUDA-OXIDE: FORBIDDEN CRATE IN DEVICE CODE",并列出允许的 crate
(core、alloc、cuda_device 和你的本地 crate)。
#[launch_bounds] -- occupancy 提示#
#[launch_bounds] 属性告诉编译器你打算每个 block 启动多少个线程。
这让 PTX 汇编器能做出更好的寄存器分配决策,并可以提高 occupancy:
#[kernel]
#[launch_bounds(256, 2)]
pub fn optimized_kernel(mut out: DisjointSlice<f32>) {
// ...
}
参数 |
必需 |
PTX 指令 |
描述 |
|---|---|---|---|
|
是 |
|
每个 block 的最大线程数 |
|
否 |
|
每个 SM 的最小并发 block 数 |
生成的 PTX 包含这些指令:
.entry optimized_kernel .maxntid 256, 1, 1 .minnctapersm 2 { ... }
小技巧
#[launch_bounds] 必须出现在 #[kernel] 之后:
#[kernel]
#[launch_bounds(256, 2)] // 正确
pub fn my_kernel(...) { }
收集器 -- 设备代码是如何被发现的#
当你使用 cargo oxide 构建时,rustc-codegen-cuda 后端运行一个
收集器遍,确定哪些函数需要为 GPU 编译:
扫描所有编译单元中位于保留的
cuda_oxide_kernel_<hash>_命名空间中的函数(由#[kernel]生成)。对于每个 kernel,遍历调用图并收集所有传递可达的函数。
根据允许的 crate 列表过滤每个被调用者:
Crate |
允许 |
原因 |
|---|---|---|
你的本地 crate |
是 |
你的 kernel 和辅助代码 |
|
是 |
GPU 内建函数(线程、warp、共享内存) |
|
是 |
|
|
否 |
需要 GPU 上不可用的 OS 设施 |
|
允许 |
能通过收集器,但目前没有配置设备端分配器。当前会产生链接时错误。 |
如果收集器遇到对禁止 crate 的调用,它会报告编译时错误, 而不是生成有问题的 PTX。
设备代码收集器:从 #[kernel] 入口点开始,编译器遍历调用图以发现 所有可达的设备函数,然后根据允许的 crate 列表(本地 crate、cuda_device、core) 过滤每个被调用者。输出是一个包含 .entry 和 .func 指令的 PTX 模块。#
no_std 和 panic 行为#
设备代码运行在隐式的 #![no_std] 环境中。你不需要自己添加
这个属性——编译器后端会处理。
Panic 行为: MIR 中所有展开路径都被视为不可达。如果 panic 在运行时
实际触发(例如数组边界检查失败),GPU 会执行陷阱指令,
这会导致主机收到 CUDA_ERROR_ILLEGAL_INSTRUCTION。这在语义上等价于
panic=abort,但不需要任何特殊编译器标志。
实践中这意味着:
unwrap()和expect()能工作,但在None/Err时会触发 GPU 陷阱。assert!和debug_assert!能工作,但失败时触发陷阱。panic!("message")不支持(格式化机制不可用)——使用gpu_assert!或debug::trap()替代。
参见
错误处理与调试章节
涵盖了 gpu_printf!、gpu_assert! 和 cargo oxide debug
用于诊断 kernel 故障。