启动 Kernel#
编写 kernel 只是故事的一半。主机必须加载设备代码、
配置启动 grid、整理参数,并将工作分发到 GPU。
cuda-oxide 的主要启动路径是 #[cuda_module]:它将生成的设备产物嵌入
主机二进制文件,并生成类型化的启动方法。底层的 load_kernel_module
和 cuda_launch! API 在需要显式 sideload 或自定义启动代码时
仍然可用。
参见
CUDA 编程指南 -- 执行配置
关于 <<<grid, block, smem, stream>>> 语义的权威参考。
启动生命周期#
每次 kernel 启动遵循相同的顺序:
初始化 CUDA context -- 绑定到 GPU 设备。
加载设备模块 -- 通常来自嵌入的产物包。
查找 kernel 函数 -- 通过其 PTX 入口点名称。
配置 grid -- block 维度、grid 维度、共享内存。
启动 -- 将 kernel 排队到 stream 上。
同步 -- 等待结果(显式或隐式)。
Kernel 启动生命周期。主机初始化 context,加载设备模块, 配置 grid,并通过类型化方法启动。GPU 调度器将 block 分发到 SM。#
在实践中,#[cuda_module] 在生成的 Rust API 背后处理步骤 2--5。
你通常只需与 context 创建、kernels::load 和类型化方法调用交互。
#[cuda_module] -- 类型化启动#
将 kernel 包装在内联的 #[cuda_module] 模块中,以生成类型化加载器和每个
#[kernel] 对应一个方法。该方法在 CUDA 意义上是"同步的":你提供
一个特定的 stream,kernel 立即被排队,尽管 GPU 执行仍然与
主机重叠,直到你进行同步。
use cuda_device::{cuda_module, kernel, thread, DisjointSlice};
use cuda_core::{CudaContext, DeviceBuffer, LaunchConfig};
#[cuda_module]
mod kernels {
use super::*;
#[kernel]
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) {
*c_elem = a[i] + b[i];
}
}
}
fn main() {
let ctx = CudaContext::new(0).unwrap();
let stream = ctx.default_stream();
let module = kernels::load(&ctx).unwrap();
let a = DeviceBuffer::from_host(&stream, &[1.0f32; 1024]).unwrap();
let b = DeviceBuffer::from_host(&stream, &[2.0f32; 1024]).unwrap();
let mut c = DeviceBuffer::<f32>::zeroed(&stream, 1024).unwrap();
module
.vecadd(&stream, LaunchConfig::for_num_elems(1024), &a, &b, &mut c)
.expect("Kernel launch failed");
let result = c.to_host_vec(&stream).unwrap();
assert_eq!(result[0], 3.0);
}
逐字段分解#
组成部分 |
描述 |
|---|---|
|
生成加载器和启动方法 |
|
加载嵌入的产物包 |
|
排队一次类型化 kernel 启动 |
|
Grid/block 维度和共享内存 |
参数映射#
生成的方法将 kernel 参数映射到主机值:
Kernel 参数 |
主机参数 |
GPU ABI |
|---|---|---|
|
|
指针 + 长度 |
|
|
指针 + 长度 |
|
|
指针 + 长度 |
标量/裸指针 |
相同值 |
直接传递值 |
返回值#
类型化启动方法返回 Result<(), DriverError>。Ok 表示 kernel
成功排队——而不是它已经完成。要检查运行时错误
(例如越界陷阱),之后需要同步 stream 或 context。
cuda_launch! -- 底层启动#
cuda_launch! 是旧代码和有意显式加载特定模块的示例所使用的
显式启动 API。当你需要手动选择侧车 PTX/cubin/LTOIR 产物时,
它仍然有用。
use cuda_host::{cuda_launch, load_kernel_module};
let module = load_kernel_module(&ctx, "vecadd").unwrap();
cuda_launch! {
kernel: vecadd,
stream: stream,
module: module,
config: LaunchConfig::for_num_elems(1024),
args: [slice(a), slice(b), slice_mut(c)]
}
.expect("Kernel launch failed");
args 中的包装器生成与生成的 #[cuda_module] 方法相同的主机数据包:
slice(...) 和 slice_mut(...) 推送 (ptr, len) 对,
标量参数直接推送其值,闭包或按值结构体作为单个 byval 值推送
(kernel 边界将其接收为一个 .param,而不是按字段展开的参数)。
产物策略#
#[cuda_module] 是启动界面特性,而不是目标选择特性。它加载编译器
放置在主机二进制文件中的嵌入负载。PTX vs LTOIR、cubin vs fatbin、
单架构 vs 多架构等决策位于编译器和产物/运行时加载器层中。
将这些策略分离使得生成的 Rust 启动方法在负载格式演变时保持稳定。
LaunchConfig#
LaunchConfig 指定 grid 形状:
use cuda_core::LaunchConfig;
let config = LaunchConfig {
grid_dim: (num_blocks, 1, 1),
block_dim: (256, 1, 1),
shared_mem_bytes: 0,
};
字段 |
类型 |
描述 |
|---|---|---|
|
|
x、y、z 维度上的 block 数 |
|
|
x、y、z 维度上每个 block 的线程数 |
|
|
每个 block 的动态共享内存 |
for_num_elems 辅助函数#
对于 1D 数据并行 kernel,常见模式是每个线程处理一个元素:
let config = LaunchConfig::for_num_elems(N as u32);
这使用每个 block 256 个线程,并通过向上取整除法计算 grid 大小:
grid_x = (N + 255) / 256。对于大多数逐元素操作这是正确的默认值。
2D 和 3D 配置#
对于矩阵操作,使用 2D block 和 grid 维度:
let config = LaunchConfig {
grid_dim: ((cols + 15) / 16, (rows + 15) / 16, 1),
block_dim: (16, 16, 1),
shared_mem_bytes: 0,
};
在 kernel 内部,结合 threadIdx_x() / blockIdx_x() 与它们的 _y()
对应变体来计算行和列索引。
选择 block 大小#
Block 大小是最重要的调优参数(详见 执行模型 章节)。 快速指南:
256 是大多数 kernel 的安全默认值。
2 的幂次(128、256、512)与 warp 边界对齐。
使用
#[launch_bounds]向编译器提示你的预期 block 大小。
类型化异步启动#
启用 cuda-host 异步特性后,#[cuda_module] 也生成
借用和拥有的异步方法。这些方法返回惰性的 DeviceOperation 值,
而不是立即排队。启动时不指定 stream——调度策略在
执行操作时选择一个:
use cuda_async::device_context::init_device_contexts;
use cuda_async::device_operation::DeviceOperation;
init_device_contexts(0, 1)?;
let module = kernels::load_async(0)?;
let op = module.vecadd_async(
LaunchConfig::for_num_elems(1024),
&a_dev,
&b_dev,
&mut c_dev,
)?;
// 执行并等待
op.sync()?;
当操作必须作为 'static future 被 spawn 或存储时,使用拥有形式:
use std::future::IntoFuture;
let op = module.vecadd_async_owned(
LaunchConfig::for_num_elems(1024),
a_dev,
b_dev,
c_dev,
)?;
let (a_dev, b_dev, c_dev) = tokio::spawn(op.into_future()).await??;
异步缓冲区生命周期#
异步启动是惰性的,因此指针生命周期很重要:
裸指针形式:
从 CUdeviceptr 构建操作
释放缓冲区
稍后运行操作 → 过期指针
借用类型化形式:
从 &DeviceBuffer 构建操作
Rust 保持缓冲区被借用直到操作完成
拥有类型化形式:
将 DeviceBox 移动到操作中
生成的任务拥有分配直到完成
cuda_launch_async! 作为底层迁移 API 仍然保留,但对于新代码,
优先使用生成的借用或拥有方法。裸指针异步启动仅在调用者能证明
指向的分配比惰性操作存活更久时才是正确的。
.sync() vs .await#
方法 |
做什么 |
|---|---|
|
使用默认调度策略执行,阻塞当前线程直到完成 |
|
执行并让出当前异步任务(需要 Tokio 运行时) |
组合 GPU 工作#
DeviceOperation 支持函数式组合。使用 and_then 链接操作,
使用 zip! 并行运行独立的工作:
use cuda_async::zip;
let forward_pass = layer1_op
.and_then(|output1| layer2_op(output1))
.and_then(|output2| layer3_op(output2));
// 并发运行两个独立操作
let combined = zip!(branch_a, branch_b);
let (result_a, result_b) = combined.sync()?;
链中的每个操作仅在其执行时被调度到 stream 上。
and_then 组合器将一个操作的输出作为下一个操作的输入传递,
形成一个惰性计算图。
参见
异步 GPU 编程
部分深入涵盖了 DeviceOperation、调度策略和 stream 管理。
Cluster 启动#
线程 Block Cluster(Hopper 及更新架构)允许 block 通过
分布式共享内存(DSMEM)在共享内存之外进行协作。要使用 cluster 启动,
将 #[cluster_launch] 添加到 kernel 并在启动中包含 cluster_dim:
use cuda_device::{kernel, cluster, cluster_launch, DisjointSlice};
#[kernel]
#[cluster_launch(4, 1, 1)]
pub fn cluster_kernel(mut out: DisjointSlice<u32>) {
let rank = cluster::block_rank();
// Block 0-3 可以通过 DSMEM 进行通信
}
在主机上,启动使用 launch_kernel_ex(扩展启动 API)配合 cluster 维度。
cuda_launch! 通过 cluster_dim 字段支持这一点:
cuda_launch! {
kernel: cluster_kernel,
stream: stream,
module: module,
config: config,
cluster_dim: (4, 1, 1),
args: [slice_mut(out_dev)]
}
.expect("Cluster launch failed");
小技巧
Cluster 启动需要 Hopper(sm_90) 或更新架构。最大 cluster 大小
通常为 16 个 block。使用 cargo oxide build --arch sm_90 来针对 Hopper 构建。
常见启动错误#
错误 |
可能的原因 |
修复方法 |
|---|---|---|
|
Grid 或 block 维度为零或超出限制 |
检查 |
|
PTX 入口点名称不匹配 |
验证 |
|
共享内存过大或每个 block 使用的寄存器过多 |
减少 |
|
Kernel 触发了陷阱(panic、assert 失败、越界) |
使用 |
|
PTX 为错误架构编译 |
使用与你的 GPU 匹配的 |
参见
错误处理与调试章节 涵盖了如何详细诊断和修复 kernel 故障。