【infra之路】03_动手写 CUDA Kernel — 向量加法到矩阵乘法

前两课讲了 GPU 线程层次和内存层次,这课全部是代码。我们要写 4 个 kernel,一个比一个快,用 profiler 亲眼看到优化的效果。

环境(WSL2 + RTX 5060 Ti)

1. 安装 CUDA Toolkit

# 在 WSL2 内执行
# 先确认 GPU 驱动已安装(Windows 侧安装 NVIDIA 驱动即可,WSL2 自动共享)
nvidia-smi

# 安装 CUDA Toolkit(如果还没装)
wget https://developer.download.nvidia.com/compute/cuda/repos/wsl-ubuntu/x86_64/cuda-wsl-ubuntu.pin
sudo mv cuda-wsl-ubuntu.pin /etc/apt/preferences.d/cuda-repository-pin-600
wget https://developer.download.nvidia.com/compute/cuda/12.8.0/local_installers/cuda-repo-wsl-ubuntu-12-8-local_12.8.0-1_amd64.deb
sudo dpkg -i cuda-repo-wsl-ubuntu-12-8-local_12.8.0-1_amd64.deb
sudo cp /var/cuda-repo-wsl-ubuntu-12-8-local/cuda-*-keyring.gpg /usr/share/keyrings/
sudo apt-get update
sudo apt-get -y install cuda-toolkit-12-8

# 加入环境变量
echo 'export PATH=/usr/local/cuda/bin:$PATH' >> ~/.bashrc
echo 'export LD_LIBRARY_PATH=/usr/local/cuda/lib64:$LD_LIBRARY_PATH' >> ~/.bashrc
source ~/.bashrc

# 验证
nvcc --version

2. 确认你的 GPU 信息

nvidia-smi
# 你应该看到 RTX 5060 Ti, 8GB VRAM, Blackwell 架构

RTX 5060 Ti 关键参数(参考):

参数
SM 数量~36
CUDA Cores~4608
显存8 GB GDDR7
显存带宽~448 GB/s
Shared Memory / SM100 KB (可配置)

文件结构

本课包含以下文件,都在同一个目录下:

lesson3/
├── 01_vector_add.cu       # 热身:向量加法
├── 02_matmul_naive.cu     # 矩阵乘法 v1:naive
├── 03_matmul_shared.cu    # 矩阵乘法 v2:shared memory tiled
├── 04_matmul_register.cu  # 矩阵乘法 v3:register tiling
└── Makefile               # 一键编译

实验 1:向量加法(热身)

最简单的 kernel,目的是熟悉 CUDA 编程的基本流程:分配内存 → 拷贝数据 → 启动 kernel → 拷回结果。

文件: 01_vector_add.cu

/*
 * 实验 1:向量加法 (Vector Addition)
 *
 * 目的:熟悉 CUDA 编程基本流程
 *   1. cudaMalloc 分配 GPU 内存
 *   2. cudaMemcpy 拷贝数据到 GPU
 *   3. <<<grid, block>>> 启动 kernel
 *   4. cudaMemcpy 拷回结果
 *   5. cudaEvent 计时
 *
 * 编译:nvcc -o vec_add 01_vector_add.cu
 * 运行:./vec_add
 */

#include <stdio.h>
#include <stdlib.h>
#include <math.h>

// ============================================================
// CUDA Kernel:向量加法 C = A + B
// ============================================================
// __global__ 表示这是一个在 GPU 上执行、从 CPU 端调用的函数
// 每个线程计算 C 中的一个元素
__global__ void vectorAdd(const float *A, const float *B, float *C, int N) {
    // 计算当前线程的全局 ID
    // threadIdx.x: 线程在当前 block 内的索引 (0 ~ blockDim.x - 1)
    // blockIdx.x:  当前 block 在 grid 内的索引
    // blockDim.x:  每个 block 的线程数
    int tid = threadIdx.x + blockIdx.x * blockDim.x;

    // 边界检查:线程总数可能 > N(因为 block 大小是固定的)
    if (tid < N) {
        C[tid] = A[tid] + B[tid];
    }
}

// ============================================================
// 辅助函数:用 CUDA Event 精确计时
// ============================================================
// cudaEvent 的精度约 0.5 微秒,是 GPU kernel 计时的标准方式
// 比 CPU 侧的 clock() 准确得多(因为 kernel launch 是异步的)

