Skip to content

Latest commit

 

History

History

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 

README.md

第六章:性能方面的考虑

《Programming Massively Parallel Processors》第四版 - 学习笔记与练习

📚 学习内容

本章系统梳理 GPU 性能优化技术:

  • 内存合并(Memory Coalescing)
  • 分区露营(Partition Camping)
  • 指令混合与吞吐量
  • 线程粗化(Thread Coarsening)
  • 资源平衡与占用率权衡
  • Warp 执行效率
  • 数据预取与双缓冲

相关博客笔记第六章:性能方面的考虑


💻 代码实现

Exercise01 - 列主序矩阵乘法(Corner Turning)

本练习实现对应图 6.4 设计的矩阵乘法 kernel,演示角转换(Corner Turning)技术。

代码位置Exercise01/

功能

  • 行主序 Tiled 版本:标准的 Tiled 矩阵乘法实现
  • 列主序 Tiled 版本:使用角转换技术,按列访问 N 矩阵以保持合并访问

核心优化

// 列主序 Tiled 矩阵乘法 Kernel(Corner Turning)
__global__ void TiledMatrixMulKernelColMajor(float* M, float* N, float* P, int m, int n, int o) {
    __shared__ float Mds[TILE_WIDTH][TILE_WIDTH];
    __shared__ float Nds[TILE_WIDTH][TILE_WIDTH];
    
    // ... 省略变量定义 ...
    
    for (int ph = 0; ph < numTiles; ++ph) {
        // 加载 M(行主序,正常访问)
        Mds[ty][tx] = M[row * n + ph * TILE_WIDTH + tx];
        
        // 加载 N(列主序:按列访问以保持合并)
        // N 存储为列主序:N[col * n + row]
        Nds[ty][tx] = N[col * n + (ph * TILE_WIDTH + ty)];
        
        __syncthreads();
        for (int k = 0; k < TILE_WIDTH; ++k) {
            Pvalue += Mds[ty][k] * Nds[k][tx];
        }
        __syncthreads();
    }
}

运行 Exercise01

cd Exercise01
make
make run

预期输出

========================================
  第六章:性能方面的考虑
  Column-Major Matrix Multiplication
========================================

矩阵大小: 4 × 3 × 2

行主序矩阵乘法结果:
列主序矩阵乘法结果:
✅ 两种方法结果一致!

Exercise02 - Thread Coarsening 矩阵乘法

本代码对比三种矩阵乘法实现的性能差异,演示 Thread Coarsening 优化技术。

代码位置Exercise02/

三种实现对比

版本 特点 算术强度
朴素版本 直接全局内存访问 0.25 OP/B
Tiled (32×32) 共享内存优化 8 OP/B
Tiled + Coarsening×4 每线程计算 4 个元素 12.8 OP/B

核心优化

// Thread Coarsening:每个线程计算 COARSE_FACTOR 个输出元素
float Pvalue[COARSE_FACTOR] = {0.0f};  // 4 个累加器

for (int ph = 0; ph < numTiles; ++ph) {
    // M 的 Tile 只加载一次
    Mds[ty][tx] = M[row * n + ph * TILE_WIDTH + tx];
    
    // 但 N 的 Tile 加载 COARSE_FACTOR 次,计算 4 个输出
    for (int c = 0; c < COARSE_FACTOR; ++c) {
        int col = colStart + c * TILE_WIDTH;
        Nds[ty][tx] = N[(ph * TILE_WIDTH + ty) * o + col];
        __syncthreads();
        
        for (int k = 0; k < TILE_WIDTH; ++k) {
            Pvalue[c] += Mds[ty][k] * Nds[k][tx];
        }
        __syncthreads();
    }
}

运行 Exercise02

cd Exercise02
make
make run

预期输出

========================================
  第六章:性能方面的考虑
  Thread Coarsening Matrix Multiplication
========================================

矩阵大小: 1024 × 1024 × 1024
测试迭代次数: 10
Thread Coarsening Factor: 4

=== 正确性验证 ===
✅ 所有方法结果一致!

=== 性能测试 ===
朴素矩阵乘法:          57.273 ms
Tiled 矩阵乘法:        53.636 ms (1.07x vs naive)
Tiled + Coarsening:    48.123 ms (1.19x vs naive, 1.11x vs tiled)

