跳转至

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形状

关键演进脉络

  1. 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)。

  2. Ampere (第3代):引入TF32(19位浮点,8位指数+10位尾数+1位符号),在不修改代码的情况下将FP32矩阵乘性能提升到接近FP16水平。同时引入稀疏化支持(2:4结构化稀疏),理论性能再翻倍。A100峰值达312 TFLOPS(TF32)。

  3. 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个线程)协作完成一次矩阵乘加操作:

\[D_{m \times n} = A_{m \times k} \times B_{k \times n} + C_{m \times n}\]

支持的矩阵形状(m×n×k): - 16×16×16(最常用,FP16) - 32×8×16、8×32×16(Volta/Turing) - Ampere起支持更多形状

WMMA编程步骤

Text Only
1. 声明fragment → 2. load_matrix_sync加载 → 3. mma_sync计算 → 4. store_matrix_sync存储

6.1.3 完整代码:WMMA实现16×16矩阵乘法

C++
// 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;
}

编译与运行

Bash
nvcc -arch=sm_70 -o wmma_matmul wmma_matmul.cu
./wmma_matmul

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中的操作可以并发执行(如果硬件资源允许)。

Text Only
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 EngineCompute Engine

Text Only
时间轴 →
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 MemorycudaMallocHostcudaHostAlloc

6.2.3 CUDA Event

Event用于计时Stream间同步

C++
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

C++
// 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;
}

编译与运行

Bash
nvcc -arch=sm_70 -o multi_stream multi_stream_pipeline.cu
./multi_stream

预期输出(GPU不同结果不同,但多Stream应更快):

Text Only
Single Stream: 12.345 ms
Multi Stream (4 streams): 5.678 ms
Speedup: 2.17x

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 = \frac{\text{FLOPs}}{\text{Bytes accessed}} \quad (\text{单位: FLOP/Byte})\]

性能上界

\[\text{Perf} \leq \min(\text{Peak FLOPS},\ \text{Peak BW} \times OI)\]

判断规则: - \(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 缓存命中率 低命中率→内存访问模式需优化
Bash
# 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调用耗时。

Bash
# 采集时间线
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,访问被串行化。

Text Only
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对比

C++
// 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;
}

编译与运行

Bash
nvcc -arch=sm_70 -o bank_conflict bank_conflict_demo.cu
./bank_conflict

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(基准)

C++
// 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

C++
// ================ 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: 共享内存分块加载 + 循环展开

C++
// ================ 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)

C++
// ================ 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预取

C++
// ================ 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 完整测试框架

C++
// (接上面的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;
}

编译与运行

Bash
nvcc -arch=sm_70 -O3 -o gemm_opt gemm_optimization.cu -lcublas
./gemm_opt

预期性能对比(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模板库:

Text Only
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周期)。

Text Only
__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级归约代码

C++
// 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;
}

编译与运行

Bash
nvcc -arch=sm_70 -o warp_reduce warp_reduce.cu
./warp_reduce

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,统一了多种粒度的同步和协作:

C++
#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详解

C++
// 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

C++
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

C++
// 显式预取到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个事务(完美合并);否则可能需要多个事务。

Text Only
✅ 合并访问(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字节边界

C++
// ❌ 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),可配置分配比例:

C++
// 设置偏好
cudaFuncSetAttribute(myKernel,
    cudaFuncAttributePreferredSharedMemoryCarveout,
    cudaSharedmemCarveoutMaxShared);  // 最大化shared memory

// 可选值:
// cudaSharedmemCarveoutDefault (默认平衡)
// cudaSharedmemCarveoutMaxShared (最大化shared memory)
// cudaSharedmemCarveoutMaxL1 (最大化L1 cache)

L2 Cache持久化(Ampere+)

C++
// 设置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的线程走同一分支

C++
// ❌ Bad:相邻线程可能走不同分支
if (threadIdx.x % 2 == 0) { ... } else { ... }

// ✅ Good:以Warp为单位分支
if (threadIdx.x / 32 == 0) { ... } else { ... }
2. 用算术替代分支val = cond ? a : bval = cond * a + (1-cond) * b 3. 数据预排序:让相似数据聚集在同一Warp


题目3:如何选择Block Size?

Q: CUDA kernel的block size如何选择?有哪些考虑因素?

A:

  1. 必须是32的倍数(一个Warp的大小),否则浪费线程
  2. 常用值:128、256、512
  3. Occupancy考虑:使用cudaOccupancyMaxPotentialBlockSize()自动计算
  4. 寄存器压力:block越大 → 每SM上线程越多 → 每线程可用寄存器越少
  5. Shared Memory:block越大 → 共享内存需求可能越大
  6. Grid充分覆盖:确保生成足够多的Block来占满所有SM
C++
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

\[\text{Global Memory访问减少比} = T \text{ 倍}\]

当T=32时,全局内存访问量减少32倍。数据在~19 TB/s的shared memory中被复用,而不是在~2 TB/s的全局内存中重复加载。


题目5:CUDA中如何实现原子操作?有什么性能问题?

Q: CUDA原子操作的原理和性能问题是什么?

A:

原理:原子操作(如atomicAddatomicCAS)保证对同一内存地址的读-改-写操作是不可分割的,不会被其他线程打断。

性能问题: - 原子操作会串行化对同一地址的并发访问 - 大量线程对少量地址做原子操作 → 严重的竞争(contention) - 全局内存原子操作延迟约400+ cycles

优化策略: 1. 分层归约:先Warp内用shuffle归约,再Block内用shared memory原子,最后才用全局原子 2. 使用shared memory原子代替全局原子(Kepler+支持shared memory atomicAdd) 3. 使用privatization:每个线程/Block维护局部计数器,最后合并

C++
// ❌ 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同步模式

C++
// 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

C++
// 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优化思维导图

Text Only
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工具验证优化效果