int main() {
    const int N = 1 << 24;  // 16M 个元素,约 64MB per array
    const size_t bytes = N * sizeof(float);

    // ---- 1. 在 CPU (Host) 上分配和初始化数据 ----
    float *h_A = (float *)malloc(bytes);
    float *h_B = (float *)malloc(bytes);
    float *h_C = (float *)malloc(bytes);

    for (int i = 0; i < N; i++) {
        h_A[i] = sinf(i) * sinf(i);
        h_B[i] = cosf(i) * cosf(i);
    }

    // ---- 2. 在 GPU (Device) 上分配内存 ----
    float *d_A, *d_B, *d_C;
    cudaMalloc(&d_A, bytes);  // cudaMalloc(指针的地址, 字节数)
    cudaMalloc(&d_B, bytes);
    cudaMalloc(&d_C, bytes);

    // ---- 3. 将数据从 CPU 拷贝到 GPU ----
    // cudaMemcpy(目标, 来源, 字节数, 方向)
    cudaMemcpy(d_A, h_A, bytes, cudaMemcpyHostToDevice);
    cudaMemcpy(d_B, h_B, bytes, cudaMemcpyHostToDevice);

    // ---- 4. 配置 kernel 启动参数并执行 ----
    const int blockSize = 256;  // 每个 block 256 个线程
    // 向上取整:确保线程总数 >= N
    const int gridSize = (N + blockSize - 1) / blockSize;

    // 用 cudaEvent 计时
    cudaEvent_t start, stop;
    cudaEventCreate(&start);
    cudaEventCreate(&stop);

    cudaEventRecord(start);  // 记录开始时间
    vectorAdd<<<gridSize, blockSize>>>(d_A, d_B, d_C, N);  // 启动 kernel
    cudaEventRecord(stop);   // 记录结束时间
    cudaEventSynchronize(stop);  // 等 kernel 跑完

    float milliseconds = 0;
    cudaEventElapsedTime(&milliseconds, start, stop);

    printf("向量加法 kernel 耗时: %.3f ms\n", milliseconds);
    printf("元素数量: %d (%.1f M)\n", N, N / 1e6);
    printf("有效带宽: %.1f GB/s\n",
           (3.0 * bytes) / (milliseconds / 1000.0) / 1e9);  // 读A + 读B + 写C

    // ---- 5. 将结果从 GPU 拷回 CPU ----
    cudaMemcpy(h_C, d_C, bytes, cudaMemcpyDeviceToHost);

    // ---- 6. 验证结果 ----
    // 注意:sin²(x) + cos²(x) = 1
    float maxError = 0.0f;
    for (int i = 0; i < N; i++) {
        maxError = fmaxf(maxError, fabsf(h_C[i] - 1.0f));
    }
    printf("最大误差: %e (应接近 0)\n", maxError);

    // ---- 7. 释放内存 ----
    cudaFree(d_A);
    cudaFree(d_B);
    cudaFree(d_C);
    free(h_A);
    free(h_B);
    free(h_C);

    cudaEventDestroy(start);
    cudaEventDestroy(stop);

    return 0;
}

编译运行:

nvcc -o vec_add 01_vector_add.cu
./vec_add

在这里插入图片描述

要点

  • cudaMalloc 在 GPU 上分配内存,cudaFree 释放
  • cudaMemcpy 在 CPU 和 GPU 之间拷贝数据
  • <<<gridSize, blockSize>>> 是 kernel 启动语法
  • cudaDeviceSynchronize() 等 GPU 算完(kernel launch 是异步的)
  • cudaEvent 做精确计时(微秒级)

实验 2-4:矩阵乘法三级优化

这是本课的核心。我们用同一个矩阵尺寸(N=2048),实现三个版本:

v1: Naive(每个线程算 C 的一个元素)

每个线程独立从 Global Memory 读 A 的一行和 B 的一列,做点积。

问题:A 的第 i 行被 N 个线程各读一次 → Global Memory 读取次数 = N² × N = N³

/*
 * 实验 2:矩阵乘法 v1 — Naive 版本
 *
 * 每个线程计算 C 的一个元素 C[row][col]
 * 该线程需要读 A 的整行(N 个元素)和 B 的整列(N 个元素)
 *
 * 问题:A 的第 i 行被 N 个线程各读一次 → Global Memory 读取次数 = 2 × N³
 * 这是最慢的实现,但逻辑最清晰,作为优化基线。
 *
 * 编译:nvcc -O3 -o matmul_naive 02_matmul_naive.cu
 * 运行:./matmul_naive
 */

#include <stdio.h>
#include <stdlib.h>
#include <math.h>

#define N 2048  // 矩阵维度:2048 × 2048

