从零开始实现一个简单的GPU矩阵乘法

假设我们要计算 C=A×BC = A \times BC=A×B,其中 AAA 是 M×KM \times KM×K 矩阵,BBB 是 K×NK \times NK×N 矩阵,CCC 是 M×NM \times NM×N 矩阵。

1. 矩阵乘法回顾

矩阵 CCC 中任意元素 Ci,jC_{i, j}Ci,j 的值,是通过将矩阵 AAA 的第 iii 行与矩阵 BBB 的第 jjj 列进行点积得到的:

Ci,j=∑k=0K−1Ai,k⋅Bk,jC_{i, j} = \sum_{k=0}^{K-1} A_{i, k} \cdot B_{k, j}Ci,j=k=0∑K−1Ai,k⋅Bk,j

2. 内存布局:行主序(Row-Major)

在 C/C++ 中,多维数组通常以**行主序(Row-Major Order)**存储在内存中,即一行接一行地存储。

  • 对于 M×NM \times NM×N 矩阵 XXX,元素 Xi,jX_{i, j}Xi,j 的一维索引是:

    Index(i,j)=i×N+j\text{Index}(i, j) = i \times N + jIndex(i,j)=i×N+j

    其中 NNN 是矩阵的列数。

3. CUDA 编程步骤

步骤 1: 定义 Kernel 启动配置

我们需要启动 M×NM \times NM×N 个线程,让每个线程负责计算输出矩阵 CCC 中的一个元素 Ci,jC_{i, j}Ci,j。

步骤 2: 计算线程的全局索引 (i,j)(i, j)(i,j)

Kernel 内部的关键是,将线程的 xxx 和 yyy 索引转换为矩阵 CCC 的行索引 iii 和列索引 jjj。

  • 行索引 iii: 对应于线程的全局 yyy 坐标。

  • 列索引 jjj: 对应于线程的全局 xxx 坐标。

i=blockIdx.y×blockDim.y+threadIdx.yj=blockIdx.x×blockDim.x+threadIdx.xi = \text{blockIdx.y} \times \text{blockDim.y} + \text{threadIdx.y} \\ j = \text{blockIdx.x} \times \text{blockDim.x} + \text{threadIdx.x}i=blockIdx.y×blockDim.y+threadIdx.yj=blockIdx.x×blockDim.x+threadIdx.x

步骤 3: 实现 Kernel 逻辑

如果 iii 在 MMM 范围内,jjj 在 NNN 范围内,线程执行 AAA 的第 iii 行和 BBB 的第 jjj 列的点积计算。

4. 完整的 CUDA C++ 代码实现

c++ 复制代码
#include <stdio.h>
#include <cuda_runtime.h>

// 矩阵维度定义 (M x K) * (K x N) = (M x N)
#define M 1024
#define K 512
#define N 1024

// 矩阵乘法 Kernel:每个线程计算 C 的一个元素
__global__ void matrixMul(const float* A, const float* B, float* C, int M, int K, int N) {
    // 1. 计算线程在输出矩阵 C 中的全局坐标 (i, j)
    
    // i 是行索引 (对应 Block Y 和 Thread Y)
    int i = blockIdx.y * blockDim.y + threadIdx.y; 
    // j 是列索引 (对应 Block X 和 Thread X)
    int j = blockIdx.x * blockDim.x + threadIdx.x; 

    // 2. 边界检查 (确保线程不超过矩阵 C 的维度 M x N)
    if (i < M && j < N) {
        float Cij = 0;
        
        // 3. 执行点积计算 (A 的第 i 行 和 B 的第 j 列)
        for (int k = 0; k < K; ++k) {
            // A[i, k] 的一维索引: i * K + k
            float Aik = A[i * K + k]; 
            // B[k, j] 的一维索引: k * N + j
            float Bkj = B[k * N + j]; 
            
            Cij += Aik * Bkj;
        }

        // 4. 将结果写回输出矩阵 C
        // C[i, j] 的一维索引: i * N + j
        C[i * N + j] = Cij;
    }
}