📖 练习题解答

练习 1

题目: 编写一个对应图 6.4 设计的矩阵乘法 kernel 函数。

解答:

图 6.4 展示的是处理列主序(Column-Major)矩阵的 Tiled 矩阵乘法。关键点是使用**角转换(Corner Turning)**技术:不按行访问 N 矩阵(会导致非合并访问),而是按列访问以保持合并访问,然后在共享内存中重新排列数据。

__global__ void TiledMatrixMulKernelColMajorOrder(float *M, float *N, float *P, 
                                                   int m, int n, int o) {
    // 角转换:按列访问 N 矩阵(合并访问)
    // 共享内存用于重排数据
    __shared__ float Mds[TILE_WIDTH][TILE_WIDTH];
    __shared__ float Nds[TILE_WIDTH][TILE_WIDTH];

    int by = blockIdx.y;
    int bx = blockIdx.x;
    int ty = threadIdx.y;
    int tx = threadIdx.x;

    int row = by * TILE_WIDTH + ty;
    int col = bx * TILE_WIDTH + tx;

    float PValue = 0;
    for (int ph = 0; ph < (n + TILE_WIDTH - 1) / TILE_WIDTH; ph++) {
        // 加载 M(行主序,正常访问)
        if (row < m && (ph * TILE_WIDTH + tx) < n)
            Mds[ty][tx] = M[row * n + ph * TILE_WIDTH + tx];
        else
            Mds[ty][tx] = 0.0f;

        // 加载 N(列主序:按列访问以保持合并)
        // N 在内存中是列主序存储:N[col][row] = N[col * n + row]
        if ((ph * TILE_WIDTH + ty) < n && (col < o))
            Nds[ty][tx] = N[col * n + (ph * TILE_WIDTH + ty)];
        else
            Nds[ty][tx] = 0.0f;

        __syncthreads();
        
        for (int k = 0; k < TILE_WIDTH; k++) {
            PValue += Mds[ty][k] * Nds[k][tx];
        }
        __syncthreads();
    }
    
    if (row < m && col < o)
        P[row * o + col] = PValue;
}

关键点

  • N[col * n + (ph * TILE_WIDTH + ty)]:按列访问 N,相邻线程访问相邻内存地址
  • 共享内存 Nds 存储后,计算时按正常方式访问

练习 2

题目: 对于 Tiled 矩阵乘法,在 BLOCK_SIZE 的可能取值范围内,哪些值能完全避免对全局内存的非合并访问?(只考虑方形块)

解答: BLOCK_SIZE 必须是 32 的倍数(32、64 等)

分析

合并访问要求 Warp 中的 32 个连续线程访问连续的内存地址。

  • 如果 BLOCK_SIZE < 32,一个 Warp 会跨越多行
  • 例如 BLOCK_SIZE = 16:Warp 的线程 0-15 访问第 0 行,线程 16-31 访问第 1 行
  • 这导致部分访问是非合并的

验证

BLOCK_SIZE Warp 划分 合并情况
16 1 个 Warp 跨 2 行 ❌ 非合并
32 1 个 Warp = 1 行 ✅ 完全合并
64 2 个 Warp = 2 行 ✅ 完全合并

实践限制

  • 共享内存大小限制通常使 BLOCK_SIZE > 64 不可行
  • 32 是最常用的选择

练习 3

题目: 考虑以下 CUDA kernel,判断每个内存访问是合并的、非合并的、还是不适用合并概念:

__global__ void foo_kernel(float* a, float* b, float* c, float* d, float* e) {
    unsigned int i = blockIdx.x*blockDim.x + threadIdx.x;
    __shared__ float a_s[256];
    __shared__ float bc_s[4*256];
    a_s[threadIdx.x] = a[i];                              // 第 5 行
    for(unsigned int j = 0; j < 4; ++j) {
        bc_s[j*256 + threadIdx.x] = 
            b[j*blockDim.x*gridDim.x + i] + c[i*4 + j];   // 第 7 行
    }
    __syncthreads();
    d[i + 8] = a_s[threadIdx.x];                          // 第 10 行
    e[i*8] = bc_s[threadIdx.x*4];                         // 第 11 行
}