// ============================================================
// Kernel:Naive 矩阵乘法 C = A × B
// ============================================================
// 每个线程计算 C[row][col] 的一个元素
// 线程需要从 Global Memory 读 A[row][*] 整行 + B[*][col] 整列
__global__ void matMulNaive(const float *A, const float *B, float *C, int width) {
    // 用 2D block/grid 组织线程
    int row = threadIdx.y + blockIdx.y * blockDim.y;  // 行索引
    int col = threadIdx.x + blockIdx.x * blockDim.x;  // 列索引

    if (row < width && col < width) {
        float sum = 0.0f;
        // 沿 K 维度(公共维度)做点积
        for (int k = 0; k < width; k++) {
            // A[row][k] = A[row * width + k]  (行优先存储)
            // B[k][col] = B[k * width + col]
            sum += A[row * width + k] * B[k * width + col];
        }
        C[row * width + col] = sum;
    }
}

// ============================================================
// CPU 参考实现(用于验证正确性)
// ============================================================
void matMulCPU(const float *A, const float *B, float *C, int width) {
    for (int i = 0; i < width; i++) {
        for (int j = 0; j < width; j++) {
            float sum = 0.0f;
            for (int k = 0; k < width; k++) {
                sum += A[i * width + k] * B[k * width + j];
            }
            C[i * width + j] = sum;
        }
    }
}

int main() {
    const int width = N;
    const size_t bytes = N * N * sizeof(float);

    printf("=== 矩阵乘法 v1: Naive ===\n");
    printf("矩阵大小: %d × %d (%.1f M 元素)\n", N, N, (float)(N * N) / 1e6);
    printf("FLOPs: %.2f G (2 × N³)\n", 2.0 * N * N * N / 1e9);

    // ---- 分配和初始化 ----
    float *h_A = (float *)malloc(bytes);
    float *h_B = (float *)malloc(bytes);
    float *h_C = (float *)malloc(bytes);

    // 用小数随机初始化,避免数值溢出
    srand(42);
    for (int i = 0; i < N * N; i++) {
        h_A[i] = (float)(rand() % 100) / 100.0f;
        h_B[i] = (float)(rand() % 100) / 100.0f;
    }

    float *d_A, *d_B, *d_C;
    cudaMalloc(&d_A, bytes);
    cudaMalloc(&d_B, bytes);
    cudaMalloc(&d_C, bytes);

    cudaMemcpy(d_A, h_A, bytes, cudaMemcpyHostToDevice);
    cudaMemcpy(d_B, h_B, bytes, cudaMemcpyHostToDevice);

    // ---- 启动 kernel ----
    // 2D block: 16×16 = 256 线程/block
    // 2D grid: 覆盖整个 N×N 矩阵
    dim3 blockSize(16, 16);
    dim3 gridSize((N + blockSize.x - 1) / blockSize.x,
                  (N + blockSize.y - 1) / blockSize.y);

    cudaEvent_t start, stop;
    cudaEventCreate(&start);
    cudaEventCreate(&stop);

    cudaEventRecord(start);
    matMulNaive<<<gridSize, blockSize>>>(d_A, d_B, d_C, width);
    cudaEventRecord(stop);
    cudaEventSynchronize(stop);

    float ms = 0;
    cudaEventElapsedTime(&ms, start, stop);

    // 计算性能指标
    double gflops = 2.0 * N * N * N / (ms / 1000.0) / 1e9;
    double bandwidth = (3.0 * N * N * sizeof(float) * N) / (ms / 1000.0) / 1e9;
    // 带宽计算说明:每个元素需要读 A 的 1 行 + B 的 1 列 ≈ 2N 次 float 读取
    // 简化:总读取 ≈ N³ × sizeof(float)(每个点积读 2N 个 float,共 N² 个点积)

    printf("Kernel 耗时: %.3f ms\n", ms);
    printf("性能: %.1f GFLOPS\n", gflops);

    cudaMemcpy(h_C, d_C, bytes, cudaMemcpyDeviceToHost);

    // ---- 验证(只检查一小部分,CPU 太慢不能算完整的 2048×2048)----
    // 用小矩阵快速验证
    const int V = 64;
    float vA[V * V], vB[V * V], vC_cpu[V * V], vC_gpu[V * V];
    for (int i = 0; i < V * V; i++) {
        vA[i] = h_A[i];
        vB[i] = h_B[i];
    }
    matMulCPU(vA, vB, vC_cpu, V);

    dim3 vBlock(16, 16);
    dim3 vGrid((V + 15) / 16, (V + 15) / 16);
    float *d_vA, *d_vB, *d_vC;
    cudaMalloc(&d_vA, V * V * sizeof(float));
    cudaMalloc(&d_vB, V * V * sizeof(float));
    cudaMalloc(&d_vC, V * V * sizeof(float));
    cudaMemcpy(d_vA, vA, V * V * sizeof(float), cudaMemcpyHostToDevice);
    cudaMemcpy(d_vB, vB, V * V * sizeof(float), cudaMemcpyHostToDevice);
    matMulNaive<<<vGrid, vBlock>>>(d_vA, d_vB, d_vC, V);
    cudaMemcpy(vC_gpu, d_vC, V * V * sizeof(float), cudaMemcpyDeviceToHost);

    float maxErr = 0;
    for (int i = 0; i < V * V; i++) {
        maxErr = fmaxf(maxErr, fabsf(vC_cpu[i] - vC_gpu[i]));
    }
    printf("验证 (64×64): 最大误差 = %e %s\n", maxErr,
           maxErr < 1e-3 ? "✓ PASS" : "✗ FAIL");

    // ---- 清理 ----
    cudaFree(d_A); cudaFree(d_B); cudaFree(d_C);
    cudaFree(d_vA); cudaFree(d_vB); cudaFree(d_vC);
    free(h_A); free(h_B); free(h_C);
    cudaEventDestroy(start); cudaEventDestroy(stop);

    return 0;
}

