本文是对《CUDA Programming Guide》的阅读整理。
1. 阅读路径
CUDA 的知识点彼此关联:不懂 SIMT 就理解不了分支分歧的代价,不懂内存层次就理解不了合并访问,不懂内存模型就写不对跨块同步。因此建议按下面的依赖顺序阅读,而不是按 API 字母序查。
2. 异构执行模型
2.1 为什么是 CPU + GPU
CPU 与 GPU 的设计目标不同,芯片资源分配因此分道扬镳 [1][2]:
| 维度 | CPU | GPU |
|---|---|---|
| 设计目标 | 让单条线程尽量快 | 让数千条线程同时在跑(吞吐优先) |
| 并行规模 | 数十条线程 | 数千条线程 |
| 晶体管去向 | 大缓存与复杂流控 | 数据处理单元(浮点等) |
| 隐藏延迟的手段 | 缓存 + 流控 | 用海量并行掩盖访存延迟 |
GPU 用更多晶体管"算",而不是"等"。但真实应用总是并行段与串行段的混合,所以系统形态是 CPU + GPU 各司其职:CPU 负责控制流与串行逻辑,GPU 负责可并行的密集计算。
2.2 硬件层次:SM 与 GPC
- GPU 由若干流多处理器(SM)组成,SM 再组成图形处理簇(GPC)。
- 每个 SM 内含寄存器堆、统一数据缓存、若干功能单元;统一数据缓存同时为共享内存和 L1 缓存提供物理资源,两者的划分比例可以运行期配置。
- 各架构的 SM 规格(内存大小、功能单元数量)不同,但不影响编程模型的正确性——模型与硬件解耦。
2.3 网格、线程块与线程
一次 kernel 启动会启动成千上万条线程,它们的组织关系是三层结构 [2]:
由此导出两条编程铁律:
- 块内可协作:块内线程同驻一个 SM,可通过片上共享内存高效通信,并用
__syncthreads()同步。 - 块间无顺序保证:块的调度顺序(并发还是串行)不受保证,因此块与块之间不得有数据依赖,任何块顺序下结果都必须正确。
启动时通过执行配置指定网格与块的维度(<<<...>>>),可选参数还有动态共享内存字节数与流;簇大小、SM 配置等更复杂的项需用 cudaLaunchKernelEx 传递启动属性 [3][9]。
2.4 warp 与 SIMT
- 块内线程以 32 条为一组构成 warp,warp 是硬件调度单位,以 SIMT(单指令多线程)方式执行。
- warp 内每条线程有自己的指令计数器与寄存器状态,因此可以走不同分支;但硬件每次只能发射一条公共指令,于是不同路径被串行执行,其余线程被掩蔽——这就是分支分歧(divergence)。
- 推论:块内线程数最好是 32 的倍数,否则最后一个 warp 存在空闲 lane,浪费功能单元与访存带宽。
- SIMT 与 SIMD 的区别:SIMD 只有单一控制流且数据宽度固定;SIMT 允许每线程独立路径,无固定数据宽度 [2]。
2.5 线程块簇(可选分组)
Compute Capability 9.0(Hopper)及以上支持把若干相邻块组成簇 [2][15]:
- 指定簇不改变网格维度与块索引;簇内所有块同驻一个 GPC,并被一起调度。
- 簇内块可以互相访问彼此的共享内存,形成分布式共享内存(DSMEM),并用 Cooperative Groups 提供的接口做跨块同步。
- 簇的可移植最大尺寸为 8 个块,实际上限由硬件决定。
2.6 计算能力(CC)
CC 是设备能力版本号,决定了块内最大线程数、共享内存上限、寄存器数量以及是否支持簇、TMA 等特性 [26]。编译期用虚拟架构与真实架构两个维度控制产物形态,运行期可用 cudaGetDeviceProperties 查询。
| 架构 | CC | 关键新增 |
|---|---|---|
| Volta | 7.0 | 独立线程调度、Tensor Core |
| Turing | 7.5 | 光线追踪核心 |
| Ampere | 8.0 | cp.async 异步拷贝、异步屏障 |
| Hopper | 9.0 | TMA、线程块簇、DPX |
| Blackwell | 10.x / 12.x | 新一代 Tensor Core 与新数据类型 |
3. Kernel、索引与边界
3.1 定义与启动
用 __global__ 声明 kernel,返回类型必须为 void;它由许多线程并行执行 [3]。启动时给出执行配置,前两个参数是网格维度与块维度,一维场景可以直接用整数:
__global__ void vecAdd(float* A, float* B, float* C) { }
// 1 个块、256 条线程
vecAdd<<<1, 256>>>(A, B, C);
多维场景用 dim3;完整形式还包含动态共享内存字节数与流:
kernel<<<gridDim, blockDim, sharedMemBytes, stream>>>(args...);
两点常被忽略:
3.2 索引计算
| 变量 | 含义 | 值域 |
|---|---|---|
threadIdx | 块内线程索引(.x/.y/.z) | 0 .. blockDim-1 |
blockDim | 块维度 | 启动时指定 |
blockIdx | 块在网格中的索引 | 0 .. gridDim-1 |
gridDim | 网格维度 | 启动时指定 |
一维并行最常见的线性化方式:
int workIndex = threadIdx.x + blockDim.x * blockIdx.x;
C[workIndex] = A[workIndex] + B[workIndex];
多维块与网格只是方便表达,本身不影响性能;线程线性化规则是 x 变化最快、y 步长为 blockDim.x、z 步长为 blockDim.x * blockDim.y,这条规则决定了线程如何被划分进 warp [4]。
3.3 边界检查与块数计算
当数据长度不是块大小的整数倍时,必须做边界检查,并把块数上取整:
__global__ void vecAddChecked(float* A, float* B, float* C, int n) {
int i = threadIdx.x + blockDim.x * blockIdx.x;
if (i < n) {
C[i] = A[i] + B[i];
}
}
int threads = 256;
int blocks = (n + threads - 1) / threads; // 上取整
vecAddChecked<<<blocks, threads>>>(devA, devB, devC, n);
CCCL 提供了 cuda::ceil_div(头文件 <cuda/cmath>)替代手写上取整 [32]。多启动几个"不做事的线程"代价很小,但应避免启动整块都无工作的块;256 线程/块是一个稳妥的起点。
3.4 一个完整可运行的示例
把前几节拼起来,就是下面这个能编译、能自校验的最小程序。语法上它就是 C++,但每一处与标准 C++ 不同的地方,都在注释里标了出来——差异集中在三点:执行空间、执行配置 <<<...>>>、以及指针属于哪一侧的内存。
// CUDA C++ 的"非标准"之处:这段代码有三处是标准 C++ 里不存在的写法——
// 执行空间说明符(__global__)、执行配置(<<<...>>>)、设备内存 API(cuda*)。
#include <cstdio>
#include <cstdlib>
#include <cuda_runtime.h>
// CUDA 的 API 是 C 风格接口:返回错误码,不像标准 C++ 那样抛异常,
// 所以每个调用都要自己检查返回值,否则错误会一路静默传递。
#define CUDA_CHECK(call) \
do { \
cudaError_t err = (call); \
if (err != cudaSuccess) { \
std::fprintf(stderr, "%s:%d %s\n", __FILE__, __LINE__, \
cudaGetErrorString(err)); \
std::exit(EXIT_FAILURE); \
} \
} while (0)
// __global__ 是执行空间说明符:标准 C++ 没有这个概念。
// 它表示"编译为 GPU 代码、由 host 启动、在 device 上执行",且必须返回 void。
__global__ void vecAdd(const float* A, const float* B, float* C, int n) {
// threadIdx / blockDim / blockIdx 不是标准库变量,而是编译器内置变量:
// 语法与普通变量一样,但每条线程各有一份,用它算出自己的唯一编号。
int i = threadIdx.x + blockDim.x * blockIdx.x;
// 块大小通常不能整除数据量,最后一块里的部分线程会越界,必须挡住。
if (i < n) {
C[i] = A[i] + B[i]; // 这里的指针指向显存,解引用由 GPU 完成
}
}
int main() {
// 从这里往下是 host 代码,由 CPU 执行,写法与普通 C++ 一样。
// 但 host 与 device 的内存是分开的:malloc / new 只对 host 侧有效。
const int n = 1 << 20; // 1M 个元素
const size_t bytes = n * sizeof(float);
float* hA = static_cast<float*>(std::malloc(bytes));
float* hB = static_cast<float*>(std::malloc(bytes));
float* hC = static_cast<float*>(std::malloc(bytes));
for (int i = 0; i < n; ++i) { hA[i] = 1.0f; hB[i] = 2.0f; }
// 为什么非要手写这些拷贝?因为 GPU 有自己的显存,与 host 的系统内存是
// 两块物理内存:kernel 跑在 device 侧,只能访问设备内存。
// 把 host 指针当普通 C++ 指针用(在 device 上解引用)不是"慢一点",
// 而是非法访问。所以流程必然是:分配设备内存 -> 拷进去 -> 启动 -> 拷回来。
float *dA = nullptr, *dB = nullptr, *dC = nullptr;
CUDA_CHECK(cudaMalloc(&dA, bytes));
CUDA_CHECK(cudaMalloc(&dB, bytes));
CUDA_CHECK(cudaMalloc(&dC, bytes));
// cudaMemcpy 的参数是 C 风格 void*,看不出方向,所以要显式指定
// cudaMemcpyHostToDevice;若系统支持统一虚拟地址空间,也可传
// cudaMemcpyDefault 让驱动按指针推断(见第 5 节)。
CUDA_CHECK(cudaMemcpy(dA, hA, bytes, cudaMemcpyHostToDevice));
CUDA_CHECK(cudaMemcpy(dB, hB, bytes, cudaMemcpyHostToDevice));
// <<<blocks, threads>>> 不是 C++ 语法,而是 nvcc 的执行配置扩展:
// 标准 C++ 里没有"一次调用启动 N 个线程"的表达方式。
const int threads = 256;
const int blocks = (n + threads - 1) / threads;
vecAdd<<<blocks, threads>>>(dA, dB, dC, n);
// 关键差异:启动是异步的。这一行返回时 kernel 可能还没开始执行,
// 与标准 C++"函数返回即执行完毕"的直觉相反,因此错误分为两处查。
CUDA_CHECK(cudaGetLastError()); // 捕获启动期错误
CUDA_CHECK(cudaDeviceSynchronize()); // 等待执行完成
CUDA_CHECK(cudaMemcpy(hC, dC, bytes, cudaMemcpyDeviceToHost));
bool ok = true;
for (int i = 0; i < n && ok; ++i) { ok = (hC[i] == 3.0f); }
std::printf("result: %s\n", ok ? "PASS" : "FAIL");
// 设备内存必须显式释放;host 内存照旧走标准 C++ 的 free。
CUDA_CHECK(cudaFree(dA));
CUDA_CHECK(cudaFree(dB));
CUDA_CHECK(cudaFree(dC));
std::free(hA);
std::free(hB);
std::free(hC);
return 0;
}
nvcc -O2 -arch=native vector_add.cu -o vector_add
./vector_add
为什么这段代码必须手写两次 cudaMemcpy:CUDA 不只是"给 C++ 加几个关键字",它把一台机器的内存劈成了两半,而两半之间不会自动搬运数据。
host 侧(CPU + 系统内存) device 侧(GPU + 显存)
hA / hB / hC dA / dB / dC
由 malloc 分配 由 cudaMalloc 分配
| ^
+---- cudaMemcpy(..., HostToDevice) -------+
+---- cudaMemcpy(..., DeviceToHost) -------+
- kernel 在 device 上执行,只能访问设备内存。把 host 指针传进 kernel 再解引用,不是"慢一点",而是非法访问(报 invalid memory access)。
- 所以"数据先在 GPU 能访问的内存里"是硬约束,不是风格选择。标准 C++ 里的引用、指针、
std::copy全部只在同一地址空间内成立,跨不过这条边界。 cudaMemcpy的第 4 个参数正是为这件事服务的:接口是 C 风格的void*,看不出方向,所以要显式写cudaMemcpyHostToDevice或cudaMemcpyDeviceToHost。系统支持统一虚拟地址空间时,也可以传cudaMemcpyDefault让驱动按指针自动推断(见第 5 节)。- 想彻底省掉这份手写拷贝,可以改用托管内存(
cudaMallocManaged),由驱动按需迁移;代价是数据放置不可控、性能更依赖系统能力。两种路线的取舍在第 5 节有对照表。
四点值得留意:
cudaGetLastError()要紧跟启动调用,它把"启动失败"(比如块尺寸越界)与"执行中出错"区分开;漏掉它,错误会漂移到后面某次同步调用上,排查时会指向错误的位置。- 校验循环只是示意。真实工程应比较浮点误差,而不是要求逐位相等。
-arch=native按本机 GPU 生成 Cubin;若要分发给别的机器,应改为生成 PTX 或显式列出多个目标(见第 9 节)。- 中文 Windows 上还有一类经典陷阱:源文件是 UTF-8 而系统代码页是 GBK 时,MSVC 会把中文注释末尾的某些字节当成续行符,于是注释吞掉下一行代码,报出
identifier "bytes" is undefined这种看起来无缘无故的错误。加-Xcompiler /utf-8即可(也可以把源文件保存为带 BOM 的 UTF-8):
nvcc -O2 -arch=native -Xcompiler /utf-8 vector_add.cu -o vector_add
4. 内存体系
4.1 层次、作用域与生命周期
| 内存类型 | 作用域 | 生命周期 | 物理位置 |
|---|---|---|---|
| 寄存器 | 线程 | 线程 | SM |
| 本地内存 | 线程 | 线程 | 显存(逻辑私有,物理在全局) |
| 共享内存 | 块 | kernel | SM 片上(与 L1 共享资源) |
| 全局内存 | 网格 + host | 应用 | 显存 |
| 常量内存 | 网格(只读) | 应用 | 显存(带缓存) |
访问代价按"距离"递增:寄存器最快,共享内存低延迟,全局内存容量大但延迟高 [2][4]。
落到硬件上,一个 SM 的组织大致是这样:
SM
+-- 寄存器堆 每线程私有,编译器分配
+-- 统一数据缓存 物理资源可配置切分
| +-- 共享内存 块内共享,用户显式管理
| +-- L1 缓存 由硬件管理
+-- 功能单元 FP32 / INT / Tensor Core / 访存单元 ...
+-- warp 调度器 每周期挑一个就绪 warp 发射指令
共享内存与 L1 抢的是同一块物理资源,这既是"多给共享内存能加速"的原因,也是"给过头反而变慢"的原因:L1 被挤小,命中率下降会把压力转移回全局内存。
4.2 共享内存:静态与动态
__shared__ float staticArray[1024]; // 静态:编译期定长
kernel<<<grid, block, sharedBytes>>>(); // 启动时给出字节数
extern __shared__ float dynamicArray[]; // 动态:一个 kernel 只能有一个
动态共享内存每个 kernel 只能有一个;需要多块视图时,申请一个大数组自行切分,并注意对齐:
extern __shared__ float array[];
short* a0 = (short*)array; // 2 字节对齐
float* a1 = (float*)&a0[128]; // 128*2 = 256 字节,满足 4 字节对齐
int* a2 = (int*)&a1[64];
若切分位置未按目标类型对齐(例如把 float* 落在 254 字节偏移处),访问行为不正确 [4]。
4.3 两种访存效率陷阱
未合并的全局访问:同一 warp 的 32 条线程若访问连续地址,硬件会把它们合并成尽可能少的内存事务;地址离散则事务数暴涨、吞吐骤降。设计 kernel 时应让 threadIdx.x 相邻的线程访问相邻地址 [4]:
float v = data[threadIdx.x + blockIdx.x * blockDim.x]; // 合并访问
结构体数组(AoS)是这件事上最常见的坑:相邻线程访问 p[i].x 时地址间隔是 sizeof(Particle),一个 128 字节的事务里只用得上一小部分;把字段拆成各自连续的数组(SoA)后,同一个 warp 的访问重新变得连续:
struct Particle { float x, y, z; };
Particle* p; // AoS:相邻线程的地址间隔 sizeof(Particle)
float x = p[i].x; // 事务利用率低
float *px, *py, *pz; // SoA:每个字段各自连续
float x2 = px[i]; // 合并访问
共享内存 bank 冲突:共享内存按 bank 组织(当前架构 32 个 bank,相邻 4 字节映射到相邻 bank)。warp 内多条线程访问同一 bank 的不同地址时访问被串行化;所有线程访问不同 bank、或访问同一地址(广播)都不会冲突。数据重排(如先读入共享内存再转置)常用于把不可合并的访问转成可合并访问 [4]。
4.4 寄存器、本地内存与占用率
- 寄存器由编译器分配,可用
-maxrregcount或__launch_bounds__限制每线程上限;限制过紧会导致寄存器溢出(spilling),值被写到全局内存再读回。 - 本地内存是逻辑私有、物理位于全局内存的存储,常用于索引不确定的数组、放不进寄存器的结构体以及溢出变量,访问代价与全局内存同级 [4]。
- 占用率 = SM 上活跃 warp 数 / SM 支持的最大 warp 数,受每线程寄存器数、每块共享内存量、块与线程数上限共同约束;资源不足以驻留至少一个块时 kernel 直接启动失败。它是"够用即好"的指标,不是越高越好——寄存器换并行度、共享内存换 L1 容量都需要权衡 [4]。
5. 数据放置:统一内存与显式管理
数据位于访问它的处理器本地内存时性能最佳。CUDA 给了两条路:让驱动自动搬迁(统一内存),或自己显式管理(cudaMalloc + cudaMemcpy)[7]。
5.1 统一虚拟地址空间(UVA)
同一 OS 进程内,host 内存与所有 GPU 显存处在同一个虚拟地址空间。因此:
cudaPointerGetAttributes()可以从指针值反查内存位置(CPU 还是哪个 GPU);cudaMemcpy*的cudaMemcpyKind可传cudaMemcpyDefault,由指针自动推断拷贝方向 [7]。
5.2 统一内存的四种范式
统一内存的行为取决于 OS、内核版本、GPU 与互联方式。用三个设备属性即可判定当前系统属于哪种范式 [7]:
| 属性 | 含义 |
|---|---|
cudaDevAttrConcurrentManagedAccess | 1 = 完整统一内存支持;0 = 受限支持 |
cudaDevAttrPageableMemoryAccess | 1 = 全部系统内存都统一;0 = 仅显式托管分配统一 |
cudaDevAttrPageableMemoryAccessUsesHostPageTables | 1 = 硬件一致性;0 = 软件一致性 |
int concurrentManagedAccess = 0, pageableMemoryAccess = 0, usesHostPageTables = 0;
cudaDeviceGetAttribute(&concurrentManagedAccess, cudaDevAttrConcurrentManagedAccess, dev);
cudaDeviceGetAttribute(&pageableMemoryAccess, cudaDevAttrPageableMemoryAccess, dev);
cudaDeviceGetAttribute(&usesHostPageTables,
cudaDevAttrPageableMemoryAccessUsesHostPageTables, dev);
- 受限支持:托管内存先落在 CPU 内存,以较粗粒度迁移;GPU 活跃期间 CPU 不得访问;不允许超额订阅。
- 完整支持:托管内存通常分配在首次触碰它的处理器内存中,被其他处理器使用时按页(软件一致性)或缓存行(硬件一致性)迁移,允许超额订阅显存。
- 硬件一致性(Grace 系列 + NVLink C2C):驻留在 GPU 的托管分配可被 CPU 直接访问而无需迁移,还支持 host 侧原生原子;但硬件一致性不会让 host 访问纯 GPU 分配(如
cudaMalloc的结果)[7]。
托管内存的分配方式有 cudaMallocManaged、cudaMallocFromPoolAsync(池类型为托管)以及 __managed__ 全局变量。性能调优的两个抓手是 cudaMemAdvise(声明只读、偏好位置、将被谁访问)与 cudaMemPrefetchAsync(在 kernel 前异步预取,与计算重叠)[7]。
5.3 取舍
| 维度 | 显式管理 | 统一内存 |
|---|---|---|
| 代码复杂度 | 高(每处拷贝都要写) | 低(自动迁移) |
| 性能可预测性 | 高(放置可控) | 取决于系统能力与提示质量 |
| 适用场景 | 性能关键、数据流明确 | 快速开发、复杂数据结构、需要超额订阅 |
显式管理的典型流程是分配、上行拷贝、执行、下行拷贝、释放;异步拷贝指向 host 的缓冲区必须是页锁定内存,否则会退化为同步行为。页锁定内存用 cudaMallocHost / cudaHostAlloc 分配,用 cudaFreeHost 释放,也可用 cudaHostRegister 把已有内存注册为页锁定 [7]。
6. 异步执行
6.1 可以并发的六件事
host 计算、device 计算、H2D 拷贝、D2H 拷贝、设备内拷贝、设备间拷贝,这些都可以彼此重叠 [6]。异步接口的语义是调用立即返回(可能在工作完成、甚至开始之前),因此需要结果时必须同步:
| 同步风格 | 行为 | 代表接口 |
|---|---|---|
| 阻塞 | 调用阻塞直到完成 | cudaDeviceSynchronize |
| 非阻塞 | 立即返回并给出状态 | cudaStreamQuery |
| 回调 | 完成时执行注册的函数 | cudaLaunchHostFunc |
6.2 流与事件
流是一个命令队列:往里追加拷贝与 kernel,按入队顺序执行;不同流之间可以并发。事件是插入流的标记,用来表达依赖与计时 [6]。
cudaStream_t stream;
cudaStreamCreate(&stream);
kernel<<<grid, block, 0, stream>>>(...); // 流内启动
cudaMemcpyAsync(dst, src, bytes, cudaMemcpyHostToDevice, stream); // 流内异步拷贝
cudaEvent_t start, stop;
cudaEventCreate(&start); cudaEventCreate(&stop);
cudaEventRecord(start, stream);
cudaEventRecord(stop, stream);
cudaStreamSynchronize(stream);
float elapsedMs = 0.0f;
cudaEventElapsedTime(&elapsedMs, start, stop);
cudaEventDestroy(start); cudaEventDestroy(stop);
cudaStreamDestroy(stream);
两个易错点:
- 异步拷贝涉及 CPU 内存时要求页锁定(pinned),否则无法与其他工作重叠 [6]。
- 默认流与其他流之间存在隐式同步语义;用
cudaStreamNonBlocking创建的流不与默认流隐式同步 [6]。
6.3 CUDA Graphs
流中每次 kernel 启动都要付出 host 侧设置开销;对短 kernel,这笔开销可能占据端到端时间的显著比例。图把工作流定义一次、实例化一次,之后重复启动的开销极低,并把整体结构交给 CUDA 系统去做跨操作优化 [14]:
- 结构:节点是操作,边是依赖;节点依赖满足即可被调度。
- 构建:Graph API 逐节点构图,或用 Stream Capture 捕获一段流操作自动构图。
- 执行:
cudaGraphInstantiate实例化后cudaGraphLaunch反复启动;图优化的主要成本在实例化期。 - 更新:
cudaGraphExecUpdate可整图或逐节点更新参数与依赖,避免重新实例化。
6.4 程序化依赖启动(PDL)
普通流模型下,后继 kernel 要等前驱完成、host 才知道可以启动,存在 CPU 往返延迟。PDL 让前驱 kernel 的设备代码自己触发后继 kernel 启动,把 kernel 间依赖交给设备侧控制,从而省掉 CPU 往返 [9][16]。
7. 同步与内存模型
7.1 块内屏障
__syncthreads() 阻塞块内所有线程直到全部到达,并保证此前所有内存访问在屏障后对块内线程可见 [4]:
__global__ void blockReduce(const int* in, int* out) {
__shared__ int shared[128];
shared[threadIdx.x] = in[blockDim.x * blockIdx.x + threadIdx.x];
__syncthreads(); // 到此为止所有写入对块内可见
if (threadIdx.x == 0) {
int sum = 0;
for (int i = 0; i < blockDim.x; ++i) sum += shared[i];
out[blockIdx.x] = sum;
}
}
若块内部分线程因分支未执行 __syncthreads(),行为未定义(可能死锁)。更细粒度的选择是 __syncwarp()(warp 内)、cuda::barrier(可被异步操作到达)与 Cooperative Groups 的组同步 [10][15][17]。
7.2 原子操作
对全局或共享内存的原子读-改-写避免数据竞争,常用 atomicAdd/atomicSub、atomicExch、atomicMin/atomicMax、atomicInc/atomicDec、atomicCAS、位运算系列 [4]。注意:同一 warp 内对同一地址的原子操作会被串行化,且执行顺序不保证(每次读-改-写都会发生)。
7.3 线程作用域
内存操作的可见范围与同步范围由线程作用域定义 [10][28]:
| 作用域 | 可见/可同步范围 |
|---|---|
cuda::thread_scope_thread | 仅本线程 |
cuda::thread_scope_block | 同块线程 |
cuda::thread_scope_device | 同一设备(同一内存同步域内) |
cuda::thread_scope_system | 系统内所有线程(含 CPU 与其他 GPU) |
它把"同步距离"的成本显式化:块内同步便宜、系统级同步昂贵,因此标准 C++ 那种"同步成本均匀"的假设在 CUDA 中不成立 [28]。
7.4 一致性保证的层次
| 同步需求 | 所需手段 |
|---|---|
| 同块内线程 | __syncthreads() 或 cuda::barrier 建立 happens-before |
| 跨块/网格内 | 设备级作用域原子或栅栏(thread_scope_device),或 cooperative launch |
| 跨设备/与 CPU | 系统级作用域原子或栅栏(thread_scope_system);跨内存同步域时还需额外的 fence [21] |
内存序方面,cuda::atomic 支持 relaxed/consume/acquire/release/acq_rel/seq_cst,默认为 seq_cst [28]。
7.5 出错时的典型症状
同步写错的麻烦在于它通常不报错,只表现为"结果偶尔不对"。按症状定位:
| 症状 | 常见原因 | 正确写法 |
|---|---|---|
| 结果偶发错误,换个块尺寸又好了 | 读共享内存前漏了块内屏障 | 写入与读取之间加 __syncthreads() |
| 程序偶发挂死 | 屏障落在分支里,部分线程到不了 | 把 __syncthreads() 移出条件分支,或改用 Cooperative Groups 的组同步 |
| 单块跑得对,多块结果错 | 依赖了块间执行顺序 | 用设备级原子或栅栏;需要网格级同步时用 cooperative launch |
| CC 7.0 前后行为不一致 | 依赖了隐式 warp 锁步 | 用 __syncwarp() 显式同步 |
| CPU 读到旧值 | 缺系统级栅栏就跨设备读写 | 用 thread_scope_system 的原子或栅栏 |
共同点:这几种写法在某一代硬件上可能"刚好能跑",但它们依赖的是实现细节,不是内存模型给出的保证。
8. 特性专题速览
下表是整个特性面的索引,按"需要知道完整细节时查官方页"的方式使用;每一项在官方指南都有独立章节 [1][13]。
| 特性 | 一句话动机 | 入口 API / 关键字 |
|---|---|---|
| Tile 编程模型 | 以块为单位写代码,把并行度交给编译器,更容易用上 TMA 与 Tensor Core | __tile_global__ / __tile__ / ct::tile [5] |
| Cooperative Groups | 提供安全、面向未来的组同步与 scan/reduce 原语,替代手写脆弱的 warp 内同步 | this_thread_block() / tiled_partition / cg::reduce [15] |
| Programmatic Dependent Launch | 前一 kernel 设备侧触发后一 kernel,消除 CPU 往返 | cudaTriggerProgrammaticLaunchCompletion [16] |
| 异步屏障 | 屏障可被异步操作"到达",支撑生产者-消费者流水线 | cuda::barrier [17] |
| 流水线 | 多阶段异步搬运,stage count 支持预取多批数据 | cuda::pipeline + cuda::memcpy_async [18] |
| 异步数据拷贝 | 全局到共享的搬运不再经寄存器;TMA 支持多维张量、im2col、Swizzle | cp.async(CC 8.0+)、TMA(CC 9.0+)[19] |
| CUDA Graphs | 定义一次、重复低开销启动,并可做跨操作优化 | cudaGraphCreate / Stream Capture [14] |
| 流序内存分配器 | 分配与释放按流排序,规避 CPU 侧分配锁与 use-after-free 竞态 | cudaMallocAsync / cudaFreeAsync [1] |
| L2 缓存控制 | 把 L2 的一部分留给频繁复读的数据 | cudaLimitPersistingL2CacheSize + accessPolicyWindow [20] |
| 内存同步域 | 跨域原子/栅栏需要额外 fence 才能保证全局排序 | cudaDevAttrMemorySyncDomainCount [21] |
| 虚拟内存管理 | 把物理分配与虚拟映射、访问权限分离,支持稀疏与自定义分配器 | cuMemCreate/cuMemMap(仅 Driver API)[22] |
| 进程间通信 | 跨进程共享显存与事件 | cudaIpcGetMemHandle / cudaIpcOpenMemHandle [24] |
| CUDA 动态并行 | 设备代码自行启动子网格,适合递归与不规则并行 | 设备侧启动(CDP2,CUDA 12.0 起默认)[23] |
| 图形互操作 | 与 OpenGL/D3D/Vulkan 共享资源,渲染与计算互喂 | cudaGraphicsGLRegisterBuffer 等 [25] |
| 绿色上下文 | 对 GPU 功耗与频率做上下文级控制 | cudaGreenCtxCreate [1] |
| 惰性加载 | 把 cubin 加载推迟到首次使用,加快启动 | CUDA_MODULE_LOADING=LAZY [1] |
| Driver 入口点访问 | 在 runtime 程序里直接取 driver 函数指针 | cudaGetDriverEntryPoint [1] |
其中两条细节值得单独记:异步代理(async proxy)——TMA 这类批量搬运走 async proxy,其与普通访问之间不保证排序,需要 proxy fence;CUDA 13 起,__global__ 函数与设备变量默认内部链接,跨编译单元引用需要显式 extern [10][8]。
9. 编译与工具链
nvcc 是编译器驱动:对外提供统一命令行,对内按阶段调用宿主 C++ 编译器、PTX 汇编器与 ptxas [8][31]。
| 扩展名 | 内容 |
|---|---|
.c / .cpp / .cc / .cxx | 仅 host 代码 |
.h / .hpp / .hh / .hxx | host / device / 混合 |
.cu | CUDA 源文件(device + host) |
.cuh | CUDA 头文件 |
架构控制(默认生成最低架构以最大化兼容性)[8]:
| 选项 | 效果 |
|---|---|
-arch=compute_XY | 只生成该虚拟架构的 PTX(可 JIT 前向兼容) |
-arch=sm_XY | 生成 PTX + Cubin |
-arch=native | 按本机 GPU 生成 Cubin(无 PTX) |
-arch=all | 全部支持架构的 Cubin + 最新 PTX |
-gencode=arch=compute_XY,code=sm_XY | 精确控制多目标 |
分离编译:默认是整体编译,使用 GPU 代码的编译单元必须自带全部设备代码。加 -rdc=true(别名 -dc)后设备代码可跨编译单元链接,组织更灵活、编译更快、二进制更小,代价是链接期优化机会减少——可用 LTO(-dlto)找回大部分性能 [8]:
nvcc -dc -dlto -arch=sm_100 definition.cu -o definition.o
nvcc -dc -dlto -arch=sm_100 example.cu -o example.o
nvcc -dlto definition.o example.o -o program
调试与优化常用组合:-g(host 调试信息)、-G(设备调试信息,会抑制优化)、-lineinfo(行号信息,供 compute-sanitizer 与 Nsight 关联)、-res-usage(资源占用报告)、-Xptxas=-warn-spills(寄存器溢出告警)。性能分析交给 Nsight Compute(kernel 级)与 Nsight Systems(系统级时间线)[33][34]。
10. 性能准则
官方优化策略可以归纳成三句话 [27]:
1. 最大化利用率 (Maximize Utilization)
应用级: 用流/图重叠拷贝与计算
设备级: 数据常驻显存, 避免反复 H2D/D2H
SM 级 : 用占用率找到块/线程平衡点
2. 最大化访存吞吐 (Maximize Memory Throughput)
减少 host-device 传输(最贵)
全局访问合并(coalesced)
复用数据放共享内存, 避免重复读全局
3. 最大化指令吞吐 (Maximize Instruction Throughput)
避免 warp 内分支分歧
使用内建快速数学函数
循环展开, 但权衡寄存器压力
设计动机值得点明:GPU 靠"海量并行 + 合并访存 + 指令级流水"掩盖延迟,所以优化目标不是让单线程更快,而是让整个 SM 的并行度、访存效率与指令效率同时最大化。反过来,任何把线程串起来、把访存打散、把分支岔开的重构,都会直接吃掉这三项收益。
先量再调,否则改的大概率不是瓶颈。三个够用的入口:
| 手段 | 看什么 | 怎么判断 |
|---|---|---|
nvcc -res-usage(或 --ptxas-options=-v) | 每线程寄存器数、共享内存用量、是否溢出 | 出现 spill 说明寄存器不够,先放宽 __launch_bounds__ |
cudaOccupancyMaxActiveBlocksPerMultiprocessor | 实际可驻留的块数 | 与理论上限差得多,说明受寄存器或共享内存限制 |
| Nsight Compute [33] | 计算与访存吞吐占比、warp stall 原因 | 访存吞吐接近峰值而计算占比低,先改访存;反之先改指令 |
用 Nsight Compute 时别一上来就看全部指标:先判断 kernel 处于"访存受限"还是"计算受限",再顺着对应的那组指标往下看。
11. 常见误解
| 误解 | 实际情况 |
|---|---|
| 占用率越高越快 | 占用率只是"够用即好"。提高它通常靠降寄存器或降共享内存,代价可能是溢出与重复访存,净效果可能更差 |
| 共享内存给得越多越快 | 共享内存与 L1 共用物理资源,给多了会挤小 L1;bank 冲突没解决时,多给也不会变快 |
cudaMemcpy 一定慢 | 慢的是不可重叠的同步拷贝。配合页锁定内存与多流,拷贝能与计算重叠,此时它不再是瓶颈 |
| 块数越多越好 | 块数不足会让 SM 空转,过多则调度与尾部开销上升。覆盖全部数据且能整除,基本就是合适的量 |
__syncthreads() 能同步整个网格 | 它只作用于当前块。跨块要用设备级原子或栅栏,网格级同步得用 cooperative launch |
12. 结语
CUDA 的每条约束都能追溯到硬件:块内线程同驻一个 SM,所以共享内存可用、块内屏障便宜;块与块之间没有调度顺序,所以跨块同步只能靠全局内存或协同启动;同步成本随线程距离上升,所以线程作用域把这笔代价显式写进了代码。那些"在某一代硬件上刚好能跑"的写法——依赖 warp 锁步、反复重读全局内存、让同一 warp 走不同分支——都会在架构换代时暴露问题。
优化的方向也只有一个:让并行度、访存效率、指令效率同时最大化。剩下的功课是把它落到工具链上:选对编译目标与分离编译策略,用流与图组织异步,需要新特性时按第 8 节的特性索引回查官方文档。
12. 术语表
| 术语 | 含义 |
|---|---|
| SM | 流多处理器,块执行的物理单元 |
| GPC | GPU 处理簇,SM 的集合 |
| warp | 32 条线程组成的硬件调度单位 |
| SIMT | 单指令多线程执行模型 |
| lane | warp 中线程的位置(0-31) |
| divergence | warp 内分支路径不同导致的串行执行 |
| coalesced access | 同一 warp 访问连续地址,合并为少数内存事务 |
| occupancy | SM 上活跃 warp 数与理论上限之比 |
| execution configuration | 启动配置 <<<grid, block, smem, stream>>> |
| PTX | 虚拟指令集,与具体 GPU 无关的中间表示 |
| Cubin / Fatbin | 原生机器码 / 多架构打包容器 |
| CC | Compute Capability,设备能力版本号 |
| UVA | 统一虚拟地址空间 |
| managed memory | 统一内存托管分配,可自动迁移 |
| pinned memory | 页锁定主机内存,异步拷贝的前提 |
| stream | GPU 命令队列 |
| event | 流内标记与时间戳 |
| CDP | CUDA 动态并行 |
| TMA | Tensor Memory Accelerator |
| DSMEM | 分布式共享内存(簇内跨块访问) |
| thread scope | 内存操作的可见性与同步范围 |
| async proxy | 异步操作的内存访问代理 |
14. 参考
行内引用编号 [n] 对应下列条目。主文档为《CUDA Programming Guide》分章节版,另有经典版《CUDA C++ Programming Guide》保留传统章节编号,两者内容互补。
链接与章节编号依据 2026 年 9 月的官方文档核对。文中带版本前提的结论(例如 CUDA 13 起设备符号默认内部链接、CDP2 的适用条件)以该版本为准,跨版本使用时请对照官方的变更说明。
- [1]: CUDA Programming Guide(总目录)— https://docs.nvidia.com/cuda/cuda-programming-guide/index.html
- [2]: CUDA Programming Guide, 1.2 Programming Model(语言无关的编程模型)— https://docs.nvidia.com/cuda/cuda-programming-guide/01-introduction/programming-model.html
- [3]: CUDA Programming Guide, 2.1 Intro to CUDA C++(kernel、启动、索引、内存、同步)— https://docs.nvidia.com/cuda/cuda-programming-guide/02-basics/intro-to-cuda-cpp.html
- [4]: CUDA Programming Guide, 2.3 Writing SIMT Kernels — https://docs.nvidia.com/cuda/cuda-programming-guide/02-basics/writing-cuda-kernels.html
- [5]: CUDA Programming Guide, 2.4 Writing Tile Kernels — https://docs.nvidia.com/cuda/cuda-programming-guide/02-basics/writing-tile-kernels.html
- [6]: CUDA Programming Guide, 2.5 Asynchronous Execution(流、事件、回调)— https://docs.nvidia.com/cuda/cuda-programming-guide/02-basics/asynchronous-execution.html
- [7]: CUDA Programming Guide, 2.6 Unified and System Memory — https://docs.nvidia.com/cuda/cuda-programming-guide/02-basics/understanding-memory.html
- [8]: CUDA Programming Guide, 2.7 NVCC: The NVIDIA CUDA Compiler — https://docs.nvidia.com/cuda/cuda-programming-guide/02-basics/nvcc.html
- [9]: CUDA Programming Guide, 3.1 Advanced CUDA APIs and Features — https://docs.nvidia.com/cuda/cuda-programming-guide/03-advanced/advanced-host-programming.html
- [10]: CUDA Programming Guide, 3.2 Advanced Kernel Programming(PTX、硬件实现、线程作用域)— https://docs.nvidia.com/cuda/cuda-programming-guide/03-advanced/advanced-kernel-programming.html
- [11]: CUDA Programming Guide, 3.3 The CUDA Driver API — https://docs.nvidia.com/cuda/cuda-programming-guide/03-advanced/driver-api.html
- [12]: CUDA Programming Guide, 3.4 Programming Systems with Multiple GPUs — https://docs.nvidia.com/cuda/cuda-programming-guide/03-advanced/multi-gpu-systems.html
- [13]: CUDA Programming Guide, 3.5 A Tour of CUDA Features(特性总览)— https://docs.nvidia.com/cuda/cuda-programming-guide/03-advanced/feature-survey.html
- [14]: CUDA Programming Guide, 4.2 CUDA Graphs — https://docs.nvidia.com/cuda/cuda-programming-guide/04-special-topics/cuda-graphs.html
- [15]: CUDA Programming Guide, 4.4 Cooperative Groups — https://docs.nvidia.com/cuda/cuda-programming-guide/04-special-topics/cooperative-groups.html
- [16]: CUDA Programming Guide, 4.5 Programmatic Dependent Launch and Synchronization — https://docs.nvidia.com/cuda/cuda-programming-guide/04-special-topics/programmatic-dependent-launch.html
- [17]: CUDA Programming Guide, 4.9 Asynchronous Barriers — https://docs.nvidia.com/cuda/cuda-programming-guide/04-special-topics/async-barriers.html
- [18]: CUDA Programming Guide, 4.10 Pipelines — https://docs.nvidia.com/cuda/cuda-programming-guide/04-special-topics/pipelines.html
- [19]: CUDA Programming Guide, 4.11 Asynchronous Data Copies(cp.async 与 TMA)— https://docs.nvidia.com/cuda/cuda-programming-guide/04-special-topics/async-copies.html
- [20]: CUDA Programming Guide, 4.13 L2 Cache Control — https://docs.nvidia.com/cuda/cuda-programming-guide/04-special-topics/l2-cache-control.html
- [21]: CUDA Programming Guide, 4.14 Memory Synchronization Domains — https://docs.nvidia.com/cuda/cuda-programming-guide/04-special-topics/memory-sync-domains.html
- [22]: CUDA Programming Guide, 4.16 Virtual Memory Management — https://docs.nvidia.com/cuda/cuda-programming-guide/04-special-topics/virtual-memory-management.html
- [23]: CUDA Programming Guide, 4.18 CUDA Dynamic Parallelism — https://docs.nvidia.com/cuda/cuda-programming-guide/04-special-topics/dynamic-parallelism.html
- [24]: CUDA Programming Guide, 4.15 Interprocess Communication — https://docs.nvidia.com/cuda/cuda-programming-guide/04-special-topics/inter-process-communication.html
- [25]: CUDA Programming Guide, 4.19 CUDA Interoperability with APIs(图形 API 互操作)— https://docs.nvidia.com/cuda/cuda-programming-guide/04-special-topics/graphics-interop.html
- [26]: CUDA Programming Guide, 5.1 Compute Capabilities — https://docs.nvidia.com/cuda/cuda-programming-guide/05-appendices/compute-capabilities.html
- [27]: CUDA C++ Programming Guide(经典版,含 Performance Guidelines 与传统章节编号)— https://docs.nvidia.com/cuda/cuda-c-programming-guide/index.html
- [28]: CUDA Programming Guide, 5.7 CUDA C++ Memory Model — https://docs.nvidia.com/cuda/cuda-programming-guide/05-appendices/cuda-cpp-memory-model.html
- [29]: CUDA Programming Guide, 5.4 C/C++ Language Extensions(说明符与内建函数)— https://docs.nvidia.com/cuda/cuda-programming-guide/05-appendices/cpp-language-extensions.html
- [30]: CUDA Programming Guide, 5.8 CUDA C++ Execution Model — https://docs.nvidia.com/cuda/cuda-programming-guide/05-appendices/cuda-cpp-execution-model.html
- [31]: nvcc 用户手册 — https://docs.nvidia.com/cuda/cuda-compiler-driver-nvcc/index.html
- [32]: CCCL / libcu++(
cuda::命名空间扩展)— https://nvidia.github.io/cccl/ - [33]: Nsight Compute(kernel 级性能分析)— https://developer.nvidia.com/nsight-compute
- [34]: Nsight Systems(系统级时间线分析)— https://developer.nvidia.com/nsight-systems
- [35]: PTX ISA 文档 — https://docs.nvidia.com/cuda/parallel-thread-execution/index.html