学习目标
学完本节你将能够:
- 深入应用异步拷贝(
cp.async)实现计算与数据加载的重叠 - 使用 CUDA Graphs 减少 Kernel 启动开销,提升小任务吞吐
- 利用多流(Streams)并发执行 Kernel 与内存拷贝,提高整体利用率
- 针对不同 GPU 架构进行微调,获得最佳性能
- 持续监控 Kernel 性能,并在开发流程中保持性能不退化
1. 异步拷贝与计算重叠的深入应用
1.1 为什么异步拷贝重要
传统 Kernel 中,全局内存到共享内存的加载是同步执行:线程发起内存读取后,必须等待数据返回才能继续执行计算,计算单元会处于空闲等待状态。
Ampere(sm_80)及以上架构引入 cp.async,可以异步将全局内存拷贝到共享内存 ,拷贝操作独立于线程流水线;线程可以同步执行其它计算,实现计算与访存互相重叠,掩盖内存延迟 。在 CUDA C++ 高层封装为 cuda::memcpy_async,配合 cuda::pipeline 流水线组件使用。
硬件要求:计算能力 ≥ 8.0(Ampere)。
1.2 使用 cuda::memcpy_async 和 pipeline
#include <cuda/pipeline>
#include <cooperative_groups.h>
namespace cg = cooperative_groups;
__global__ void asyncCopyKernel(float *globalIn, float *globalOut,
int N, int tileSize) {
__shared__ float sTile[1024];
cg::thread_block block = cg::this_thread_block();
cg::thread_block_tile<32> tile32 = cg::tiled_partition<32>(block);
int tileStart = blockIdx.x * tileSize;
int idx = threadIdx.x;
// 异步加载 tile 数据到共享内存
cuda::memcpy_async(tile32, sTile, globalIn + tileStart,
tileSize * sizeof(float));
cuda::pipeline_commit();
// ----- 此处可以执行独立计算,和拷贝并行 -----
// 等待异步拷贝完成
cuda::pipeline_wait_prior(0);
block.sync();
// 使用共享内存数据做计算
if (tileStart + idx < N) {
globalOut[tileStart + idx] = sTile[idx] * 2.0f;
}
}
1.3 双缓冲(Double Buffering)实现流水线
处理多块 tile 时,使用两套共享内存缓冲区;计算当前缓冲区数据的同时,异步预取下一块数据到另一个缓冲区,最大化重叠度。
__global__ void pipelinedKernel(float *globalIn, float *globalOut,
int N, int tileSize) {
__shared__ float sTile[2][1024]; // 双缓冲
cg::thread_block block = cg::this_thread_block();
cg::thread_block_tile<32> tile32 = cg::tiled_partition<32>(block);
int numTiles = (N + tileSize - 1) / tileSize;
int buf = 0;
// 预加载第一个 tile
int tileStart = blockIdx.x * tileSize;
cuda::memcpy_async(tile32, sTile[buf], globalIn + tileStart,
tileSize * sizeof(float));
cuda::pipeline_commit();
for (int t = 0; t < numTiles; t++) {
// 等待当前buffer加载完成
cuda::pipeline_wait_prior(0);
block.sync();
// 预取下一个 tile 到另一个buffer
int nextTile = t + 1;
if (nextTile < numTiles) {
int nextStart = (blockIdx.x * numTiles + nextTile) * tileSize;
cuda::memcpy_async(tile32, sTile[buf ^ 1], globalIn + nextStart,
tileSize * sizeof(float));
cuda::pipeline_commit();
}
// ----------------
// 此处处理 sTile[buf] 的业务计算
// ----------------
buf ^= 1; // 切换buffer
}
}
注意事项:
- 双缓冲会占用两倍共享内存,会降低SM可驻留Block数量,需要权衡占用率与访存重叠收益。
cuda::pipeline_wait_prior(0)等待最旧的异步任务;cuda::pipeline_commit()提交异步拷贝组。
2. 使用 CUDA Graphs 减少 Kernel 启动开销
2.1 Kernel 启动开销来源
每次调用 kernel<<<>>>(),CPU驱动需要向GPU提交任务,包含系统调用、命令组装,单次启动开销约 5~10μs 。
当程序大量执行细小Kernel(推理、迭代计算),频繁启动开销会成为性能瓶颈。
2.2 CUDA Graphs 基本概念
CUDA Graph 将一系列操作(Kernel调用、内存拷贝、事件)记录为一个有向图;后续可以一次性提交整个图给GPU,消除重复启动开销。
2.3 使用流程
-
创建graph,开启流捕获;
-
在捕获模式下执行Kernel、拷贝;
-
结束捕获,实例化可执行图;
-
循环多次launch执行图;
-
销毁资源。
cudaStream_t stream;
cudaStreamCreate(&stream);cudaGraph_t graph;
cudaGraphCreate(&graph, 0);// 开启流捕获
cudaStreamBeginCapture(stream, cudaStreamCaptureModeGlobal);// 捕获一系列GPU操作
kernel1<<<grid, block, 0, stream>>>(...);
kernel2<<<grid, block, 0, stream>>>(...);
cudaMemcpyAsync(dst, src, size, cudaMemcpyDeviceToDevice, stream);// 结束捕获
cudaStreamEndCapture(stream, &graph);// 实例化为可执行对象
cudaGraphExec_t graphExec;
cudaGraphInstantiate(&graphExec, graph, 0);// 重复执行图
for (int i = 0; i < iterations; i++) {
cudaGraphLaunch(graphExec, stream);
}
cudaStreamSynchronize(stream);// 释放资源
cudaGraphExecDestroy(graphExec);
cudaGraphDestroy(graph);
cudaStreamDestroy(stream);
2.4 适用场景与限制
✅ 适合 :重复执行固定操作序列;深度学习推理、迭代仿真算法。
❌ 限制 :捕获阶段不能有主机同步(cudaDeviceSynchronize);捕获过程不能调用 cudaMalloc/cudaFree;输入输出指针运行时可更新,但不能动态改变Kernel参数结构。
3. 利用多流并发提升整体吞吐
3.1 默认流的局限
默认流(stream 0)中所有操作串行执行;Kernel、H2D/D2H拷贝无法重叠,GPU资源利用率低。
3.2 多流并发
创建多个独立CUDA流,把互相独立任务分派到不同流,可以实现:
-
内存拷贝(H2D/D2H)与Kernel计算互相重叠;
-
多个独立小Kernel并发执行,填满SM空闲资源。
cudaStream_t stream1, stream2;
cudaStreamCreate(&stream1);
cudaStreamCreate(&stream2);// 任务1放到 stream1
cudaMemcpyAsync(d_in1, h_in1, bytes, cudaMemcpyHostToDevice, stream1);
kernel<<<grid, block, 0, stream1>>>(d_in1, d_out1, N);
cudaMemcpyAsync(h_out1, d_out1, bytes, cudaMemcpyDeviceToHost, stream1);// 任务2放到 stream2
cudaMemcpyAsync(d_in2, h_in2, bytes, cudaMemcpyHostToDevice, stream2);
kernel<<<grid, block, 0, stream2>>>(d_in2, d_out2, N);
cudaMemcpyAsync(h_out2, d_out2, bytes, cudaMemcpyDeviceToHost, stream2);cudaStreamSynchronize(stream1);
cudaStreamSynchronize(stream2);
3.3 使用Event处理跨流依赖
不同流之间存在数据依赖时,使用 cudaEvent_t 做同步,不要使用全局同步。
cudaEvent_t event;
cudaEventCreate(&event);
cudaEventRecord(event, stream1);
cudaStreamWaitEvent(stream2, event, 0); // stream2等待stream1的event完成
适用场景:多任务推理、视频处理、多路数据流;单Kernel内存/计算已经打满带宽时,多流收益很小。
4. 针对特定架构的微调技巧
4.1 编译期宏区分架构
使用 __CUDA_ARCH__ 编译期宏,为不同算力生成不同分块参数:
#if __CUDA_ARCH__ >= 800
// Ampere / Hopper:更大tile,支持cp.async
const int tileSize = 64;
#elif __CUDA_ARCH__ >=700
// Volta / Turing:Tensor Core,共享内存更小
const int tileSize = 32;
#else
// 老架构保守配置
const int tileSize = 16;
#endif
4.2 各架构关键特性
| 架构 | 关键特性 | 调优要点 |
|---|---|---|
| Volta(sm70) | 第一代Tensor Core | FP16混合精度;独立线程调度 |
| Turing(sm75) | 消费级Tensor Core | 每SM最大线程仅1024,占用率容易受限 |
| Ampere(sm80) | cp.async、更大共享内存 | 优先异步拷贝流水线;更大tile |
| Hopper(sm90) | TMA、分布式共享内存 | 使用TMA加速矩阵加载;cluster_group |
4.3 运行时动态适配
代码运行时调用 cudaGetDeviceProperties 查询硬件真实参数,动态选择block/tile大小,避免硬编码硬件常量。
5. 持续监控与性能保持
5.1 集成到CI/CD
把性能基准测试纳入自动化CI流程:
- 运行基准程序,记录Kernel耗时、关键ncu指标;
- 设置性能退化阈值;当性能下降超过阈值,触发告警。
5.2 Nsight Compute导出指标脚本
命令行导出csv,用于自动化采集指标:
ncu --metrics gld_efficiency,gst_efficiency,achieved_occupancy \
--export report.csv ./app
5.3 代码评审的性能意识
- 热路径避免不必要全局内存访问、不必要同步;
- 修改数据结构时,警惕破坏合并访问模式;
- 关键Kernel定期重跑profiling,防止迭代开发中性能退化。
6. 代码演示:多流与 CUDA Graph 的简单示例
#include <cstdio>
#include <cuda_runtime.h>
#define CUDA_CHECK(call) \
do { \
cudaError_t err = call; \
if (err != cudaSuccess) { \
fprintf(stderr, "CUDA error at %s:%d: %s\n", __FILE__, __LINE__, cudaGetErrorString(err)); \
exit(EXIT_FAILURE); \
} \
} while (0)
__global__ void dummyKernel(float *out, int N) {
int idx = blockIdx.x * blockDim.x + threadIdx.x;
if (idx < N) out[idx] = idx * 1.0f;
}
int main() {
const int N = 1 << 20;
float *d_out;
CUDA_CHECK(cudaMalloc(&d_out, N * sizeof(float)));
// ========== 多流示例 ==========
cudaStream_t stream1, stream2;
CUDA_CHECK(cudaStreamCreate(&stream1));
CUDA_CHECK(cudaStreamCreate(&stream2));
dummyKernel<<<(N+255)/256, 256, 0, stream1>>>(d_out, N);
dummyKernel<<<(N+255)/256, 256, 0, stream2>>>(d_out, N);
CUDA_CHECK(cudaStreamSynchronize(stream1));
CUDA_CHECK(cudaStreamSynchronize(stream2));
// ========== CUDA Graph示例 ==========
cudaStream_t stream;
CUDA_CHECK(cudaStreamCreate(&stream));
cudaGraph_t graph;
CUDA_CHECK(cudaGraphCreate(&graph, 0));
CUDA_CHECK(cudaStreamBeginCapture(stream, cudaStreamCaptureModeGlobal));
dummyKernel<<<(N+255)/256, 256, 0, stream>>>(d_out, N);
dummyKernel<<<(N+255)/256, 256, 0, stream>>>(d_out, N);
CUDA_CHECK(cudaStreamEndCapture(stream, &graph));
cudaGraphExec_t graphExec;
CUDA_CHECK(cudaGraphInstantiate(&graphExec, graph, 0));
for (int i = 0; i < 10; i++) {
CUDA_CHECK(cudaGraphLaunch(graphExec, stream));
}
CUDA_CHECK(cudaStreamSynchronize(stream));
CUDA_CHECK(cudaGraphExecDestroy(graphExec));
CUDA_CHECK(cudaGraphDestroy(graph));
CUDA_CHECK(cudaStreamDestroy(stream));
CUDA_CHECK(cudaStreamDestroy(stream1));
CUDA_CHECK(cudaStreamDestroy(stream2));
CUDA_CHECK(cudaFree(d_out));
return 0;
}
编译:
nvcc -arch=sm_80 advanced_opt.cu -o advanced_opt
7. 课后练习
练习1:实现双缓冲异步拷贝
使用 cuda::memcpy_async + pipeline 实现向量处理流水线Kernel;对比单缓冲版本性能,分析重叠带来收益。(需要Ampere及以上GPU)
练习2:使用 CUDA Graphs 加速推理
构造由多个小Kernel组成的简易推理流程,分别普通流、CUDA Graph执行;测量启动开销差异。
练习3:多流重叠实验
设计任务包含H2D拷贝、Kernel计算、D2H拷贝;分别单流/多流实现;用Nsight Systems观察时间线重叠,计算加速比。
练习4:架构特定优化
利用__CUDA_ARCH__,为Ampere、Turing设置不同GEMM tile大小,对比性能。
练习5:性能监控脚本
编写shell脚本,调用ncu导出gld_efficiency、gst_efficiency、achieved_occupancy指标输出csv,实现自动化采集。
8. 第5板块总结
恭喜你完成第5板块「Kernel全方位性能优化(调优实战)」的全部内容!
回顾核心知识:
- ✅ 性能调优方法论:测量→假设→优化→验证迭代闭环;Nsight Systems / Compute工具分工。
- ✅ 基于Profiling迭代实战:解读报告,针对内存/依赖/同步/执行单元各类Warp停滞做针对性优化;识别调优陷阱。
- ✅ 典型算子深度优化:GEMM多级优化、im2col卷积、FlashAttention分块重计算思想。
- ✅ 高级调优技巧:
cp.async异步拷贝流水线双缓冲;CUDA Graph消除Kernel启动开销;多流并发;架构适配;自动化性能回归监控。
下一步
下一板块将进入 编译、调用、错误捕获、调试工具(工程基础),学习:
- NVCC编译选项与代码生成深入
- CUDA运行时与驱动API的选择
- 错误处理与调试工具(cuda‑gdb、compute‑sanitizer)
- CMake/Makefile工程构建集成
- 多文件项目与库链接