在这里插入图片描述

v2: Shared Memory Tiled

把矩阵沿 K 维度分块,每个 Block 协作加载 tile 到 Shared Memory,Block 内线程从 Shared Memory 读。

收益:Global Memory 读取次数 = N² × N / TILE_SIZE = N³/32(TILE_SIZE=32 时,减少 32 倍)

/*
 * 实验 3:矩阵乘法 v2 — Shared Memory Tiled
 *
 * 核心优化:把 A 和 B 的 tile 加载到 Shared Memory,
 * Block 内所有线程从 Shared Memory 读取,大幅减少 Global Memory 访问。
 *
 * TILE_SIZE = 32:每个 Block 有 32×32 = 1024 个线程
 * 每个线程仍只算 C 的 1 个元素
 *
 * 编译:nvcc -O3 -o matmul_shared 03_matmul_shared.cu
 * 运行:./matmul_shared
 */

#include <stdio.h>
#include <stdlib.h>
#include <math.h>

#define N 2048
#define TILE_SIZE 32  // tile 维度,也是 block 的维度

// ============================================================
// Kernel:Shared Memory Tiled 矩阵乘法
// ============================================================
__global__ void matMulShared(const float *A, const float *B, float *C, int width) {
    // ① 声明 Shared Memory:每个 Block 分配两块 TILE×TILE
    // __shared__ 表示这块内存在 SM 芯片上,Block 内所有线程共享
    __shared__ float sA[TILE_SIZE][TILE_SIZE];
    __shared__ float sB[TILE_SIZE][TILE_SIZE];

    // ② 当前线程负责 C 的哪个元素
    int row = threadIdx.y + blockIdx.y * TILE_SIZE;
    int col = threadIdx.x + blockIdx.x * TILE_SIZE;

    float sum = 0.0f;

    // ③ 沿 K 维度分块遍历
    // 每次加载一个 TILE×TILE 的 tile 到 Shared Memory
    // 共需要 width / TILE_SIZE 次迭代
    for (int t = 0; t < (width + TILE_SIZE - 1) / TILE_SIZE; t++) {
        // ④ 协作加载:每个线程负责从 Global Memory 搬一个元素
        //
        // 加载 A 的 tile:当前行 row,第 t 块的列
        // A 的坐标: [row][t * TILE_SIZE + threadIdx.x]
        int aCol = t * TILE_SIZE + threadIdx.x;
        int aRow = row;
        if (aRow < width && aCol < width)
            sA[threadIdx.y][threadIdx.x] = A[aRow * width + aCol];
        else
            sA[threadIdx.y][threadIdx.x] = 0.0f;  // 边界填充 0

        // 加载 B 的 tile:第 t 块的行,当前列 col
        // B 的坐标: [t * TILE_SIZE + threadIdx.y][col]
        int bRow = t * TILE_SIZE + threadIdx.y;
        int bCol = col;
        if (bRow < width && bCol < width)
            sB[threadIdx.y][threadIdx.x] = B[bRow * width + bCol];
        else
            sB[threadIdx.y][threadIdx.x] = 0.0f;

        // ⑤ 屏障同步:等 Block 内所有线程都加载完了
        // 如果没有这个,有些线程可能还没写完 Shared Memory,其他线程就开始读了
        __syncthreads();

        // ⑥ 从 Shared Memory 做计算(~20 cycles vs Global Memory ~300+ cycles)
        for (int k = 0; k < TILE_SIZE; k++) {
            sum += sA[threadIdx.y][k] * sB[k][threadIdx.x];
        }

        // ⑦ 第二次同步:等所有线程算完了,才能加载下一个 tile(会覆盖 sA/sB)
        __syncthreads();
    }

    // ⑧ 写回结果到 Global Memory
    if (row < width && col < width) {
        C[row * width + col] = sum;
    }
}

