CUDA 01 · 环境、工具链与第一个 Kernel
先分清四个版本
| 名称 | 作用 | 常用查询 |
|---|---|---|
| NVIDIA Driver | 操作系统驱动;决定可支持的 CUDA Driver API 上限 | nvidia-smi |
| CUDA Toolkit | nvcc、头文件、Runtime、库、调试和分析工具 | nvcc --version |
| CUDA Runtime | 应用链接的运行时 API;静态或动态链接 | cudaRuntimeGetVersion |
| 框架自带 Runtime | PyTorch 等发行包携带的运行时依赖 | 框架自己的版本查询 |
nvidia-smi 显示的 “CUDA Version” 通常表示驱动最高兼容能力,不等于本机安装了同版本 nvcc。能运行 PyTorch CUDA 也不代表能编译 .cu 文件。
基础自检:
nvidia-smi
nvcc --version
cmake --versionLinux 还可检查动态库;Windows 需要受当前 Toolkit 支持的 MSVC。具体组合不要凭经验猜,直接查对应 Toolkit 的 Installation Guide 与 Release Notes。
编译链到底做了什么
┌─ host C++ ── host compiler ── CPU object ─┐
hello.cu ── nvcc 拆分 ─┤ ├─ linker ─ executable
└─ device C++ ─ PTX ─ ptxas ─ cubin/fatbin ┘
运行时:fatbin 中有匹配的 SASS/cubin → 直接加载
只有兼容 PTX → Driver JIT 为当前 GPU 编译- PTX 是面向虚拟 GPU 的中间指令集,便于向未来架构 JIT。
- SASS/cubin 是面向具体
sm_xx的机器码,启动快且可针对目标架构优化。 - fatbin 可同时携带多个 cubin 和一份或多份 PTX。
compute_xx表示虚拟架构能力,sm_xx表示真实架构目标。
官方 NVCC 文档建议把“最低需要的虚拟能力”与“实际部署的真实架构”分开考虑。生产发行常带目标 GPU 的 cubin,并保留一份合适的 PTX 作为前向兼容路径;但 PTX JIT 不是无条件保证,过老 Toolkit 的 PTX 可能无法利用新特性,驱动也必须能理解该 PTX 版本。
示例:
# 当前机器快速实验;生成本机可见 GPU 的机器码,不额外保留 PTX
nvcc -std=c++17 -O3 -arch=native vector_add.cu -o vector_add
# 例:为已知目标生成 cubin,并保留同一虚拟架构 PTX
nvcc -std=c++17 -O3 \
-gencode arch=compute_90,code=sm_90 \
-gencode arch=compute_90,code=compute_90 \
vector_add.cu -o vector_add不要照抄 sm_90:用 nvidia-smi --query-gpu=name,compute_cap 或 Runtime API 查询本机 Compute Capability,并按部署矩阵选择目标。
第一个完整程序
#include <cuda_runtime.h>
#include <cmath>
#include <cstdlib>
#include <iostream>
#include <vector>
#define CUDA_CHECK(call) \
do { \
cudaError_t err__ = (call); \
if (err__ != cudaSuccess) { \
std::cerr << "CUDA error: " << cudaGetErrorString(err__) \
<< " at " << __FILE__ << ':' << __LINE__ << '\n'; \
std::exit(EXIT_FAILURE); \
} \
} while (0)
__global__ void vector_add(const float* a, const float* b, float* c, int n) {
int i = blockIdx.x * blockDim.x + threadIdx.x;
if (i < n) c[i] = a[i] + b[i];
}
int main() {
constexpr int n = 1 << 20;
const size_t bytes = n * sizeof(float);
std::vector<float> a(n, 1.25f), b(n, 2.5f), c(n);
float *d_a = nullptr, *d_b = nullptr, *d_c = nullptr;
CUDA_CHECK(cudaMalloc(&d_a, bytes));
CUDA_CHECK(cudaMalloc(&d_b, bytes));
CUDA_CHECK(cudaMalloc(&d_c, bytes));
CUDA_CHECK(cudaMemcpy(d_a, a.data(), bytes, cudaMemcpyHostToDevice));
CUDA_CHECK(cudaMemcpy(d_b, b.data(), bytes, cudaMemcpyHostToDevice));
constexpr int threads = 256;
const int blocks = (n + threads - 1) / threads;
vector_add<<<blocks, threads>>>(d_a, d_b, d_c, n);
// 检查 launch 配置类错误;真正的执行错误常在同步点才暴露。
CUDA_CHECK(cudaGetLastError());
CUDA_CHECK(cudaMemcpy(c.data(), d_c, bytes, cudaMemcpyDeviceToHost));
for (int i = 0; i < n; ++i) {
if (std::abs(c[i] - 3.75f) > 1e-6f) return EXIT_FAILURE;
}
CUDA_CHECK(cudaFree(d_c));
CUDA_CHECK(cudaFree(d_b));
CUDA_CHECK(cudaFree(d_a));
std::cout << "ok\n";
}四元启动语法:
kernel<<<grid_dim, block_dim, dynamic_shared_bytes, stream>>>(args...);前两个参数可以是 int 或 dim3;第三项只计算 extern __shared__ 的动态共享内存;第四项决定操作进入哪条 stream。
为什么错误检查必须分两层
Kernel launch 对 host 通常是异步的:
CPU: launch ── cudaGetLastError ───────── memcpy/synchronize ──►
GPU: [排队] [真正执行 kernel] ─────────────────────►cudaGetLastError()检查非法 grid/block、参数等 launch 错误。cudaDeviceSynchronize()、同步 memcpy 或 event/stream 同步会暴露异步执行错误。- 调试阶段可暂时设置
CUDA_LAUNCH_BLOCKING=1让定位更直接,但不要拿这种模式测性能。
更稳妥的调试模板:
kernel<<<grid, block>>>(...);
CUDA_CHECK(cudaGetLastError());
#ifndef NDEBUG
CUDA_CHECK(cudaDeviceSynchronize());
#endifCMake 最小工程
cmake_minimum_required(VERSION 3.24)
project(cuda_tutorial LANGUAGES CXX CUDA)
add_executable(vector_add vector_add.cu)
target_compile_features(vector_add PRIVATE cxx_std_17 cuda_std_17)
set_target_properties(vector_add PROPERTIES
CUDA_SEPARABLE_COMPILATION ON
CUDA_ARCHITECTURES native)发布项目不应长期写 native,应显式维护部署 GPU 架构列表。跨 .cu 文件调用 device function 时需要可重定位设备代码/设备链接;CUDA_SEPARABLE_COMPILATION 会让 CMake 管理这一过程,但它可能妨碍部分跨文件优化,应按工程结构与性能测量决定。
常用编译选项
| 选项 | 用途 | 注意 |
|---|---|---|
-O3 | 优化 host/device code | release 基线 |
-lineinfo | 保留源码行映射 | 适合 profiler,代价小 |
-G | 生成 device debug 信息并禁用很多优化 | 只用于调试,不用于 benchmark |
-Xptxas=-v | 输出寄存器、spill、shared 等资源 | 观察资源压力 |
--use_fast_math | 使用较快、精度较低的运算路径 | 必须经过误差验证 |
-res-usage | 汇总 kernel 资源占用 | 便于 CI 留档 |
--keep / -ptx / -cubin | 检查中间产物 | 学习编译过程 |
Runtime API 与 Driver API
| 维度 | Runtime API | Driver API |
|---|---|---|
| 典型前缀 | cuda* | cu* |
| 抽象 | 隐式管理 primary context,易用 | 显式 context/module/function,控制更细 |
| Kernel 启动 | <<<>>> 或扩展 launch API | cuLaunchKernel 等 |
| 适合 | 大多数 CUDA C++ 应用 | JIT、插件系统、动态模块、运行时编译 |
二者可以互操作,但必须清楚当前 device、context、stream 和资源所有权。初学阶段先使用 Runtime API。
本章实验
- 编译并运行向量加法;把
n改成非 block size 整数倍验证边界检查。 - 删除边界检查,在 Compute Sanitizer 下观察越界报告。
- 使用
-Xptxas=-v记录寄存器数量。 - 分别生成 PTX 和 cubin,用
cuobjdump --list、cuobjdump --dump-sass检查产物。 - 写
deviceQuery小程序打印名称、Compute Capability、SM 数、warp size、最大 block threads、shared memory 和是否支持并发 copy/execute。