解答:

a. 第 5 行访问数组 a

解答:合并访问

a[blockIdx.x*blockDim.x + threadIdx.x]:相邻线程的 threadIdx.x 连续,访问连续内存地址。


b. 第 5 行访问数组 a_s

解答:不适用

a_s 是共享内存,合并访问概念只适用于全局内存。


c. 第 7 行访问数组 b

解答:合并访问

b[j*blockDim.x*gridDim.x + blockIdx.x*blockDim.x + threadIdx.x]:对于固定的 j,相邻线程访问连续地址。


d. 第 7 行访问数组 c

解答:非合并访问

c[(blockIdx.x*blockDim.x + threadIdx.x)*4 + j]:相邻线程之间跳跃 4 个元素,步长为 4。


e. 第 7 行访问数组 bc_s

解答:不适用

bc_s 是共享内存。


f. 第 10 行访问数组 a_s

解答:不适用

a_s 是共享内存。


g. 第 10 行访问数组 d

解答:合并访问

d[blockIdx.x*blockDim.x + threadIdx.x + 8]:相邻线程访问连续地址(偏移 8 不影响合并)。


h. 第 11 行访问数组 bc_s

解答:不适用

bc_s 是共享内存。


i. 第 11 行访问数组 e

解答:非合并访问

e[(blockIdx.x*blockDim.x + threadIdx.x)*8]:相邻线程之间跳跃 8 个元素,每个线程访问不同的内存段。


练习 4

题目: 计算以下矩阵乘法 kernel 的浮点运算与全局内存访问比(OP/B):

假设矩阵 M 大小为 (m, n),矩阵 N 大小为 (n, o),使用 float32(4 字节)。


a. 第三章的简单 kernel,无优化

解答: 0.25 OP/B

分析

对于结果矩阵的每个元素:

  • 加载 M 的一行:n 次内存读取
  • 加载 N 的一列:n 次内存读取
  • 执行乘法:n 次
  • 执行加法:n-1 ≈ n 次
内存访问: 2n × 4 bytes = 8n bytes
浮点运算: 2n (n 次乘法 + n 次加法)
比值: 2n / 8n = 0.25 OP/B

b. 第五章使用 32×32 Tile 的 kernel

解答: 8 OP/B

分析

使用 Tiling 后,每个线程只需从全局内存加载 n/32 个元素(M 和 N 各 n/32):

  • 同一 Tile 被 32 个线程共享使用
  • 每个元素从全局内存只读 1 次,在共享内存被使用 32 次
内存访问: 2 × (n/32) × 4 bytes = n/4 bytes
浮点运算: 2n
比值: 2n / (n/4) = 8 OP/B

相比朴素版本提升 32 倍


c. 本章使用 32×32 Tile + Thread Coarsening (factor=4) 的 kernel

解答: 12.8 OP/B

分析

Thread Coarsening 让每个线程计算 4 个输出元素:

  • M 的 Tile 被复用 4 次:n/32/4 = n/128 次加载
  • N 的 Tile 正常加载:n/32 次加载
  • 计算量不变:2n 操作
内存访问: (n/128 + n/32) × 4 bytes = (n/128 + 4n/128) × 4 = 5n/32 bytes
浮点运算: 2n
比值: 2n / (5n/32) = 2n × 32 / 5n = 12.8 OP/B

相比 Tiled 版本提升 1.6 倍


🔧 开发环境

  • CUDA Toolkit: 11.0 或更高版本
  • 编译器: GCC 7.5+ / Visual Studio 2019+ + NVCC
  • GPU: 支持 CUDA 的 NVIDIA 显卡(计算能力 3.5+)

💡 学习建议

  1. 优先保证合并访问:检查全局内存访问模式
  2. 使用 Profilerncu 比猜测更有效
  3. 权衡占用率:计算密集型可接受低占用率
  4. Grid-stride loop:灵活处理任意大小数据
  5. 实测验证:不同 GPU 性能特性不同

🚀 下一步

完成本章学习后,继续学习:

  • 第七章:卷积
  • 第八章:模板
  • 第九章:并行直方图

📚 参考资料

学习愉快! 🎓