// ============================================================
// CPU 参考实现
// ============================================================
void matMulCPU(const float *A, const float *B, float *C, int width) {
    for (int i = 0; i < width; i++) {
        for (int j = 0; j < width; j++) {
            float sum = 0.0f;
            for (int k = 0; k < width; k++) {
                sum += A[i * width + k] * B[k * width + j];
            }
            C[i * width + j] = sum;
        }
    }
}

int main() {
    const int width = N;
    const size_t bytes = N * N * sizeof(float);

    printf("=== 矩阵乘法 v2: Shared Memory Tiled ===\n");
    printf("矩阵大小: %d × %d\n", N, N);
    printf("Tile 大小: %d × %d\n", TILE_SIZE, TILE_SIZE);
    printf("每个 Block 线程数: %d\n", TILE_SIZE * TILE_SIZE);

    // ---- 分配和初始化 ----
    float *h_A = (float *)malloc(bytes);
    float *h_B = (float *)malloc(bytes);
    float *h_C = (float *)malloc(bytes);

    srand(42);
    for (int i = 0; i < N * N; i++) {
        h_A[i] = (float)(rand() % 100) / 100.0f;
        h_B[i] = (float)(rand() % 100) / 100.0f;
    }

    float *d_A, *d_B, *d_C;
    cudaMalloc(&d_A, bytes);
    cudaMalloc(&d_B, bytes);
    cudaMalloc(&d_C, bytes);

    cudaMemcpy(d_A, h_A, bytes, cudaMemcpyHostToDevice);
    cudaMemcpy(d_B, h_B, bytes, cudaMemcpyHostToDevice);

    // ---- 启动 kernel ----
    // Block: 32×32 = 1024 线程
    // Grid: 覆盖 N×N
    dim3 blockSize(TILE_SIZE, TILE_SIZE);
    dim3 gridSize((N + TILE_SIZE - 1) / TILE_SIZE,
                  (N + TILE_SIZE - 1) / TILE_SIZE);

    printf("Grid: %d × %d blocks\n", gridSize.x, gridSize.y);

    cudaEvent_t start, stop;
    cudaEventCreate(&start);
    cudaEventCreate(&stop);

    // warmup:第一次运行可能有驱动初始化开销
    matMulShared<<<gridSize, blockSize>>>(d_A, d_B, d_C, width);
    cudaDeviceSynchronize();

    // 正式计时
    cudaEventRecord(start);
    matMulShared<<<gridSize, blockSize>>>(d_A, d_B, d_C, width);
    cudaEventRecord(stop);
    cudaEventSynchronize(stop);

    float ms = 0;
    cudaEventElapsedTime(&ms, start, stop);

    double gflops = 2.0 * N * N * N / (ms / 1000.0) / 1e9;
    printf("Kernel 耗时: %.3f ms\n", ms);
    printf("性能: %.1f GFLOPS\n", gflops);

    cudaMemcpy(h_C, d_C, bytes, cudaMemcpyDeviceToHost);

    // ---- 验证 ----
    const int V = 64;
    float vA[V * V], vB[V * V], vC_cpu[V * V], vC_gpu[V * V];
    for (int i = 0; i < V * V; i++) {
        vA[i] = h_A[i];
        vB[i] = h_B[i];
    }
    matMulCPU(vA, vB, vC_cpu, V);

    dim3 vBlock(TILE_SIZE, TILE_SIZE);
    dim3 vGrid((V + TILE_SIZE - 1) / TILE_SIZE, (V + TILE_SIZE - 1) / TILE_SIZE);
    float *d_vA, *d_vB, *d_vC;
    cudaMalloc(&d_vA, V * V * sizeof(float));
    cudaMalloc(&d_vB, V * V * sizeof(float));
    cudaMalloc(&d_vC, V * V * sizeof(float));
    cudaMemcpy(d_vA, vA, V * V * sizeof(float), cudaMemcpyHostToDevice);
    cudaMemcpy(d_vB, vB, V * V * sizeof(float), cudaMemcpyHostToDevice);
    matMulShared<<<vGrid, vBlock>>>(d_vA, d_vB, d_vC, V);
    cudaMemcpy(vC_gpu, d_vC, V * V * sizeof(float), cudaMemcpyDeviceToHost);

    float maxErr = 0;
    for (int i = 0; i < V * V; i++) {
        maxErr = fmaxf(maxErr, fabsf(vC_cpu[i] - vC_gpu[i]));
    }
    printf("验证 (64×64): 最大误差 = %e %s\n", maxErr,
           maxErr < 1e-3 ? "✓ PASS" : "✗ FAIL");

    // ---- 清理 ----
    cudaFree(d_A); cudaFree(d_B); cudaFree(d_C);
    cudaFree(d_vA); cudaFree(d_vB); cudaFree(d_vC);
    free(h_A); free(h_B); free(h_C);
    cudaEventDestroy(start); cudaEventDestroy(stop);

    return 0;
}

