跳转至

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 { ... }
  1. 用算术替代分支val = cond ? a : bval = cond * a + (1-cond) * b
  2. 数据预排序:让相似数据聚集在同一 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 工具验证优化效果