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] 做了三件事:

  1. 重命名函数到保留的 cuda_oxide_kernel_<hash>_<name> 命名空间,以便编译器的收集器可以将其识别为设备入口点。 确切的前缀由工作区内部的 reserved-oxide-symbols crate 控制; <hash> 后缀使命名空间对用户代码不可猜测。

  2. 添加 #[no_mangle] 以在生成的 PTX 中保留符号名称。

  3. 生成一个标记结构体,实现 CudaKernel(或针对泛型 kernel 的 GenericCudaKernel),以便主机启动代码可以在编译时查找 正确的 PTX 入口点。

在生成的 PTX 中,kernel 成为一个 .entry 指令——GPU 版本的 main

.entry vecadd(.param .u64 a, .param .u64 a_len, ...) { ... }

参数约束#

Kernel 参数通过参数标量化(详见 内存与数据移动章节)跨越主机/设备 ABI 边界。 关键规则:

  • 切片&[T]DisjointSlice<T>)变为指针 + 长度对。

  • 标量u32f32 等)直接传递。

  • 结构体和闭包按值传递作为单个 .param byval 传递。逐字段展开 仍然适用于内部的设备到设备调用,但 kernel 边界本身接收 整个聚合体作为一个值,以匹配主机启动器推送的单数据包槽位。

  • 不允许堆分配类型VecStringBox)——alloc crate 可以通过编译器,但目前没有配置 #[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] 属性在三种自动发现不充分的特定场景下需要:

场景

为什么需要 #[device]

独立设备库

Crate 中没有 #[kernel],因此收集器没有入口点可供遍历

跨 crate 设备函数

函数位于与 kernel 不同的 crate 中

设备 FFI

函数暴露为 #[device] extern "C" 以便通过 LTOIR 链接 CUDA C++

use cuda_device::device;

#[device]
pub fn magnitude(x: f32, y: f32) -> f32 {
    (x * x + y * y).sqrt()
}

#[kernel] vs #[device]#

特性

#[kernel]

#[device]

自动发现

PTX 指令

.entry

.func

.func(或内联)

可从主机启动

是,通过类型化模块

可以有返回值

否(必须是 ()

可从设备代码调用

需要注解

总是需要

仅独立/跨 crate/FFI 时需要

永远不需要

哪些 Rust 特性可在 GPU 上使用#

cuda-oxide 通过 rustc 编译标准 Rust——它不是子集语言。 但 GPU 代码运行在 no_std 环境中,且目前没有配置设备端堆分配器, 因此某些 Rust 特性当前不可用。以下是当前的支持矩阵:

支持的#

特性

说明

基本类型(u8..u64f32f64bool

完全支持

结构体和元组

在 ABI 边界分解

枚举(Option<T>Result<T,E>、自定义)

包括 match

match / if / if let

多路分支

for 循环和 while 循环

基于范围和迭代器的

迭代器(.iter().enumerate()

通过 MIR 脱糖

breakcontinue

在循环内部

数组([T; N]

读取、写入、索引

切片(&[T]

只读;可变写入通过 DisjointSlice

闭包(在设备代码内部)

正常 Rust 语义

泛型函数

每个调用点单态化

unsafe 块和裸指针

用于高级模式

不支持的#

特性

原因

替代方案

StringVecBox

需要堆分配器(目前没有设备端 #[global_allocator]

使用固定大小数组或切片

format!println!

需要格式化机制 + I/O

使用 gpu_printf!

std I/O、网络、文件系统

GPU 上没有 OS

通过缓冲区通信

trait 对象(dyn Trait

需要虚表派发

使用泛型(单态化)

带消息的 panic!

格式化 + 分配

使用 gpu_assert!debug::trap()

小技巧

如果你不小心使用了不支持的特性,编译器会产生一个明确的错误: "CUDA-OXIDE: FORBIDDEN CRATE IN DEVICE CODE",并列出允许的 crate (corealloccuda_device 和你的本地 crate)。

#[launch_bounds] -- occupancy 提示#

#[launch_bounds] 属性告诉编译器你打算每个 block 启动多少个线程。 这让 PTX 汇编器能做出更好的寄存器分配决策,并可以提高 occupancy:

#[kernel]
#[launch_bounds(256, 2)]
pub fn optimized_kernel(mut out: DisjointSlice<f32>) {
    // ...
}

参数

必需

PTX 指令

描述

max_threads

.maxntid

每个 block 的最大线程数

min_blocks

.minnctapersm

每个 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 编译:

  1. 扫描所有编译单元中位于保留的 cuda_oxide_kernel_<hash>_ 命名空间中的函数(由 #[kernel] 生成)。

  2. 对于每个 kernel,遍历调用图并收集所有传递可达的函数。

  3. 根据允许的 crate 列表过滤每个被调用者:

Crate

允许

原因

你的本地 crate

你的 kernel 和辅助代码

cuda_device

GPU 内建函数(线程、warp、共享内存)

core

no_std Rust 核心库

std

需要 GPU 上不可用的 OS 设施

alloc

允许

能通过收集器,但目前没有配置设备端分配器。当前会产生链接时错误。

如果收集器遇到对禁止 crate 的调用,它会报告编译时错误, 而不是生成有问题的 PTX。

gpu-programming/images/collector-traversal.svg

设备代码收集器:从 #[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 故障。