在这里插入图片描述

v3: Register Tiling(进一步优化)

每个线程不只算 C 的一个元素,而是算 TM×TN 个小块。加载到 Shared Memory 后,每个线程把 A 和 B 的一行/列缓存到寄存器,进一步减少 Shared Memory 读取。

收益:Shared Memory 读取次数也大幅减少,计算强度(Arithmetic Intensity)更高。

/*
 * 实验 4:矩阵乘法 v3 — Register Tiling(寄存器分块)
 *
 * 在 Shared Memory Tiled 的基础上进一步优化:
 * - 每个线程不再只算 C 的 1 个元素,而是算 TM × TN 个小块(这里 TM=TN=4)
 * - 从 Shared Memory 加载后,把 A 的一行和 B 的一列缓存到寄存器
 * - 大幅减少 Shared Memory 的读取次数
 *
 * 这是 cuBLAS 等高性能库的基本思路(当然它们还有更多优化)。
 *
 * 编译:nvcc -O3 -o matmul_register 04_matmul_register.cu
 * 运行:./matmul_register
 */

#include <stdio.h>
#include <stdlib.h>
#include <math.h>

#define N 2048

// ============================================================
// 分块参数
// ============================================================
// BM, BN: Block 负责的 C 的子矩阵大小(一个 Block 算 C 的 BM × BN 区域)
#define BM 64
#define BN 64

// BK: 沿 K 维度的 tile 大小
#define BK 16

// TM, TN: 每个线程负责的子块大小
// 每线程算 TM × TN = 4 × 4 = 16 个 C 的元素
#define TM 4
#define TN 4

// Block 线程数: (BM/TM) × (BN/TN) = 16 × 16 = 256
// 即 Block 是 16×16 的 2D grid,每个线程负责一个 4×4 的小块