void setupAndLaunch() {
    // --- 1. 内存分配与初始化 ---
    size_t size_A = (size_t)M * K * sizeof(float);
    size_t size_B = (size_t)K * N * sizeof(float);
    size_t size_C = (size_t)M * N * sizeof(float);
    
    // Host 端内存
    float *h_A, *h_B, *h_C;
    h_A = (float*)malloc(size_A);
    h_B = (float*)malloc(size_B);
    h_C = (float*)malloc(size_C);
    
    // 初始化 A 和 B (略,假设已初始化)
    // for (int i = 0; i < M*K; ++i) h_A[i] = ...;

    // Device 端内存
    float *d_A, *d_B, *d_C;
    cudaMalloc((void**)&d_A, size_A);
    cudaMalloc((void**)&d_B, size_B);
    cudaMalloc((void**)&d_C, size_C);

    // --- 2. 数据传输:Host -> Device ---
    cudaMemcpy(d_A, h_A, size_A, cudaMemcpyHostToDevice);
    cudaMemcpy(d_B, h_B, size_B, cudaMemcpyHostToDevice);
    
    // --- 3. 配置启动参数与 Kernel 启动 ---
    
    // 定义 Thread Block 大小 (例如 16x16)
    int TILE_SIZE = 16;
    dim3 threadsPerBlock(TILE_SIZE, TILE_SIZE); 

    // 计算 Grid 大小
    // M / TILE_SIZE 向上取整,作为 Grid 的 Y 维度
    int gridX = (N + TILE_SIZE - 1) / TILE_SIZE; 
    // N / TILE_SIZE 向上取整,作为 Grid 的 X 维度
    int gridY = (M + TILE_SIZE - 1) / TILE_SIZE;
    dim3 blocksPerGrid(gridX, gridY);
    
    // 启动 Kernel
    matrixMul<<<blocksPerGrid, threadsPerBlock>>>(d_A, d_B, d_C, M, K, N);
    
    // 检查是否有 Kernel 启动错误
    cudaError_t err = cudaGetLastError();
    if (err != cudaSuccess) {
        fprintf(stderr, "CUDA launch error: %s\n", cudaGetErrorString(err));
        return;
    }
    
    // 等待 GPU 完成
    cudaDeviceSynchronize();

    // --- 4. 数据传输:Device -> Host ---
    cudaMemcpy(h_C, d_C, size_C, cudaMemcpyDeviceToHost);

    // ... 验证结果 h_C ...

    // --- 5. 清理 ---
    cudaFree(d_A);
    cudaFree(d_B);
    cudaFree(d_C);
    free(h_A);
    free(h_B);
    free(h_C);
}

int main() {
    setupAndLaunch();
    return 0;
}

5. 性能分析(该简单实现的局限性)

这个"从零开始"的实现虽然功能正确,但性能非常低效

  1. 全局内存带宽限制: 计算 Ci,jC_{i, j}Ci,j 时,每个线程都需要读取 KKK 个 AAA 元素和 KKK 个 BBB 元素。如果 KKK 很大,会导致大量的全局内存读取。

  2. 内存访问不连续:

    • 读取 Ai,kA_{i, k}Ai,k(同一行)是连续的,对 GPU 有利。

    • 读取 Bk,jB_{k, j}Bk,j(同一列)在行主序存储中是不连续 的(跨步访问)。这会导致严重的内存合并(Memory Coalescing)失败,极大降低内存吞吐量。


优化方向:使用共享内存(Shared Memory)

要实现高性能的矩阵乘法,必须采用 **Tile Blocking(分块)**技术,并将输入矩阵的块加载到 **共享内存(Shared Memory)**中,以实现数据重用和内存合并。

这是下一个层次的优化,涉及更复杂的 Kernel 逻辑和资源管理。

相关推荐
qq_433554547 小时前
C++ 状压DP(01矩阵约束问题)
c++·算法·矩阵
Zevalin爱灰灰9 小时前
线性代数 第一章——行列式
线性代数
牧歌悠悠10 小时前
【Random Matrices】第一章-随机矩阵入门
线性代数·数学·概率论·随机矩阵·高维概率
iAkuya1 天前
(leetcode)力扣100 21搜索二维矩阵2(z型搜索)
linux·leetcode·矩阵
一碗姜汤1 天前
LS性能边界、QR分解、RLS自适应
线性代数·机器学习
CreasyChan1 天前
数学基础-矩阵与变换
线性代数·矩阵
com_4sapi1 天前
2026年矩阵系统三家优质服务商可靠支撑
线性代数·矩阵
会编程是什么感觉...1 天前
算法 - FOC
线性代数·算法·矩阵·无刷电机
MicroTech20251 天前
MLGO微算法科技发布改进量子ODE算法,支持不可对角化矩阵与非齐次系统实现指数级误差优化
科技·算法·矩阵