启动 Kernel#

编写 kernel 只是故事的一半。主机必须加载设备代码、 配置启动 grid、整理参数,并将工作分发到 GPU。 cuda-oxide 的主要启动路径是 #[cuda_module]:它将生成的设备产物嵌入 主机二进制文件,并生成类型化的启动方法。底层的 load_kernel_modulecuda_launch! API 在需要显式 sideload 或自定义启动代码时 仍然可用。

参见

CUDA 编程指南 -- 执行配置 关于 <<<grid, block, smem, stream>>> 语义的权威参考。

启动生命周期#

每次 kernel 启动遵循相同的顺序:

  1. 初始化 CUDA context -- 绑定到 GPU 设备。

  2. 加载设备模块 -- 通常来自嵌入的产物包。

  3. 查找 kernel 函数 -- 通过其 PTX 入口点名称。

  4. 配置 grid -- block 维度、grid 维度、共享内存。

  5. 启动 -- 将 kernel 排队到 stream 上。

  6. 同步 -- 等待结果(显式或隐式)。

gpu-programming/images/launch-lifecycle.svg

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);
}

逐字段分解#

组成部分

描述

#[cuda_module]

生成加载器和启动方法

kernels::load(&ctx)

加载嵌入的产物包

module.vecadd(...)

排队一次类型化 kernel 启动

LaunchConfig

Grid/block 维度和共享内存

参数映射#

生成的方法将 kernel 参数映射到主机值:

Kernel 参数

主机参数

GPU ABI

&[T]

&DeviceBuffer<T>

指针 + 长度

&mut [T]

&mut DeviceBuffer<T>

指针 + 长度

DisjointSlice<T>

&mut DeviceBuffer<T>

指针 + 长度

标量/裸指针

相同值

直接传递值

返回值#

类型化启动方法返回 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,
};

字段

类型

描述

grid_dim

(u32, u32, u32)

x、y、z 维度上的 block 数

block_dim

(u32, u32, u32)

x、y、z 维度上每个 block 的线程数

shared_mem_bytes

u32

每个 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#

方法

做什么

.sync()

使用默认调度策略执行,阻塞当前线程直到完成

.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 构建。

常见启动错误#

错误

可能的原因

修复方法

CUDA_ERROR_INVALID_VALUE

Grid 或 block 维度为零或超出限制

检查 LaunchConfig 值;最大 block 为 1024 线程

CUDA_ERROR_NOT_FOUND

PTX 入口点名称不匹配

验证 #[kernel] 名称与加载的模块匹配

CUDA_ERROR_LAUNCH_OUT_OF_RESOURCES

共享内存过大或每个 block 使用的寄存器过多

减少 shared_mem_bytes 或 block 大小;使用 #[launch_bounds]

CUDA_ERROR_ILLEGAL_INSTRUCTION

Kernel 触发了陷阱(panic、assert 失败、越界)

使用 cargo oxide debuggpu_printf! 调试

CUDA_ERROR_NO_BINARY_FOR_GPU

PTX 为错误架构编译

使用与你的 GPU 匹配的 --arch 重新构建

参见

错误处理与调试章节 涵盖了如何详细诊断和修复 kernel 故障。