// ============================================================
// Kernel:Register Tiled 矩阵乘法
// ============================================================
__global__ void matMulRegister(const float *A, const float *B, float *C, int width) {
    // Block 负责的 C 的子矩阵的起始行列
    const int blockRow = blockIdx.y * BM;
    const int blockCol = blockIdx.x * BN;

    // 线程在 Block 内的坐标
    const int tx = threadIdx.x;  // 0 ~ BN/TN - 1 = 0 ~ 15
    const int ty = threadIdx.y;  // 0 ~ BM/TM - 1 = 0 ~ 15

    // 该线程负责的 C 的 TM×TN 子块的起始位置(相对于 block 起点)
    const int threadRow = ty * TM;
    const int threadCol = tx * TN;

    // ① Shared Memory:加载 A 和 B 的 tile
    __shared__ float sA[BM][BK];  // 64 × 16
    __shared__ float sB[BK][BN];  // 16 × 64

    // ② 寄存器:存储当前线程的累积结果(TM × TN = 16 个 float)
    float threadResults[TM][TN] = {0.0f};

    // ③ 沿 K 维度分块遍历
    for (int t = 0; t < (width + BK - 1) / BK; t++) {
        // ---- 协作加载 A 的 tile 到 Shared Memory ----
        // A 的 tile: [blockRow ~ blockRow+BM][t*BK ~ t*BK+BK] = 64 × 16
        // 256 个线程加载 64×16 = 1024 个元素 → 每线程加载 4 个
        for (int loadOffset = 0; loadOffset < BM * BK; loadOffset += blockDim.x * blockDim.y) {
            int loadIdx = loadOffset + ty * blockDim.x + tx;
            if (loadIdx < BM * BK) {
                int loadRow = loadIdx / BK;
                int loadCol = loadIdx % BK;
                int globalRow = blockRow + loadRow;
                int globalCol = t * BK + loadCol;
                sA[loadRow][loadCol] = (globalRow < width && globalCol < width)
                                       ? A[globalRow * width + globalCol] : 0.0f;
            }
        }

        // ---- 协作加载 B 的 tile 到 Shared Memory ----
        // B 的 tile: [t*BK ~ t*BK+BK][blockCol ~ blockCol+BN] = 16 × 64
        // 16×64 = 1024 个元素 → 每线程加载 4 个
        for (int loadOffset = 0; loadOffset < BK * BN; loadOffset += blockDim.x * blockDim.y) {
            int loadIdx = loadOffset + ty * blockDim.x + tx;
            if (loadIdx < BK * BN) {
                int loadRow = loadIdx / BN;
                int loadCol = loadIdx % BN;
                int globalRow = t * BK + loadRow;
                int globalCol = blockCol + loadCol;
                sB[loadRow][loadCol] = (globalRow < width && globalCol < width)
                                       ? B[globalRow * width + globalCol] : 0.0f;
            }
        }

        __syncthreads();

        // ---- 在寄存器内做计算 ----
        // 对 K 维度内的每个 k:
        for (int k = 0; k < BK; k++) {
            // 把 A 的一行加载到寄存器(TM 个值)
            // A 的行:sA[threadRow + 0..TM-1][k]
            float regA[TM];
            #pragma unroll
            for (int m = 0; m < TM; m++) {
                regA[m] = sA[threadRow + m][k];
            }

            // 把 B 的一列加载到寄存器(TN 个值)
            // B 的列:sB[k][threadCol + 0..TN-1]
            float regB[TN];
            #pragma unroll
            for (int n = 0; n < TN; n++) {
                regB[n] = sB[k][threadCol + n];
            }

            // 外积累加:TM × TN 次乘加
            #pragma unroll
            for (int m = 0; m < TM; m++) {
                #pragma unroll
                for (int n = 0; n < TN; n++) {
                    threadResults[m][n] += regA[m] * regB[n];
                }
            }
        }

        __syncthreads();
    }

    // ④ 写回结果到 Global Memory
    #pragma unroll
    for (int m = 0; m < TM; m++) {
        int globalRow = blockRow + threadRow + m;
        if (globalRow < width) {
            #pragma unroll
            for (int n = 0; n < TN; n++) {
                int globalCol = blockCol + threadCol + n;
                if (globalCol < width) {
                    C[globalRow * width + globalCol] = threadResults[m][n];
                }
            }
        }
    }
}

// ============================================================
// CPU 参考实现
// ============================================================
void matMulCPU(const float *A, const float *B, float *C, int width) {
    for (int i = 0; i < width; i++) {
        for (int j = 0; j < width; j++) {
            float sum = 0.0f;
            for (int k = 0; k < width; k++) {
                sum += A[i * width + k] * B[k * width + j];
            }
            C[i * width + j] = sum;
        }
    }
}

