06-CUDA 高级优化¶
本章是 GPU 并行计算模块的核心进阶篇,覆盖 Tensor Core 编程、 Stream 异步执行、性能分析方法论、高性能 GEMM 五步优化、 Warp 级原语、内存优化进阶等 NVIDIA/字节 AML 面试高频考点。所有代码均为完整可编译的 CUDA C++,附 nvcc 编译命令。
6.1 Tensor Core 编程¶
6.1.1 Tensor Core 硬件架构演进¶
Tensor Core 是 NVIDIA 从 Volta 架构开始引入的专用矩阵运算单元,能够在单个时钟周期内完成小型矩阵的乘加( MMA )运算。
| 架构 | 代表 GPU | Tensor Core 代次 | 支持精度 | 单次 MMA 操作 |
|---|---|---|---|---|
| Volta (2017) | V100 | 第 1 代 | FP16×FP16+FP32 | 4×4×4 MMA |
| Turing (2018) | RTX 2080 Ti | 第 2 代 | +INT8, INT4, INT1 | 同上 |
| Ampere (2020) | A100 | 第 3 代 | +TF32, BF16, FP64 | 扩展 MMA 形状 |
| Hopper (2022) | H100 | 第 4 代 | +FP8 (E4M3/E5M2) | wgmma 异步 MMA |
| Blackwell (2024) | B200 | 第 5 代 | +FP4, 微缩 Tensor Core | 更大 MMA 形状 |
关键演进脉络:
-
Volta (第 1 代):每个 SM 包含 8 个 Tensor Core ,每个 Tensor Core 每周期执行一个 \(D = A \times B + C\) 操作,其中 A 、 B 为 FP16 的 4×4 矩阵, C 、 D 为 FP16 或 FP32 的 4×4 矩阵。 V100 的峰值 Tensor TFLOPS ≈ 125 TFLOPS ( FP16 )。
-
Ampere (第 3 代):引入 TF32 ( 19 位浮点, 8 位指数+10 位尾数+1 位符号),在不修改代码的情况下将 FP32 矩阵乘性能提升到接近 FP16 水平。同时引入稀疏化支持( 2:4 结构化稀疏),理论性能再翻倍。 A100 峰值达 312 TFLOPS ( TF32 )。
-
Hopper (第 4 代):引入Tensor Memory Accelerator (TMA),支持异步数据搬运;引入
wgmma指令,支持跨 Warp Group 的 128×128×16 操作;新增 FP8 精度, H100 峰值近 2000 TFLOPS ( FP8 稀疏)。同时引入Thread Block Cluster和分布式共享内存。
6.1.2 WMMA API 编程¶
CUDA 提供nvcuda::wmma API ( Warp-level Matrix Multiply-Accumulate ),让开发者在 Warp 级别操作 Tensor Core 。一个 Warp ( 32 个线程)协作完成一次矩阵乘加操作:
支持的矩阵形状( m×n×k ): - 16×16×16(最常用, FP16 ) - 32×8×16 、 8×32×16 ( Volta/Turing ) - Ampere 起支持更多形状
WMMA 编程步骤:
6.1.3 完整代码: WMMA 实现 16×16 矩阵乘法¶
// file: wmma_matmul.cu
// 使用WMMA API实现16x16矩阵乘法:D = A * B + C
// 编译: nvcc -arch=sm_70 -o wmma_matmul wmma_matmul.cu
// (sm_70=Volta, sm_80=Ampere, sm_90=Hopper)
#include <cuda_runtime.h>
#include <mma.h> // WMMA头文件
#include <cuda_fp16.h> // half精度
#include <cstdio>
#include <cstdlib>
#include <cmath>
using namespace nvcuda;
// ---- 矩阵维度:必须满足Tensor Core对齐要求 ----
constexpr int M = 16;
constexpr int N = 16;
constexpr int K = 16;
// ---- WMMA Kernel ----
__global__ void wmma_matmul_kernel(const half *A, const half *B,
float *C, float *D,
int m, int n, int k) {
// 声明fragment
// A: 行主序(row_major),B: 列主序(col_major) —— WMMA要求
wmma::fragment<wmma::matrix_a, M, N, K, half, wmma::row_major> a_frag;
wmma::fragment<wmma::matrix_b, M, N, K, half, wmma::col_major> b_frag;
wmma::fragment<wmma::accumulator, M, N, K, float> acc_frag;
wmma::fragment<wmma::accumulator, M, N, K, float> c_frag;
// 加载A(m×k,行主序,leading dimension = k)
wmma::load_matrix_sync(a_frag, A, K);
// 加载B(k×n,列主序,leading dimension = k)
wmma::load_matrix_sync(b_frag, B, K);
// 加载偏置/累加器C(m×n,行主序,leading dimension = n)
wmma::load_matrix_sync(c_frag, C, N, wmma::mem_row_major);
// 执行MMA:acc = A * B + C
wmma::mma_sync(acc_frag, a_frag, b_frag, c_frag);
// 存储结果D(m×n,行主序)
wmma::store_matrix_sync(D, acc_frag, N, wmma::mem_row_major);
}
// ---- CPU参考实现 ----
void cpu_matmul(const float *A, const float *B, const float *C,
float *D, int m, int n, int k) {
for (int i = 0; i < m; ++i)
for (int j = 0; j < n; ++j) {
float sum = C[i * n + j];
for (int p = 0; p < k; ++p)
sum += A[i * k + p] * B[p * n + j];
D[i * n + j] = sum;
}
}
int main() {
// ---- 主机端数据准备 ----
float h_A_fp32[M * K], h_B_fp32[K * N];
float h_C[M * N], h_D_gpu[M * N], h_D_cpu[M * N];
srand(42);
for (int i = 0; i < M * K; ++i) h_A_fp32[i] = (float)(rand() % 5);
for (int i = 0; i < K * N; ++i) h_B_fp32[i] = (float)(rand() % 5);
for (int i = 0; i < M * N; ++i) h_C[i] = 0.0f; // C初始化为0
// 转FP16(用于GPU Tensor Core输入)
half h_A_fp16[M * K], h_B_fp16[K * N];
for (int i = 0; i < M * K; ++i) h_A_fp16[i] = __float2half(h_A_fp32[i]);
// 注意:B需要转为列主序存储给WMMA
for (int i = 0; i < K; ++i)
for (int j = 0; j < N; ++j)
h_B_fp16[j * K + i] = __float2half(h_B_fp32[i * N + j]); // 转置
// ---- 设备端内存分配 ----
half *d_A, *d_B;
float *d_C, *d_D;
cudaMalloc(&d_A, M * K * sizeof(half));
cudaMalloc(&d_B, K * N * sizeof(half));
cudaMalloc(&d_C, M * N * sizeof(float));
cudaMalloc(&d_D, M * N * sizeof(float));
cudaMemcpy(d_A, h_A_fp16, M * K * sizeof(half), cudaMemcpyHostToDevice);
cudaMemcpy(d_B, h_B_fp16, K * N * sizeof(half), cudaMemcpyHostToDevice);
cudaMemcpy(d_C, h_C, M * N * sizeof(float), cudaMemcpyHostToDevice);
// ---- 启动Kernel(必须一个完整Warp = 32线程)----
wmma_matmul_kernel<<<1, 32>>>(d_A, d_B, d_C, d_D, M, N, K);
cudaDeviceSynchronize();
cudaMemcpy(h_D_gpu, d_D, M * N * sizeof(float), cudaMemcpyDeviceToHost);
// ---- CPU验证 ----
cpu_matmul(h_A_fp32, h_B_fp32, h_C, h_D_cpu, M, N, K);
float max_err = 0.0f;
for (int i = 0; i < M * N; ++i) {
float err = fabsf(h_D_gpu[i] - h_D_cpu[i]);
if (err > max_err) max_err = err;
}
printf("Max error between GPU(Tensor Core) and CPU: %f\n", max_err);
if (max_err < 1e-2f)
printf("PASSED! Tensor Core result matches CPU.\n");
else
printf("FAILED! Max error too large.\n");
// ---- 打印部分结果 ----
printf("\nGPU result (first 4x4):\n");
for (int i = 0; i < 4; ++i) {
for (int j = 0; j < 4; ++j)
printf("%8.1f", h_D_gpu[i * N + j]);
printf("\n");
}
cudaFree(d_A); cudaFree(d_B); cudaFree(d_C); cudaFree(d_D);
return 0;
}
编译与运行:
6.1.4 Tensor Core 使用条件与注意事项¶
| 条件 | 要求 |
|---|---|
| 矩阵尺寸 | 必须为 WMMA 支持的形状(如 16×16×16 ) |
| 数据精度 | 输入: FP16/BF16/TF32/INT8 等;累加: FP32/INT32 |
| 内存对齐 | 矩阵首地址需要 256 字节对齐(性能最优) |
| Leading Dimension | 必须为 16 的倍数 |
| Warp 级操作 | 必须 32 线程一起调用,不能有线程分支 |
| 架构要求 | 最低 Volta (sm_70) |
6.1.5 面试考点¶
Q1: Tensor Core 与普通 CUDA Core 有什么区别? A: CUDA Core 每周期执行一次标量 FMA (\(a \times b + c\)); Tensor Core 每周期执行一次矩阵 MMA (如 4×4×4 的 FP16 矩阵乘+FP32 累加),吞吐量高一个数量级。 Tensor Core 是专用硬件单元,需要使用 WMMA/MMA PTX 指令才能触发。
Q2: WMMA 中 fragment 的含义是什么? A: fragment 是分布在一个 Warp 的 32 个线程中的矩阵数据抽象。一个 16×16 的矩阵被拆分到 32 个线程的寄存器中,每个线程持有若干元素,具体映射由硬件决定(对程序员透明)。
Q3: TF32 是什么?为什么 Ampere 引入它? A: TF32 使用 8 位指数(与 FP32 相同范围)+10 位尾数(与 FP16 相同精度),总计 19 位。它让 FP32 输入的矩阵乘无需手动转 FP16 即可利用 Tensor Core ,在保持 FP32 训练逻辑不变的前提下获得巨大加速。 cuBLAS 在 Ampere 上默认开启 TF32 用于 GEMM 。
6.2 CUDA Stream 与异步执行¶
6.2.1 Stream 概念¶
CUDA Stream 是 GPU 上操作的有序队列。同一 Stream 内的操作按 FIFO 顺序执行;不同 Stream 中的操作可以并发执行(如果硬件资源允许)。
Stream 0 (默认): [H2D_A] → [Kernel_A] → [D2H_A]
Stream 1: [H2D_B] → [Kernel_B] → [D2H_B]
↑ 不同Stream之间可重叠
默认 Stream ( Stream 0 / Legacy Default Stream ): - 所有未指定 Stream 的 CUDA 操作进入默认 Stream - 默认 Stream 会与其它 Stream隐式同步(除非使用--default-stream per-thread编译选项) - 这是常见的性能陷阱!
6.2.2 多 Stream 并发原理¶
现代 GPU 有独立的Copy Engine和Compute Engine:
时间轴 →
Copy Engine: [H2D_0][H2D_1][D2H_0][H2D_2][D2H_1][D2H_2]
Compute Engine: [Ker_0][Ker_1] [Ker_2]
↑ 计算与传输重叠 = Pipeline
要实现重叠,必须满足: 1. 使用不同的 Stream 2. 使用异步 API(如cudaMemcpyAsync) 3. Host 内存使用 Pinned Memory(cudaMallocHost或cudaHostAlloc)
6.2.3 CUDA Event¶
Event 用于计时和Stream 间同步:
cudaEvent_t start, stop;
cudaEventCreate(&start);
cudaEventCreate(&stop);
cudaEventRecord(start, stream);
myKernel<<<grid, block, 0, stream>>>(...);
cudaEventRecord(stop, stream);
cudaEventSynchronize(stop);
float ms = 0;
cudaEventElapsedTime(&ms, start, stop); // 精确GPU计时
6.2.4 完整代码:多 Stream Pipeline¶
// file: multi_stream_pipeline.cu
// 演示多Stream实现H2D + Compute + D2H三阶段Pipeline重叠
// 编译: nvcc -arch=sm_70 -o multi_stream multi_stream_pipeline.cu
#include <cuda_runtime.h>
#include <cstdio>
#include <cstdlib>
// 简单Kernel:向量逐元素乘以常数
__global__ void scale_kernel(float *data, float factor, int n) {
int idx = blockIdx.x * blockDim.x + threadIdx.x;
if (idx < n) {
// 模拟较重计算
float val = data[idx];
for (int i = 0; i < 100; ++i)
val = val * factor + 0.001f;
data[idx] = val;
}
}
int main() {
const int NUM_STREAMS = 4;
const int CHUNK_SIZE = 1 << 20; // 每个chunk 1M个float ≈ 4MB
const int TOTAL_SIZE = NUM_STREAMS * CHUNK_SIZE;
const float FACTOR = 1.0001f;
// ---- 分配Pinned Host内存(必须!否则无法真正异步)----
float *h_data;
cudaMallocHost(&h_data, TOTAL_SIZE * sizeof(float));
// 初始化数据
for (int i = 0; i < TOTAL_SIZE; ++i)
h_data[i] = (float)(i % 1000) * 0.001f;
// ---- 分配Device内存 ----
float *d_data;
cudaMalloc(&d_data, TOTAL_SIZE * sizeof(float));
// ---- 创建Stream和Event ----
cudaStream_t streams[NUM_STREAMS];
for (int i = 0; i < NUM_STREAMS; ++i)
cudaStreamCreate(&streams[i]);
cudaEvent_t start_event, stop_event;
cudaEventCreate(&start_event);
cudaEventCreate(&stop_event);
// ======== 方式1: 单Stream(无重叠,作为基准)========
cudaEventRecord(start_event, 0);
cudaMemcpy(d_data, h_data, TOTAL_SIZE * sizeof(float),
cudaMemcpyHostToDevice);
int threads = 256;
int blocks = (TOTAL_SIZE + threads - 1) / threads;
scale_kernel<<<blocks, threads>>>(d_data, FACTOR, TOTAL_SIZE);
cudaMemcpy(h_data, d_data, TOTAL_SIZE * sizeof(float),
cudaMemcpyDeviceToHost);
cudaEventRecord(stop_event, 0);
cudaEventSynchronize(stop_event);
float single_ms = 0;
cudaEventElapsedTime(&single_ms, start_event, stop_event);
printf("Single Stream: %.3f ms\n", single_ms);
// 重新初始化
for (int i = 0; i < TOTAL_SIZE; ++i)
h_data[i] = (float)(i % 1000) * 0.001f;
// ======== 方式2: 多Stream Pipeline(H2D+Compute+D2H重叠)========
cudaEventRecord(start_event, 0);
int chunk_blocks = (CHUNK_SIZE + threads - 1) / threads;
for (int i = 0; i < NUM_STREAMS; ++i) {
int offset = i * CHUNK_SIZE;
size_t bytes = CHUNK_SIZE * sizeof(float);
// 阶段1: 异步H2D
cudaMemcpyAsync(d_data + offset, h_data + offset, bytes,
cudaMemcpyHostToDevice, streams[i]);
// 阶段2: 异步Kernel执行
scale_kernel<<<chunk_blocks, threads, 0, streams[i]>>>(
d_data + offset, FACTOR, CHUNK_SIZE);
// 阶段3: 异步D2H
cudaMemcpyAsync(h_data + offset, d_data + offset, bytes,
cudaMemcpyDeviceToHost, streams[i]);
}
cudaEventRecord(stop_event, 0);
// 等待所有Stream完成
for (int i = 0; i < NUM_STREAMS; ++i)
cudaStreamSynchronize(streams[i]);
cudaEventSynchronize(stop_event);
float multi_ms = 0;
cudaEventElapsedTime(&multi_ms, start_event, stop_event);
printf("Multi Stream (%d streams): %.3f ms\n", NUM_STREAMS, multi_ms);
printf("Speedup: %.2fx\n", single_ms / multi_ms);
// ---- 清理 ----
for (int i = 0; i < NUM_STREAMS; ++i)
cudaStreamDestroy(streams[i]);
cudaEventDestroy(start_event);
cudaEventDestroy(stop_event);
cudaFree(d_data);
cudaFreeHost(h_data);
return 0;
}
编译与运行:
预期输出( GPU 不同结果不同,但多 Stream 应更快):
6.2.5 面试考点¶
Q1: 为什么
cudaMemcpy(同步)不能实现传输与计算重叠? A:cudaMemcpy是阻塞调用, CPU 会等待传输完成才返回,所有操作变成串行。必须使用cudaMemcpyAsync+ Pinned Memory + 非默认 Stream 才能实现真正的异步重叠。Q2: 默认 Stream 为什么是性能陷阱? A: Legacy 默认 Stream 会与所有其它 Stream 隐式同步——当默认 Stream 中有操作时,其它 Stream 必须等待它完成。解决方案:① 总是创建显式 Stream ;② 使用
--default-stream per-thread编译,使每个 CPU 线程有独立默认 Stream 。Q3: 多 Stream 一定比单 Stream 快吗? A: 不一定。如果 Kernel 已经完全占满 GPU 算力( compute-bound ),多 Stream 无法并行更多 Kernel 。多 Stream 主要收益在于重叠数据传输和计算。小数据量时 Stream 创建/调度的开销可能抵消收益。
6.3 性能分析方法论¶
6.3.1 Roofline Model¶
Roofline Model 是判断 Kernel 性能瓶颈的核心工具。它可视化了算力( FLOPS )和带宽( Bytes/s )之间的关系。
计算强度( Operational Intensity ):
性能上界:
判断规则: - \(OI < OI_{ridge}\)(拐点):Memory-bound,优化内存访问(合并、缓存、 prefetch ) - \(OI > OI_{ridge}\):Compute-bound,优化计算( Tensor Core 、指令级并行)
常见 Kernel 的计算强度:
| Kernel | OI (FLOP/Byte) | 瓶颈类型 |
|---|---|---|
| 向量加法 | 0.25 | Memory-bound |
| 矩阵乘 GEMM | ~O(N) | Compute-bound (大 N 时) |
| ReLU 激活 | 0.125 | Memory-bound |
| BatchNorm | ~1 | Memory-bound |
| Attention | ~O(d) | 取决于序列长度 |
6.3.2 Nsight Compute 核心指标¶
Nsight Compute (ncu)是 Kernel 级性能分析工具,核心指标:
| 指标 | 含义 | 关注点 |
|---|---|---|
| Occupancy | SM 上活跃 Warp 数 / 最大 Warp 数 | >50%为佳,低则检查寄存器/shared mem 使用 |
| Memory Throughput | 实际带宽 / 峰值带宽 | Memory-bound kernel 应>60% |
| Compute Throughput | 实际 FLOPS / 峰值 FLOPS | Compute-bound kernel 应>60% |
| Warp Stall Reasons | Warp 等待原因分布 | 找出主要 stall 原因 |
| L1/L2 Hit Rate | 缓存命中率 | 低命中率→内存访问模式需优化 |
# Nsight Compute分析命令
ncu --set full -o profile_result ./my_kernel
ncu --metrics sm__throughput.avg.pct_of_peak_sustained_elapsed \
--metrics dram__throughput.avg.pct_of_peak_sustained_elapsed \
./my_kernel
6.3.3 Nsight Systems 时间线分析¶
Nsight Systems (nsys)是系统级时间线工具,展示 CPU/GPU 活动、 Stream 并发、 API 调用耗时。
# 采集时间线
nsys profile --stats=true -o timeline_report ./my_app
# 在GUI中打开查看
nsys-ui timeline_report.nsys-rep
时间线中需要关注的问题: - GPU 空闲( idle )间隙 → Kernel Launch 延迟 / CPU 端瓶颈 - Stream 之间缺乏重叠 → 重新设计 Pipeline - 频繁的cudaDeviceSynchronize → 消除不必要的同步
6.3.4 常见瓶颈与优化¶
Bank Conflict (共享内存 Bank 冲突)¶
共享内存被划分为 32 个 Bank (每 Bank 4 字节宽度)。当同一 Warp 的多个线程访问同一 Bank 的不同地址时,产生 Bank Conflict ,访问被串行化。
Bank 0 Bank 1 Bank 2 ... Bank 31
| | | |
Thread0 Thread1 Thread2 ... Thread31
✓ 无冲突(每线程访问不同Bank)
Thread0 → Bank 0
Thread1 → Bank 0 ← 2-way bank conflict!
解决方案:在 shared memory 中添加 padding 。
Low Occupancy¶
Occupancy 低意味着 SM 上活跃 Warp 少,无法有效隐藏延迟。
常见原因: - 每线程寄存器使用过多(大于 64-128 个) - 共享内存使用过多 - Block size 选择不当
查看方法:ncu --metrics sm__warps_active.avg.per_cycle_active
Warp Divergence¶
同一 Warp 内线程走不同的 if/else 分支,导致串行执行两个分支。
6.3.5 代码: Naive vs 优化 Kernel 对比¶
// file: bank_conflict_demo.cu
// 演示Bank Conflict影响与padding优化
// 编译: nvcc -arch=sm_70 -o bank_conflict bank_conflict_demo.cu
#include <cuda_runtime.h>
#include <cstdio>
constexpr int TILE_DIM = 32;
constexpr int BLOCK_ROWS = 8;
// ---- Naive转置(有Bank Conflict)----
__global__ void transpose_naive(float *odata, const float *idata,
int width, int height) {
// shared memory: 32×32, 列访问时32线程全部命中同一Bank
__shared__ float tile[TILE_DIM][TILE_DIM];
int xIndex = blockIdx.x * TILE_DIM + threadIdx.x;
int yIndex = blockIdx.y * TILE_DIM + threadIdx.y;
int index_in = xIndex + yIndex * width;
// 加载:行访问,无Bank Conflict
for (int j = 0; j < TILE_DIM; j += BLOCK_ROWS)
tile[threadIdx.y + j][threadIdx.x] = idata[index_in + j * width];
__syncthreads();
// 写出转置位置
xIndex = blockIdx.y * TILE_DIM + threadIdx.x;
yIndex = blockIdx.x * TILE_DIM + threadIdx.y;
// 列访问shared memory → 32-way Bank Conflict!
for (int j = 0; j < TILE_DIM; j += BLOCK_ROWS)
odata[xIndex + (yIndex + j) * height] =
tile[threadIdx.x][threadIdx.y + j];
}
// ---- 优化转置(Padding消除Bank Conflict)----
__global__ void transpose_optimized(float *odata, const float *idata,
int width, int height) {
// +1 padding:错开Bank映射
__shared__ float tile[TILE_DIM][TILE_DIM + 1]; // 关键优化!
int xIndex = blockIdx.x * TILE_DIM + threadIdx.x;
int yIndex = blockIdx.y * TILE_DIM + threadIdx.y;
int index_in = xIndex + yIndex * width;
for (int j = 0; j < TILE_DIM; j += BLOCK_ROWS)
tile[threadIdx.y + j][threadIdx.x] = idata[index_in + j * width];
__syncthreads();
xIndex = blockIdx.y * TILE_DIM + threadIdx.x;
yIndex = blockIdx.x * TILE_DIM + threadIdx.y;
// 列访问时由于+1 padding,相邻线程访问不同Bank → 无冲突!
for (int j = 0; j < TILE_DIM; j += BLOCK_ROWS)
odata[xIndex + (yIndex + j) * height] =
tile[threadIdx.x][threadIdx.y + j];
}
int main() {
const int WIDTH = 4096, HEIGHT = 4096;
size_t bytes = WIDTH * HEIGHT * sizeof(float);
float *h_input = (float *)malloc(bytes);
float *h_output = (float *)malloc(bytes);
for (int i = 0; i < WIDTH * HEIGHT; ++i) h_input[i] = (float)i;
float *d_input, *d_output;
cudaMalloc(&d_input, bytes);
cudaMalloc(&d_output, bytes);
cudaMemcpy(d_input, h_input, bytes, cudaMemcpyHostToDevice);
dim3 grid(WIDTH / TILE_DIM, HEIGHT / TILE_DIM);
dim3 block(TILE_DIM, BLOCK_ROWS);
// 计时Naive版
cudaEvent_t start, stop;
cudaEventCreate(&start);
cudaEventCreate(&stop);
// Warmup
transpose_naive<<<grid, block>>>(d_output, d_input, WIDTH, HEIGHT);
cudaDeviceSynchronize();
cudaEventRecord(start);
for (int i = 0; i < 100; ++i)
transpose_naive<<<grid, block>>>(d_output, d_input, WIDTH, HEIGHT);
cudaEventRecord(stop);
cudaEventSynchronize(stop);
float naive_ms;
cudaEventElapsedTime(&naive_ms, start, stop);
// 计时优化版
cudaEventRecord(start);
for (int i = 0; i < 100; ++i)
transpose_optimized<<<grid, block>>>(d_output, d_input, WIDTH, HEIGHT);
cudaEventRecord(stop);
cudaEventSynchronize(stop);
float opt_ms;
cudaEventElapsedTime(&opt_ms, start, stop);
printf("Naive transpose: %.3f ms (avg over 100 runs)\n", naive_ms / 100);
printf("Optimized transpose: %.3f ms (avg over 100 runs)\n", opt_ms / 100);
printf("Speedup: %.2fx\n", naive_ms / opt_ms);
// 带宽计算(读+写各一次)
float bw_naive = 2.0f * bytes / (naive_ms / 100.0f * 1e-3f) / 1e9f;
float bw_opt = 2.0f * bytes / (opt_ms / 100.0f * 1e-3f) / 1e9f;
printf("Effective bandwidth: naive=%.1f GB/s, optimized=%.1f GB/s\n",
bw_naive, bw_opt);
cudaFree(d_input); cudaFree(d_output);
free(h_input); free(h_output);
cudaEventDestroy(start); cudaEventDestroy(stop);
return 0;
}
编译与运行:
6.3.6 面试考点¶
Q1: 如何判断一个 Kernel 是 compute-bound 还是 memory-bound ? A: 计算 Operational Intensity ( OI = FLOPs / Bytes )。与 GPU 的 ridge point (峰值 FLOPS / 峰值带宽)比较。 OI < ridge point → memory-bound 。也可直接用 Nsight Compute 看 compute throughput 和 memory throughput 哪个更接近峰值。
Q2: 什么是 Bank Conflict ?如何解决? A: 共享内存有 32 个 Bank ,当 Warp 中多个线程访问同一 Bank 的不同地址时,访问被串行化。解决方案:① 在 shared memory 声明时添加 padding (如
[32][33]代替[32][32]);② 调整访问模式使相邻线程访问相邻 Bank 。Q3: Occupancy 越高性能越好吗? A: 不一定。 Occupancy 帮助隐藏延迟,但过高的 Occupancy 可能导致每线程可用寄存器减少,增加 register spill 到 local memory ,反而降低性能。通常 50%以上就足够,关键是找到 Occupancy 和资源使用的平衡点。
6.4 高性能 GEMM 优化之旅¶
GEMM ( General Matrix Multiply )是深度学习的核心算子。本节通过5 步渐进优化,从 Naive 到接近 cuBLAS 水平,每步附完整代码和性能分析。
矩阵乘法定义:\(C_{M \times N} = A_{M \times K} \times B_{K \times N}\)
6.4.1 Step 1: Naive GEMM (基准)¶
// file: gemm_optimization.cu
// 5步GEMM优化全代码
// 编译: nvcc -arch=sm_70 -O3 -o gemm_opt gemm_optimization.cu -lcublas
#include <cuda_runtime.h>
#include <cublas_v2.h>
#include <cstdio>
#include <cstdlib>
#include <cmath>
// ================ Step 1: Naive GEMM ================
// 每个线程计算C的一个元素,直接从全局内存读A和B
// 性能瓶颈:全局内存访问被大量重复(A的每行被读N次,B的每列被读M次)
__global__ void gemm_naive(const float *A, const float *B, float *C,
int M, int N, int K) {
int row = blockIdx.y * blockDim.y + threadIdx.y;
int col = blockIdx.x * blockDim.x + threadIdx.x;
if (row < M && col < N) {
float sum = 0.0f;
for (int k = 0; k < K; ++k) {
sum += A[row * K + k] * B[k * N + col];
}
C[row * N + col] = sum;
}
}
// 性能:~300 GFLOPS (A100),约峰值的1.5%
分析: - 计算量:\(2MNK\) FLOPs - 全局内存访问量:每个线程读 A 的一行( K 个 float )和 B 的一列( K 个 float )= \(2K \times 4\) Bytes - A 的每一行被 N 个线程重复读取 → 巨大的全局内存带宽浪费
6.4.2 Step 2: Shared Memory Tiling¶
// ================ Step 2: Shared Memory Tiling ================
// 将矩阵分块(Tile),用shared memory缓存,减少全局内存访问
constexpr int TILE_SIZE = 32;
__global__ void gemm_shared_tiling(const float *A, const float *B, float *C,
int M, int N, int K) {
__shared__ float As[TILE_SIZE][TILE_SIZE];
__shared__ float Bs[TILE_SIZE][TILE_SIZE];
int row = blockIdx.y * TILE_SIZE + threadIdx.y;
int col = blockIdx.x * TILE_SIZE + threadIdx.x;
float sum = 0.0f;
// 沿K维度分tile迭代
for (int t = 0; t < (K + TILE_SIZE - 1) / TILE_SIZE; ++t) {
// 协作加载A的tile到shared memory
int a_col = t * TILE_SIZE + threadIdx.x;
if (row < M && a_col < K)
As[threadIdx.y][threadIdx.x] = A[row * K + a_col];
else
As[threadIdx.y][threadIdx.x] = 0.0f;
// 协作加载B的tile到shared memory
int b_row = t * TILE_SIZE + threadIdx.y;
if (b_row < K && col < N)
Bs[threadIdx.y][threadIdx.x] = B[b_row * N + col];
else
Bs[threadIdx.y][threadIdx.x] = 0.0f;
__syncthreads();
// 在shared memory中完成tile内的乘加
for (int k = 0; k < TILE_SIZE; ++k)
sum += As[threadIdx.y][k] * Bs[k][threadIdx.x];
__syncthreads();
}
if (row < M && col < N)
C[row * N + col] = sum;
}
// 性能:~1500 GFLOPS(A100),约5x提速
为什么 Tiling 有效?
- 全局内存访问量从 \(O(MNK)\) 降为 \(O(MNK / T)\),其中 T 是 tile 大小
- Shared memory 带宽约 19 TB/s ( A100 ),全局内存仅 2 TB/s → 数据复用在快速存储中完成
- 每个数据从全局内存只加载一次到 shared memory ,被 tile 内所有线程共享
6.4.3 Step 3: 共享内存分块加载 + 循环展开¶
// ================ Step 3: 共享内存分块加载 + 循环展开 ================
// 在 Step 2 基础上增加 #pragma unroll 循环展开,提高指令级并行度
constexpr int TILE3 = 32;
constexpr int FLOAT4_COUNT = TILE3 / 4; // 每行8个float4
__global__ void gemm_vectorized(const float *A, const float *B, float *C,
int M, int N, int K) {
__shared__ float As[TILE3][TILE3];
__shared__ float Bs[TILE3][TILE3];
int row = blockIdx.y * TILE3 + threadIdx.y;
int col = blockIdx.x * TILE3 + threadIdx.x;
float sum = 0.0f;
for (int t = 0; t < (K + TILE3 - 1) / TILE3; ++t) {
int a_col = t * TILE3 + threadIdx.x;
if (row < M && a_col < K)
As[threadIdx.y][threadIdx.x] = A[row * K + a_col];
else
As[threadIdx.y][threadIdx.x] = 0.0f;
// B的列方向是连续的 → 可以用float4加载
int b_row = t * TILE3 + threadIdx.y;
if (b_row < K && col < N)
Bs[threadIdx.y][threadIdx.x] = B[b_row * N + col];
else
Bs[threadIdx.y][threadIdx.x] = 0.0f;
__syncthreads();
// 内层循环展开
#pragma unroll
for (int k = 0; k < TILE3; ++k)
sum += As[threadIdx.y][k] * Bs[k][threadIdx.x];
__syncthreads();
}
if (row < M && col < N)
C[row * N + col] = sum;
}
// 性能:~2000 GFLOPS,进一步提升内存带宽利用率
循环展开核心思想: - #pragma unroll 指示编译器完全展开内层循环,消除循环控制开销 - 展开后编译器可更好地调度指令,隐藏访存延迟 - 如需真正的向量化加载( float4 ),需将加载语句改为 reinterpret_cast<float4*> 方式读取,一条指令搬运 128 位数据
6.4.4 Step 4: 寄存器 Tiling ( Register Blocking )¶
// ================ Step 4: Register Tiling ================
// 每个线程计算多个C元素(TM×TN的子矩阵),增加计算/访存比
constexpr int BM = 128; // Block tile M
constexpr int BN = 128; // Block tile N
constexpr int BK = 8; // Block tile K
constexpr int TM = 8; // Thread tile M(每线程计算8行)
constexpr int TN = 8; // Thread tile N(每线程计算8列)
__global__ void gemm_register_tiling(const float *A, const float *B, float *C,
int M, int N, int K) {
// Block内线程数 = (BM/TM) × (BN/TN) = 16 × 16 = 256
const int thread_row = threadIdx.x / (BN / TN); // 0-15
const int thread_col = threadIdx.x % (BN / TN); // 0-15
// Shared memory分配
__shared__ float As[BM][BK]; // 128×8
__shared__ float Bs[BK][BN]; // 8×128
// 指向当前Block负责的C子矩阵
A += blockIdx.y * BM * K;
B += blockIdx.x * BN;
C += blockIdx.y * BM * N + blockIdx.x * BN;
// 寄存器中存储TM×TN的局部结果
float thread_results[TM][TN] = {0.0f};
// 寄存器缓存A和B的列/行片段
float reg_A[TM];
float reg_B[TN];
// 沿K维度分BK大小的tile迭代
for (int bk = 0; bk < K; bk += BK) {
// ---- 协作加载A的tile到shared memory ----
// 每线程加载多个元素以填满 BM×BK 的shared memory
for (int load_offset = 0; load_offset < BM * BK;
load_offset += blockDim.x) {
int idx = load_offset + threadIdx.x;
if (idx < BM * BK) {
int sm_row = idx / BK;
int sm_col = idx % BK;
int global_row = blockIdx.y * BM + sm_row;
int global_col = bk + sm_col;
if (global_row < M && global_col < K)
As[sm_row][sm_col] = A[sm_row * K + bk + sm_col];
else
As[sm_row][sm_col] = 0.0f;
}
}
// ---- 协作加载B的tile到shared memory ----
for (int load_offset = 0; load_offset < BK * BN;
load_offset += blockDim.x) {
int idx = load_offset + threadIdx.x;
if (idx < BK * BN) {
int sm_row = idx / BN;
int sm_col = idx % BN;
int global_row = bk + sm_row;
int global_col = blockIdx.x * BN + sm_col;
if (global_row < K && global_col < N)
Bs[sm_row][sm_col] = B[(bk + sm_row) * N +
blockIdx.x * BN + sm_col];
else
Bs[sm_row][sm_col] = 0.0f;
}
}
__syncthreads();
// ---- 寄存器级计算 ----
for (int dot_idx = 0; dot_idx < BK; ++dot_idx) {
// 加载A的TM个元素到寄存器
for (int i = 0; i < TM; ++i)
reg_A[i] = As[thread_row * TM + i][dot_idx];
// 加载B的TN个元素到寄存器
for (int j = 0; j < TN; ++j)
reg_B[j] = Bs[dot_idx][thread_col * TN + j];
// 外积:TM × TN次FMA
for (int i = 0; i < TM; ++i)
for (int j = 0; j < TN; ++j)
thread_results[i][j] += reg_A[i] * reg_B[j];
}
__syncthreads();
}
// ---- 写回C ----
for (int i = 0; i < TM; ++i)
for (int j = 0; j < TN; ++j) {
int global_row = blockIdx.y * BM + thread_row * TM + i;
int global_col = blockIdx.x * BN + thread_col * TN + j;
if (global_row < M && global_col < N)
C[(thread_row * TM + i) * N +
thread_col * TN + j] = thread_results[i][j];
}
}
// 性能:~6000-8000 GFLOPS(A100),计算/访存比从1提升到TM*TN=64
为什么 Register Tiling 有效?
| 度量 | Step 2 (Shared Tiling) | Step 4 (Register Tiling) |
|---|---|---|
| 每线程计算 C 元素数 | 1 | TM×TN = 64 |
| 计算/Shared 访存比 | 1 FMA / 2 loads | TM×TN FMA / (TM+TN) loads |
| 线程总数 | 多但低效 | 少但高效 |
6.4.5 Step 5: Double Buffering 预取¶
// ================ Step 5: Double Buffering ================
// 使用双缓冲:当前tile在计算时,下一个tile已经在加载
// 隐藏shared memory加载延迟
constexpr int DB_BM = 64;
constexpr int DB_BN = 64;
constexpr int DB_BK = 8;
constexpr int DB_TM = 4;
constexpr int DB_TN = 4;
__global__ void gemm_double_buffer(const float *A, const float *B, float *C,
int M, int N, int K) {
// 双缓冲:两组shared memory交替使用
__shared__ float As[2][DB_BM][DB_BK];
__shared__ float Bs[2][DB_BK][DB_BN];
const int thread_row = threadIdx.x / (DB_BN / DB_TN);
const int thread_col = threadIdx.x % (DB_BN / DB_TN);
float thread_results[DB_TM][DB_TN] = {0.0f};
float reg_A[DB_TM], reg_B[DB_TN];
int num_tiles = (K + DB_BK - 1) / DB_BK;
// ---- 预加载第一个tile到buffer 0 ----
for (int load_offset = 0; load_offset < DB_BM * DB_BK;
load_offset += blockDim.x) {
int idx = load_offset + threadIdx.x;
if (idx < DB_BM * DB_BK) {
int sr = idx / DB_BK, sc = idx % DB_BK;
int gr = blockIdx.y * DB_BM + sr;
int gc = sc;
As[0][sr][sc] = (gr < M && gc < K) ? A[gr * K + gc] : 0.0f;
}
}
for (int load_offset = 0; load_offset < DB_BK * DB_BN;
load_offset += blockDim.x) {
int idx = load_offset + threadIdx.x;
if (idx < DB_BK * DB_BN) {
int sr = idx / DB_BN, sc = idx % DB_BN;
int gr = sr;
int gc = blockIdx.x * DB_BN + sc;
Bs[0][sr][sc] = (gr < K && gc < N) ? B[gr * N + gc] : 0.0f;
}
}
__syncthreads();
// ---- 主循环:计算当前buffer,预取下一个buffer ----
for (int tile = 0; tile < num_tiles; ++tile) {
int cur_buf = tile % 2;
int nxt_buf = 1 - cur_buf;
// 异步预加载下一个tile到nxt_buf
if (tile + 1 < num_tiles) {
int next_bk = (tile + 1) * DB_BK;
for (int load_offset = 0; load_offset < DB_BM * DB_BK;
load_offset += blockDim.x) {
int idx = load_offset + threadIdx.x;
if (idx < DB_BM * DB_BK) {
int sr = idx / DB_BK, sc = idx % DB_BK;
int gr = blockIdx.y * DB_BM + sr;
int gc = next_bk + sc;
As[nxt_buf][sr][sc] = (gr < M && gc < K) ?
A[gr * K + gc] : 0.0f;
}
}
for (int load_offset = 0; load_offset < DB_BK * DB_BN;
load_offset += blockDim.x) {
int idx = load_offset + threadIdx.x;
if (idx < DB_BK * DB_BN) {
int sr = idx / DB_BN, sc = idx % DB_BN;
int gr = next_bk + sr;
int gc = blockIdx.x * DB_BN + sc;
Bs[nxt_buf][sr][sc] = (gr < K && gc < N) ?
B[gr * N + gc] : 0.0f;
}
}
}
// 计算当前buffer
for (int dot_idx = 0; dot_idx < DB_BK; ++dot_idx) {
for (int i = 0; i < DB_TM; ++i)
reg_A[i] = As[cur_buf][thread_row * DB_TM + i][dot_idx];
for (int j = 0; j < DB_TN; ++j)
reg_B[j] = Bs[cur_buf][dot_idx][thread_col * DB_TN + j];
for (int i = 0; i < DB_TM; ++i)
for (int j = 0; j < DB_TN; ++j)
thread_results[i][j] += reg_A[i] * reg_B[j];
}
__syncthreads(); // 确保预加载完成
}
// 写回C
for (int i = 0; i < DB_TM; ++i)
for (int j = 0; j < DB_TN; ++j) {
int gr = blockIdx.y * DB_BM + thread_row * DB_TM + i;
int gc = blockIdx.x * DB_BN + thread_col * DB_TN + j;
if (gr < M && gc < N)
C[gr * N + gc] = thread_results[i][j];
}
}
6.4.6 完整测试框架¶
// (接上面的kernel定义,下面是main函数和验证代码)
void init_matrix(float *mat, int rows, int cols) {
for (int i = 0; i < rows * cols; ++i)
mat[i] = (float)(rand() % 10) * 0.1f;
}
void cpu_gemm(const float *A, const float *B, float *C,
int M, int N, int K) {
for (int i = 0; i < M; ++i)
for (int j = 0; j < N; ++j) {
float sum = 0.0f;
for (int k = 0; k < K; ++k)
sum += A[i * K + k] * B[k * N + j];
C[i * N + j] = sum;
}
}
float benchmark_kernel(void (*launch_fn)(const float*, const float*, float*,
float*, int, int, int),
const float *d_A, const float *d_B, float *d_C,
float *d_ref, int M, int N, int K, const char *name) {
cudaEvent_t start, stop;
cudaEventCreate(&start);
cudaEventCreate(&stop);
// Warmup
launch_fn(d_A, d_B, d_C, d_ref, M, N, K);
cudaDeviceSynchronize();
// Benchmark
int iters = 20;
cudaEventRecord(start);
for (int i = 0; i < iters; ++i)
launch_fn(d_A, d_B, d_C, d_ref, M, N, K);
cudaEventRecord(stop);
cudaEventSynchronize(stop);
float ms;
cudaEventElapsedTime(&ms, start, stop);
ms /= iters;
float gflops = 2.0f * M * N * K / (ms * 1e-3f) / 1e9f;
printf("%-25s: %8.3f ms | %8.1f GFLOPS\n", name, ms, gflops);
cudaEventDestroy(start);
cudaEventDestroy(stop);
return gflops;
}
// 各Kernel的launch wrapper
void launch_naive(const float *A, const float *B, float *C,
float *ref, int M, int N, int K) {
dim3 block(32, 32);
dim3 grid((N + 31) / 32, (M + 31) / 32);
gemm_naive<<<grid, block>>>(A, B, C, M, N, K);
}
void launch_shared(const float *A, const float *B, float *C,
float *ref, int M, int N, int K) {
dim3 block(TILE_SIZE, TILE_SIZE);
dim3 grid((N + TILE_SIZE - 1) / TILE_SIZE, (M + TILE_SIZE - 1) / TILE_SIZE);
gemm_shared_tiling<<<grid, block>>>(A, B, C, M, N, K);
}
void launch_vectorized(const float *A, const float *B, float *C,
float *ref, int M, int N, int K) {
dim3 block(TILE3, TILE3);
dim3 grid((N + TILE3 - 1) / TILE3, (M + TILE3 - 1) / TILE3);
gemm_vectorized<<<grid, block>>>(A, B, C, M, N, K);
}
void launch_register_tiling(const float *A, const float *B, float *C,
float *ref, int M, int N, int K) {
dim3 block((BM / TM) * (BN / TN)); // 256 threads
dim3 grid((N + BN - 1) / BN, (M + BM - 1) / BM);
gemm_register_tiling<<<grid, block>>>(A, B, C, M, N, K);
}
void launch_double_buffer(const float *A, const float *B, float *C,
float *ref, int M, int N, int K) {
dim3 block((DB_BM / DB_TM) * (DB_BN / DB_TN)); // 256 threads
dim3 grid((N + DB_BN - 1) / DB_BN, (M + DB_BM - 1) / DB_BM);
gemm_double_buffer<<<grid, block>>>(A, B, C, M, N, K);
}
int main() {
const int M = 2048, N = 2048, K = 2048;
size_t size_A = M * K * sizeof(float);
size_t size_B = K * N * sizeof(float);
size_t size_C = M * N * sizeof(float);
float *h_A = (float *)malloc(size_A);
float *h_B = (float *)malloc(size_B);
float *h_C = (float *)malloc(size_C);
srand(42);
init_matrix(h_A, M, K);
init_matrix(h_B, K, N);
float *d_A, *d_B, *d_C;
cudaMalloc(&d_A, size_A);
cudaMalloc(&d_B, size_B);
cudaMalloc(&d_C, size_C);
cudaMemcpy(d_A, h_A, size_A, cudaMemcpyHostToDevice);
cudaMemcpy(d_B, h_B, size_B, cudaMemcpyHostToDevice);
printf("GEMM Optimization Benchmark (M=%d, N=%d, K=%d)\n", M, N, K);
printf("=============================================\n");
benchmark_kernel(launch_naive, d_A, d_B, d_C, nullptr, M, N, K,
"Step1: Naive");
benchmark_kernel(launch_shared, d_A, d_B, d_C, nullptr, M, N, K,
"Step2: Shared Tiling");
benchmark_kernel(launch_vectorized, d_A, d_B, d_C, nullptr, M, N, K,
"Step3: Vectorized");
benchmark_kernel(launch_register_tiling, d_A, d_B, d_C, nullptr, M, N, K,
"Step4: Register Tiling");
benchmark_kernel(launch_double_buffer, d_A, d_B, d_C, nullptr, M, N, K,
"Step5: Double Buffer");
// cuBLAS作为参考
cublasHandle_t handle;
cublasCreate(&handle);
float alpha = 1.0f, beta = 0.0f;
cudaEvent_t start, stop;
cudaEventCreate(&start);
cudaEventCreate(&stop);
// Warmup
cublasSgemm(handle, CUBLAS_OP_N, CUBLAS_OP_N, N, M, K,
&alpha, d_B, N, d_A, K, &beta, d_C, N);
cudaDeviceSynchronize();
cudaEventRecord(start);
for (int i = 0; i < 20; ++i)
cublasSgemm(handle, CUBLAS_OP_N, CUBLAS_OP_N, N, M, K,
&alpha, d_B, N, d_A, K, &beta, d_C, N);
cudaEventRecord(stop);
cudaEventSynchronize(stop);
float cublas_ms;
cudaEventElapsedTime(&cublas_ms, start, stop);
cublas_ms /= 20;
float cublas_gflops = 2.0f * M * N * K / (cublas_ms * 1e-3f) / 1e9f;
printf("%-25s: %8.3f ms | %8.1f GFLOPS\n", "cuBLAS (reference)", cublas_ms,
cublas_gflops);
cublasDestroy(handle);
cudaFree(d_A); cudaFree(d_B); cudaFree(d_C);
free(h_A); free(h_B); free(h_C);
cudaEventDestroy(start); cudaEventDestroy(stop);
return 0;
}
编译与运行:
预期性能对比( A100 80GB 为例, FP32 SGEMM ):
| Step | 方法 | 典型 GFLOPS | 相对 Naive |
|---|---|---|---|
| 1 | Naive | ~300 | 1x |
| 2 | Shared Memory Tiling | ~1500 | 5x |
| 3 | Vectorized Load | ~2000 | 6.7x |
| 4 | Register Tiling | ~7000 | 23x |
| 5 | Double Buffering | ~8000 | 27x |
| Ref | cuBLAS | ~15000 | 50x |
cuBLAS 还使用了更激进的 autotuning 、汇编级 PTX 指令、 warp-level MMA 等优化。
6.4.7 CUTLASS 库简介¶
CUTLASS( CUDA Templates for Linear Algebra Subroutines )是 NVIDIA 开源的高性能 GEMM 模板库:
CUTLASS架构层次:
├── Epilogue(输出处理:ReLU fuse、bias add)
├── Mainloop(主循环:double buffering、async copy)
├── Warp-level MMA(Tensor Core WMMA/MMA指令)
├── Thread-level Fragment(寄存器tile)
└── Memory TileIterator(全局→共享→寄存器的数据搬运)
核心优势: - 模板化设计:编译期确定 tile 大小、数据类型、布局 - 接近 cuBLAS 性能( 90-99%) - 可自定义 epilogue (如 GEMM + bias + ReLU 融合) - 支持 Tensor Core 各精度( FP16/BF16/TF32/INT8/FP8 )
6.4.8 面试考点¶
Q1: 为什么 Tiling 有效? Tile 大小如何选择? A: Tiling 利用数据局部性( data locality ),将频繁访问的数据放入高速的 shared memory 中复用,减少慢速全局内存的访问次数。 Tile 大小选择需平衡:① 太大→shared memory 不够 / Occupancy 低;② 太小→计算/访存比低、全局内存事务效率低。典型选择: Block tile 64-256 , K tile 4-16 , Thread tile 4-8 。需要根据目标 GPU 的 shared memory 容量和寄存器数量调优。
Q2: Register Tiling 的核心思想是什么? A: 让每个线程负责 C 矩阵的一个 TM×TN 子块而非单个元素。这样在内层 K 循环中,每次从 shared memory 加载 TM+TN 个数据,但执行 TM×TN 次 FMA ,计算/访存比从 2:1 提升到约 TM×TN/(TM+TN),极大提高了计算密度。
Q3: Double Buffering 解决了什么问题? A: 解决了 shared memory 加载与计算之间的串行依赖。使用两组 shared memory buffer ,当 Kernel 在 buffer A 中计算时,异步将下一个 tile 加载到 buffer B ,实现了数据预取( prefetching )与计算的重叠,消除了数据加载的等待时间。
6.5 Warp 级原语¶
6.5.1 Warp Shuffle 指令¶
Warp Shuffle 允许 Warp 内线程直接交换寄存器数据,无需经过共享内存,延迟极低( 1-2 周期)。
__shfl_sync(mask, val, srcLane) // 从srcLane广播
__shfl_up_sync(mask, val, delta) // 从lane-delta获取
__shfl_down_sync(mask, val, delta) // 从lane+delta获取
__shfl_xor_sync(mask, val, laneMask) // 从lane^laneMask获取
参数说明: - mask: 参与线程掩码,通常0xFFFFFFFF(全部 32 线程) - val: 当前线程贡献的值 - srcLane / delta / laneMask: 数据来源的指定方式
6.5.2 Warp 级归约代码¶
// file: warp_reduce.cu
// 使用Warp Shuffle实现高效归约
// 编译: nvcc -arch=sm_70 -o warp_reduce warp_reduce.cu
#include <cuda_runtime.h>
#include <cstdio>
constexpr int WARP_SIZE = 32;
// ---- Warp级求和归约(无共享内存)----
__device__ float warp_reduce_sum(float val) {
// 5步蝶形归约(log2(32) = 5)
for (int offset = WARP_SIZE / 2; offset > 0; offset >>= 1)
val += __shfl_down_sync(0xFFFFFFFF, val, offset);
return val; // 结果在lane 0
}
// ---- Warp级最大值归约 ----
__device__ float warp_reduce_max(float val) {
for (int offset = WARP_SIZE / 2; offset > 0; offset >>= 1)
val = fmaxf(val, __shfl_down_sync(0xFFFFFFFF, val, offset));
return val;
}
// ---- Block级归约:先Warp内归约,再跨Warp归约 ----
__global__ void block_reduce_kernel(const float *input, float *output, int n) {
__shared__ float warp_sums[32]; // 最多32个Warp
int tid = threadIdx.x;
int global_id = blockIdx.x * blockDim.x + threadIdx.x;
// 每线程加载一个元素
float val = (global_id < n) ? input[global_id] : 0.0f;
// 第一步:Warp内归约
int lane = tid % WARP_SIZE;
int warp_id = tid / WARP_SIZE;
val = warp_reduce_sum(val);
// Warp内lane 0将结果写入shared memory
if (lane == 0)
warp_sums[warp_id] = val;
__syncthreads();
// 第二步:第一个Warp读取所有Warp的结果,再做一次归约
int num_warps = (blockDim.x + WARP_SIZE - 1) / WARP_SIZE;
if (warp_id == 0) {
val = (lane < num_warps) ? warp_sums[lane] : 0.0f;
val = warp_reduce_sum(val);
}
// Block的结果在thread 0
if (tid == 0)
output[blockIdx.x] = val;
}
// ---- 验证用CPU归约 ----
float cpu_reduce_sum(const float *data, int n) {
double sum = 0;
for (int i = 0; i < n; ++i) sum += data[i];
return (float)sum;
}
int main() {
const int N = 1 << 20; // 1M elements
size_t bytes = N * sizeof(float);
float *h_input = (float *)malloc(bytes); // 动态内存分配,需手动free释放
for (int i = 0; i < N; ++i) h_input[i] = 1.0f; // 全1,结果应为N
float *d_input, *d_partial;
cudaMalloc(&d_input, bytes);
cudaMemcpy(d_input, h_input, bytes, cudaMemcpyHostToDevice);
int threads = 256;
int blocks = (N + threads - 1) / threads;
cudaMalloc(&d_partial, blocks * sizeof(float));
// 第一轮归约
block_reduce_kernel<<<blocks, threads>>>(d_input, d_partial, N);
// 第二轮归约(把partial results再归约一次)
float *d_result;
cudaMalloc(&d_result, sizeof(float));
block_reduce_kernel<<<1, ((blocks + 31) / 32) * 32>>>(d_partial, d_result,
blocks);
float gpu_result;
cudaMemcpy(&gpu_result, d_result, sizeof(float), cudaMemcpyDeviceToHost);
float cpu_result = cpu_reduce_sum(h_input, N);
printf("GPU reduce sum: %.0f\n", gpu_result);
printf("CPU reduce sum: %.0f\n", cpu_result);
printf("Match: %s\n", (fabsf(gpu_result - cpu_result) < 1.0f) ?
"PASSED" : "FAILED");
cudaFree(d_input); cudaFree(d_partial); cudaFree(d_result);
free(h_input);
return 0;
}
编译与运行:
6.5.3 Warp Shuffle 应用场景¶
| 应用 | 原语 | 说明 |
|---|---|---|
| 归约( sum/max/min ) | __shfl_down_sync | 替代 shared memory 归约,无 bank conflict |
| 广播 | __shfl_sync | 一个 lane 的值广播给整个 Warp |
| 扫描( prefix sum ) | __shfl_up_sync | Warp 级 exclusive/inclusive scan |
| 矩阵转置 | __shfl_xor_sync | Warp 内数据重排 |
| Softmax | __shfl_down_sync + __shfl_sync | 行级 max 和 sum 的 Warp 归约 |
6.5.4 Cooperative Groups 简介¶
Cooperative Groups 是 CUDA 9 引入的灵活线程分组 API ,统一了多种粒度的同步和协作:
#include <cooperative_groups.h> // 引入头文件
namespace cg = cooperative_groups;
__global__ void kernel() {
// 当前线程所在Warp的活跃线程组
auto warp = cg::coalesced_threads();
// 当前Thread Block
auto block = cg::this_thread_block();
// 按大小分割成子组
auto tile = cg::tiled_partition<16>(block); // 16线程的tile
// 子组内归约
float val = tile.thread_rank();
for (int i = tile.size() / 2; i > 0; i >>= 1) {
val += tile.shfl_down(val, i);
}
// 子组内同步
tile.sync();
}
Cooperative Groups 优势: - 比硬编码的__shfl_down_sync + __syncthreads更安全 - 支持 Grid 级同步(cooperative_groups::this_grid().sync()) - 分支代码中自动跟踪活跃线程
6.5.5 面试考点¶
Q1: Warp Shuffle 相比 Shared Memory 归约有什么优势? A: ① 无需分配 shared memory → 不影响 Occupancy ;② 无 Bank Conflict → 无需 padding ;③ 延迟更低( 1-2 周期 vs shared memory 的~20 周期);④ 不需要
__syncthreads()同步。但局限在于只能在 Warp 内( 32 线程)通信。Q2:
__shfl_down_sync的 mask 参数有什么作用? A: mask 是一个 32 位整数,每 bit 代表一个 lane 是否参与操作。设为0xFFFFFFFF表示全部 32 线程参与。在分支代码中,只有 mask 中标记的线程才会执行 shuffle 操作,避免未定义行为。 CUDA 9.0 之后所有 Warp 级原语都要求显式 mask ,以确保线程同步语义正确。
6.6 内存优化进阶¶
6.6.1 Host 内存类型¶
| 类型 | API | 特点 | 适用场景 |
|---|---|---|---|
| Pageable Memory | malloc / new | 默认,可能被 OS 换页 | 一般 CPU 计算 |
| Pinned Memory | cudaMallocHost | 页锁定, DMA 直接访问 | 频繁 H2D/D2H 传输 |
| Unified Memory | cudaMallocManaged | 自动迁移, CPU/GPU 同指针 | 原型开发 |
| 零拷贝(Mapped) | cudaHostAlloc(FLAG_MAPPED) | GPU 直接访问 Host 内存 | 小数据/稀疏访问 |
6.6.2 Pinned Memory 详解¶
// Pinned Memory使用
float *h_pinned;
cudaMallocHost(&h_pinned, size); // 页锁定分配
// 异步传输(必须Pinned Memory + 非默认Stream)
cudaMemcpyAsync(d_data, h_pinned, size,
cudaMemcpyHostToDevice, stream);
// 释放
cudaFreeHost(h_pinned);
性能对比: - Pageable → PCIe 传输需要经过临时 Pinned buffer → 两次拷贝 - Pinned → DMA 引擎直接搬运 → 一次拷贝,带宽提升约 2x - 注意: Pinned Memory 过多会降低系统可用内存,影响 OS 性能
6.6.3 Unified Memory¶
float *data; // 指针:存储变量的内存地址
cudaMallocManaged(&data, N * sizeof(float));
// CPU和GPU使用同一个指针
for (int i = 0; i < N; ++i) data[i] = i; // CPU访问
kernel<<<grid, block>>>(data, N); // GPU访问
cudaDeviceSynchronize();
printf("%f\n", data[0]); // CPU再次访问(自动迁移回来)
cudaFree(data);
Unified Memory 的页迁移机制: - 采用按需页迁移( demand paging ):首次访问时才迁移对应的页面 - Volta+支持硬件页错误处理( page fault ), GPU 可以按需从 CPU 内存拉取页面 - 可用cudaMemPrefetchAsync提示迁移方向,减少 page fault
// 显式预取到GPU
cudaMemPrefetchAsync(data, N * sizeof(float), 0); // deviceId=0
// 预取回CPU
cudaMemPrefetchAsync(data, N * sizeof(float), cudaCpuDeviceId);
6.6.4 合并访问( Coalesced Access )¶
全局内存以128 字节事务为粒度访问。当一个 Warp 的 32 个线程访问连续的 128 字节时,只需1 个事务(完美合并);否则可能需要多个事务。
✅ 合并访问(1个事务):
Thread0→addr[0], Thread1→addr[1], ..., Thread31→addr[31]
❌ 跨步访问(32个事务):
Thread0→addr[0], Thread1→addr[32], ..., Thread31→addr[31*32]
优化建议: - 使用SoA ( Structure of Arrays )而非 AoS ( Array of Structures ) - 矩阵按行存储, Kernel 中线程沿列方向排布 - 使用 padding 对齐到 128 字节边界
// ❌ AoS:非合并(相邻线程访问不连续地址)
struct Particle { float x, y, z, vx, vy, vz; }; // struct结构体:自定义复合数据类型
Particle particles[N];
// Thread i 访问 particles[i].x → 跨步24字节
// ✅ SoA:合并(相邻线程访问连续地址)
float x[N], y[N], z[N], vx[N], vy[N], vz[N];
// Thread i 访问 x[i] → 连续4字节,完美合并
6.6.5 L1/L2 Cache 配置¶
L1 Cache / Shared Memory 分配( Volta+):
Volta 架构开始, L1 Cache 和 Shared Memory 共享同一物理 SRAM (每 SM 128KB-228KB ),可配置分配比例:
// 设置偏好
cudaFuncSetAttribute(myKernel,
cudaFuncAttributePreferredSharedMemoryCarveout,
cudaSharedmemCarveoutMaxShared); // 最大化shared memory
// 可选值:
// cudaSharedmemCarveoutDefault (默认平衡)
// cudaSharedmemCarveoutMaxShared (最大化shared memory)
// cudaSharedmemCarveoutMaxL1 (最大化L1 cache)
L2 Cache 持久化( Ampere+):
// 设置L2 cache中为特定数据预留的窗口
cudaDeviceProp prop;
cudaGetDeviceProperties(&prop, 0);
size_t l2_persist_size = min((size_t)(prop.l2CacheSize * 0.75),
prop.persistingL2CacheMaxSize);
cudaDeviceSetLimit(cudaLimitPersistingL2CacheSize, l2_persist_size);
// 设置访问策略
cudaStreamAttrValue attr;
attr.accessPolicyWindow.base_ptr = d_data;
attr.accessPolicyWindow.num_bytes = data_size;
attr.accessPolicyWindow.hitRatio = 0.6f; // 60%的访问会使用L2
attr.accessPolicyWindow.hitProp = cudaAccessPropertyPersisting;
attr.accessPolicyWindow.missProp = cudaAccessPropertyStreaming;
cudaStreamSetAttribute(stream, cudaStreamAttributeAccessPolicyWindow, &attr);
6.6.6 面试考点¶
Q1: Pinned Memory 为什么能加速 H2D 传输? A: 普通 Pageable Memory 在传输前, CUDA 驱动需要先将数据拷贝到一块临时的 Pinned buffer ,再由 DMA 引擎搬运到 GPU 。 Pinned Memory 跳过了这步中间拷贝, DMA 引擎直接从 Host 物理地址读取数据到 GPU 显存,减少一次拷贝,并且支持异步传输与计算重叠。
Q2: Unified Memory 的缺点是什么? A: ① Page fault 导致的迁移延迟不可预测,影响性能稳定性;② 频繁 CPU/GPU 交替访问导致页面"乒乓"( thrashing ),性能急剧下降;③ 多 GPU 场景下迁移策略复杂;④ 无法精细控制数据布局。生产环境通常用显式的
cudaMemcpy以获得可预测的性能。Q3: 什么是合并访问?举一个非合并访问的例子及优化方法。 A: 合并访问指 Warp 内相邻线程访问全局内存中连续地址,使多个线程的请求合并为最少的 128 字节事务。典型非合并场景:矩阵列访问(跨步=矩阵宽度)。优化方法:① 先加载到 shared memory 再按列读取( tiling );② 转换为 SoA 布局;③ 转置矩阵。
6.7 面试高频题¶
题目 1 : CUDA 中的内存层次及其带宽差异¶
Q: 请描述 CUDA 中的内存层次结构,并比较各层的带宽和延迟。
A:
| 内存层次 | 作用域 | 带宽( A100 ) | 延迟 | 大小 |
|---|---|---|---|---|
| 寄存器 | 线程私有 | ~19 TB/s | 1 cycle | 每 SM 65536×32bit |
| L1 / Shared Memory | Block 内共享 | ~19 TB/s | ~20 cycles | 每 SM 164-228 KB |
| L2 Cache | 全 GPU 共享 | ~5 TB/s | ~200 cycles | 40 MB (A100) |
| Global Memory (HBM) | 全 GPU 共享 | ~2 TB/s | ~400 cycles | 40/80 GB (A100) |
| Host Memory (DRAM) | CPU 域 | ~50 GB/s | ~1000 cycles | 数百 GB |
| PCIe/NVLink | 跨 CPU-GPU | 32/600 GB/s | 微秒级 | N/A |
优化原则:"数据尽量靠近计算"——优先使用寄存器 > shared memory > L2 > global memory 。
题目 2 :解释 Warp Divergence 及其影响¶
Q: 什么是 Warp Divergence ?它如何影响性能?如何避免?
A:
定义:同一 Warp 内的 32 个线程在遇到条件分支( if/else )时,如果部分线程进入 true 分支、部分进入 false 分支, GPU 必须串行执行两个分支——先执行 true 分支( false 线程闲置),再执行 false 分支( true 线程闲置)。
性能影响:最坏情况下,分支代码的执行时间等于所有分支执行时间之和,而非最长分支的时间。
避免方法: 1. 按 Warp 对齐分支条件:确保同一 Warp 的线程走同一分支
// ❌ Bad:相邻线程可能走不同分支
if (threadIdx.x % 2 == 0) { ... } else { ... }
// ✅ Good:以Warp为单位分支
if (threadIdx.x / 32 == 0) { ... } else { ... }
- 用算术替代分支:
val = cond ? a : b→val = cond * a + (1-cond) * b - 数据预排序:让相似数据聚集在同一 Warp
题目 3 :如何选择 Block Size¶
Q: CUDA kernel 的 block size 如何选择?有哪些考虑因素?
A:
- 必须是 32 的倍数(一个 Warp 的大小),否则浪费线程
- 常用值: 128 、 256 、 512
- Occupancy 考虑:使用
cudaOccupancyMaxPotentialBlockSize()自动计算 - 寄存器压力: block 越大 → 每 SM 上线程越多 → 每线程可用寄存器越少
- Shared Memory: block 越大 → 共享内存需求可能越大
- Grid 充分覆盖:确保生成足够多的 Block 来占满所有 SM
int minGridSize, blockSize;
cudaOccupancyMaxPotentialBlockSize(&minGridSize, &blockSize,
myKernel, 0, 0);
int gridSize = (N + blockSize - 1) / blockSize;
myKernel<<<gridSize, blockSize>>>(...);
题目 4 : GEMM 优化为什么用 Tiling¶
Q: 矩阵乘法为什么要做 Tiling ?从计算复杂度和内存访问两个角度分析。
A:
计算复杂度:\(O(M \times N \times K)\),不随 Tiling 改变。
内存访问分析:
对于 M×K 的矩阵 A 与 K×N 的矩阵 B 相乘: - Naive:每个 C 元素需要读 A 的一行和 B 的一列,总全局内存访问量 ≈ \(M \times N \times 2K \times 4\) Bytes - Tile 大小 T 的 Tiling:沿 K 维分为 K/T 个 tile ,每个 Block 加载 T²大小的 A 片和 B 片到 shared memory ,被 T²个线程共享。全局内存访问量降为 \(M \times N \times 2K/T \times 4\) Bytes
当 T=32 时,全局内存访问量减少 32 倍。数据在~19 TB/s 的 shared memory 中被复用,而不是在~2 TB/s 的全局内存中重复加载。
题目 5 : CUDA 中如何实现原子操作?有什么性能问题¶
Q: CUDA 原子操作的原理和性能问题是什么?
A:
原理:原子操作(如atomicAdd、atomicCAS)保证对同一内存地址的读-改-写操作是不可分割的,不会被其他线程打断。
性能问题: - 原子操作会串行化对同一地址的并发访问 - 大量线程对少量地址做原子操作 → 严重的竞争( contention ) - 全局内存原子操作延迟约 400+ cycles
优化策略: 1. 分层归约:先 Warp 内用 shuffle 归约,再 Block 内用 shared memory 原子,最后才用全局原子 2. 使用 shared memory 原子代替全局原子( Kepler+支持 shared memory atomicAdd ) 3. 使用 privatization:每个线程/Block 维护局部计数器,最后合并
// ❌ Bad:所有线程竞争同一个全局计数器
atomicAdd(&global_counter, 1);
// ✅ Good:先Block内归约,再原子加
__shared__ int block_count;
if (threadIdx.x == 0) block_count = 0;
__syncthreads();
atomicAdd(&block_count, local_count); // shared memory atomic
__syncthreads();
if (threadIdx.x == 0)
atomicAdd(&global_counter, block_count); // 每Block只1次全局atomic
题目 6 : Stream 和 Event 的区别与联系¶
Q: 请对比 CUDA Stream 和 Event 的作用。
A:
| 维度 | Stream | Event |
|---|---|---|
| 本质 | GPU 操作的有序队列 | 时间线上的标记点 |
| 主要用途 | 实现并发执行、重叠传输与计算 | 计时、跨 Stream 同步 |
| 同步语义 | cudaStreamSynchronize 等待整个 Stream | cudaEventSynchronize 等待到标记点 |
| 跨 Stream 同步 | 不直接支持 | cudaStreamWaitEvent(streamB, eventA) → B 等 A 到达 event |
| 计时 | 不支持 | cudaEventElapsedTime(start, stop) GPU 端精确计时 |
典型的跨 Stream 同步模式:
// Stream A产生数据,Stream B消费
cudaEventRecord(data_ready, streamA); // 在A中记录事件
cudaStreamWaitEvent(streamB, data_ready); // B等待A的事件
kernelB<<<..., streamB>>>(...); // B中的操作等到A完成才开始
题目 7 :如何优化 Kernel Launch Latency¶
Q: CUDA kernel 的启动延迟是多少?如何减少其影响?
A:
Kernel Launch 延迟:通常 5-10μs (从 CPU 发出到 GPU 开始执行)。虽然单次延迟很小,但大量小 Kernel 连续启动时,延迟会累积成为瓶颈。
优化策略: 1. Kernel 融合( Kernel Fusion ):将多个小 Kernel 合并为一个大 Kernel 2. CUDA Graphs:将一系列 Kernel + memcpy 捕获为 graph ,一次 launch 全部执行 3. 增大每个 Kernel 的工作量:避免启动计算量极小的 Kernel 4. 使用 Graph API:
// CUDA Graph使用示例
cudaGraph_t graph;
cudaGraphExec_t instance;
cudaStreamBeginCapture(stream, cudaStreamCaptureModeGlobal);
kernelA<<<..., stream>>>(...);
kernelB<<<..., stream>>>(...);
kernelC<<<..., stream>>>(...);
cudaStreamEndCapture(stream, &graph);
cudaGraphInstantiate(&instance, graph, nullptr, nullptr, 0);
// 后续多次launch只需一个调用
for (int i = 0; i < 1000; ++i)
cudaGraphLaunch(instance, stream);
题目 8 :比较 cuBLAS 、 CUTLASS 、 Triton 的适用场景¶
Q: 你会在什么场景下选择 cuBLAS 、 CUTLASS 或 Triton ?
A:
| 维度 | cuBLAS | CUTLASS | Triton |
|---|---|---|---|
| 开发者 | NVIDIA (闭源) | NVIDIA (开源 C++模板) | OpenAI (开源 Python DSL ) |
| 性能 | 最优(手写汇编+autotuning ) | 接近 cuBLAS ( 90-99%) | 接近 cuBLAS ( 80-95%) |
| 灵活性 | 低(只有标准 BLAS 接口) | 高(可自定义 epilogue 、数据类型、布局) | 中高( Python 级别自定义) |
| 开发难度 | 最低(一行调用) | 高(需要理解模板元编程) | 中( Python 编写 Kernel ) |
| 典型场景 | 标准 GEMM/GEMV | 自定义融合算子( GEMM+激活+残差) | 快速原型、 FlashAttention 实现 |
| 支持硬件 | 仅 NVIDIA | 仅 NVIDIA | NVIDIA + AMD (实验) |
选择建议: - 标准计算 → cuBLAS - 需要融合算子或自定义精度 → CUTLASS - 快速原型和算法研究 → Triton - 从 PyTorch 中调用 → torch.compile(底层自动选择 Triton 或 cuBLAS )
总结: CUDA 优化思维导图¶
CUDA高级优化
├── 计算优化
│ ├── Tensor Core (WMMA/MMA)
│ ├── Register Tiling (增大计算/访存比)
│ ├── 指令级并行 (ILP, #pragma unroll)
│ └── Warp级原语 (Shuffle归约)
│
├── 内存优化
│ ├── Shared Memory Tiling (数据复用)
│ ├── 合并访问 (Coalesced Access)
│ ├── 向量化加载 (float4/LDG.128)
│ ├── Bank Conflict消除 (Padding)
│ ├── Pinned Memory (加速传输)
│ └── L2 Cache Persistence
│
├── 并发优化
│ ├── Multi-Stream Pipeline
│ ├── CUDA Graphs (减少Launch延迟)
│ └── Double Buffering (预取)
│
├── 分析工具
│ ├── Roofline Model (瓶颈定位)
│ ├── Nsight Compute (Kernel级分析)
│ └── Nsight Systems (系统级时间线)
│
└── 高级库
├── cuBLAS (标准BLAS)
├── CUTLASS (自定义GEMM模板)
└── Triton (Python Kernel DSL)
推荐学习路径: 1. 先理解 Roofline Model ,学会判断 Kernel 瓶颈类型 2. 掌握 Shared Memory Tiling 和合并访问(最基础的优化) 3. 学习 GEMM 五步优化,理解每一步解决什么问题 4. 掌握 Stream/Event 异步编程 5. 深入 Tensor Core 和 Warp Shuffle 6. 使用 Nsight 工具验证优化效果