int main() {
    const int width = N;
    const size_t bytes = N * N * sizeof(float);

    printf("=== 矩阵乘法 v3: Register Tiled ===\n");
    printf("矩阵大小: %d × %d\n", N, N);
    printf("Block tile: %d × %d, 每线程 tile: %d × %d\n", BM, BN, TM, TN);
    printf("Block 线程数: %d × %d = %d\n", BN / TN, BM / TM,
           (BN / TN) * (BM / TM));

    // ---- 分配和初始化 ----
    float *h_A = (float *)malloc(bytes);
    float *h_B = (float *)malloc(bytes);
    float *h_C = (float *)malloc(bytes);

    srand(42);
    for (int i = 0; i < N * N; i++) {
        h_A[i] = (float)(rand() % 100) / 100.0f;
        h_B[i] = (float)(rand() % 100) / 100.0f;
    }

    float *d_A, *d_B, *d_C;
    cudaMalloc(&d_A, bytes);
    cudaMalloc(&d_B, bytes);
    cudaMalloc(&d_C, bytes);

    cudaMemcpy(d_A, h_A, bytes, cudaMemcpyHostToDevice);
    cudaMemcpy(d_B, h_B, bytes, cudaMemcpyHostToDevice);

    // ---- 启动 kernel ----
    dim3 blockSize(BN / TN, BM / TM);  // 16 × 16 = 256
    dim3 gridSize((N + BN - 1) / BN, (N + BM - 1) / BM);

    printf("Grid: %d × %d blocks\n", gridSize.x, gridSize.y);

    cudaEvent_t start, stop;
    cudaEventCreate(&start);
    cudaEventCreate(&stop);

    // warmup
    matMulRegister<<<gridSize, blockSize>>>(d_A, d_B, d_C, width);
    cudaDeviceSynchronize();

    // 正式计时
    cudaEventRecord(start);
    matMulRegister<<<gridSize, blockSize>>>(d_A, d_B, d_C, width);
    cudaEventRecord(stop);
    cudaEventSynchronize(stop);

    float ms = 0;
    cudaEventElapsedTime(&ms, start, stop);

    double gflops = 2.0 * N * N * N / (ms / 1000.0) / 1e9;
    printf("Kernel 耗时: %.3f ms\n", ms);
    printf("性能: %.1f GFLOPS\n", gflops);

    cudaMemcpy(h_C, d_C, bytes, cudaMemcpyDeviceToHost);

    // ---- 验证 ----
    const int V = 64;
    float vA[V * V], vB[V * V], vC_cpu[V * V], vC_gpu[V * V];
    for (int i = 0; i < V * V; i++) {
        vA[i] = h_A[i];
        vB[i] = h_B[i];
    }
    matMulCPU(vA, vB, vC_cpu, V);

    dim3 vBlock(BN / TN, BM / TM);
    dim3 vGrid((V + BN - 1) / BN, (V + BM - 1) / BM);
    float *d_vA, *d_vB, *d_vC;
    cudaMalloc(&d_vA, V * V * sizeof(float));
    cudaMalloc(&d_vB, V * V * sizeof(float));
    cudaMalloc(&d_vC, V * V * sizeof(float));
    cudaMemcpy(d_vA, vA, V * V * sizeof(float), cudaMemcpyHostToDevice);
    cudaMemcpy(d_vB, vB, V * V * sizeof(float), cudaMemcpyHostToDevice);
    matMulRegister<<<vGrid, vBlock>>>(d_vA, d_vB, d_vC, V);
    cudaMemcpy(vC_gpu, d_vC, V * V * sizeof(float), cudaMemcpyDeviceToHost);

    float maxErr = 0;
    for (int i = 0; i < V * V; i++) {
        maxErr = fmaxf(maxErr, fabsf(vC_cpu[i] - vC_gpu[i]));
    }
    printf("验证 (64×64): 最大误差 = %e %s\n", maxErr,
           maxErr < 1e-3 ? "✓ PASS" : "✗ FAIL");

    // ---- 清理 ----
    cudaFree(d_A); cudaFree(d_B); cudaFree(d_C);
    cudaFree(d_vA); cudaFree(d_vB); cudaFree(d_vC);
    free(h_A); free(h_B); free(h_C);
    cudaEventDestroy(start); cudaEventDestroy(stop);

    return 0;
}

在这里插入图片描述


编译与运行

# 一键编译所有程序
make

# 或者单独编译
nvcc -O3 -o matmul_naive 02_matmul_naive.cu
nvcc -O3 -o matmul_shared 03_matmul_shared.cu
nvcc -O3 -o matmul_register 04_matmul_register.cu

# 运行
./matmul_naive
./matmul_shared
./matmul_register

性能分析

# 用 nsight compute 分析(如果可用)
ncu --metrics sm__throughput.avg.pct_of_peak_sustained_elapsed,dram__throughput.avg.pct_of_peak_sustained_elapsed ./matmul_naive
ncu --metrics sm__throughput.avg.pct_of_peak_sustained_elapsed,dram__throughput.avg.pct_of_peak_sustained_elapsed ./matmul_shared

# 或者用 nvprof(旧版)
nvprof ./matmul_naive

预期结果

版本大致时间 (N=2048)相对加速瓶颈
Naive~50-80 ms1xGlobal Memory 带宽
Shared Tiled~10-20 ms3-5xShared Memory bank conflict
Register Tiled~3-8 ms8-15x寄存器数量 / occupancy

注:具体数值取决于你的 GPU 和 CUDA 版本,重点是看相对加速比和瓶颈转移。


本课小结

版本优化手段减少的访问
Naive
Shared TiledShared Memory 缓存Global Memory → Shared Memory
Register Tiled寄存器缓存 + 每线程多元素Shared Memory → Register

这三个版本就是 GPU 优化的核心范式:把数据从慢的内存搬到快的内存,最大化复用。FlashAttention、vLLM 的 PagedAttention 本质上都在做同样的事,只是搬的数据和层级不同。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值