一、方法一

#include <iostream>
#include <cuda_runtime.h>

__global__ void matrixMultiply(int *A, int *B, int *C, int N) {
    // 计算每个线程负责的矩阵元素位置
    int row = blockIdx.y * blockDim.y + threadIdx.y;
    int col = blockIdx.x * blockDim.x + threadIdx.x;

    if (row < N && col < N) {
        int value = 0;
        // 计算矩阵 C 的元素 C[row][col]
        for (int k = 0; k < N; ++k) {
            value += A[row * N + k] * B[k * N + col];
        }
        C[row * N + col] = value;
    }
}

int main() {
    int N = 1024;  // 矩阵大小 N x N
    int size = N * N * sizeof(int);  // 每个矩阵的字节大小

    // 为矩阵 A, B, C 在主机上分配内存
    int *h_A = new int[N * N];
    int *h_B = new int[N * N];
    int *h_C = new int[N * N];

    // 初始化矩阵 A 和 B
    for (int i = 0; i < N * N; ++i) {
        h_A[i] = 1;  // 矩阵 A 初始化为 1
        h_B[i] = 2;  // 矩阵 B 初始化为 2
    }

    // 为设备上的矩阵 A, B, C 分配内存
    int *d_A, *d_B, *d_C;
    cudaMalloc(&d_A, size);
    cudaMalloc(&d_B, size);
    cudaMalloc(&d_C, size);

    // 将矩阵 A 和 B 从主机复制到设备
    cudaMemcpy(d_A, h_A, size, cudaMemcpyHostToDevice);
    cudaMemcpy(d_B, h_B, size, cudaMemcpyHostToDevice);

    // 设置线程和块的维度
    dim3 threadsPerBlock(16, 16);  // 每个块 16x16 的线程
    dim3 blocksPerGrid((N + 15) / 16, (N + 15) / 16);  // 网格的块的数量

    // 启动内核进行矩阵乘法
    matrixMultiply<<<blocksPerGrid, threadsPerBlock>>>(d_A, d_B, d_C, N);

    // 检查内核启动是否有错误
    cudaError_t error = cudaGetLastError();
    if (error != cudaSuccess) {
        std::cerr << "CUDA kernel launch failed: " << cudaGetErrorString(error) << std::endl;
        return -1;
    }

    // 等待设备上的所有计算完成
    cudaDeviceSynchronize();

    // 将结果从设备复制回主机
    cudaMemcpy(h_C, d_C, size, cudaMemcpyDeviceToHost);

    // 打印部分结果验证
    for (int i = 0; i < 10; ++i) {  // 打印前 10 个结果
        std::cout << "C[" << i << "] = " << h_C[i] << std::endl;
    }

    // 释放设备内存
    cudaFree(d_A);
    cudaFree(d_B);
    cudaFree(d_C);

    // 释放主机内存
    delete[] h_A;
    delete[] h_B;
    delete[] h_C;

    return 0;
}

方法一和矩阵加法类似,不再赘述

二、使用共享内存来优化矩阵乘法

#include <iostream>
#include <cuda_runtime.h>

#define TILE_SIZE 16  // 线程块的大小,16x16 的块

__global__ void matrixMultiplyWithSharedMemory(int *A, int *B, int *C, int N) {
    __shared__ int s_A[TILE_SIZE][TILE_SIZE];  // 用于存储矩阵 A 的共享内存
    __shared__ int s_B[TILE_SIZE][TILE_SIZE];  // 用于存储矩阵 B 的共享内存

    int row = blockIdx.y * TILE_SIZE + threadIdx.y;  // 当前线程处理的矩阵行
    int col = blockIdx.x * TILE_SIZE + threadIdx.x;  // 当前线程处理的矩阵列

    int value = 0;
    for (int i = 0; i < (N / TILE_SIZE); ++i) {
        // 将 A 和 B 的子块加载到共享内存
        s_A[threadIdx.y][threadIdx.x] = A[row * N + (i * TILE_SIZE + threadIdx.x)];
        s_B[threadIdx.y][threadIdx.x] = B[(i * TILE_SIZE + threadIdx.y) * N + col];

        __syncthreads();  // 确保所有线程都完成了共享内存加载

        // 执行子块的乘法
        for (int j = 0; j < TILE_SIZE; ++j) {
            value += s_A[threadIdx.y][j] * s_B[j][threadIdx.x];
        }

        __syncthreads();  // 等待所有线程完成本次子块计算
    }

    if (row < N && col < N) {
        C[row * N + col] = value;  // 将计算结果存储到矩阵 C 中
    }
}

int main() {
    int N = 1024;  // 矩阵的大小 N x N
    int size = N * N * sizeof(int);  // 每个矩阵的字节大小

    // 在主机上分配矩阵 A, B, C 的内存
    int *h_A = new int[N * N];
    int *h_B = new int[N * N];
    int *h_C = new int[N * N];

    // 初始化矩阵 A 和 B
    for (int i = 0; i < N * N; ++i) {
        h_A[i] = 1;  // 矩阵 A 初始化为 1
        h_B[i] = 2;  // 矩阵 B 初始化为 2
    }

    // 为设备上的矩阵 A, B, C 分配内存
    int *d_A, *d_B, *d_C;
    cudaMalloc(&d_A, size);
    cudaMalloc(&d_B, size);
    cudaMalloc(&d_C, size);

    // 将矩阵 A 和 B 从主机复制到设备
    cudaMemcpy(d_A, h_A, size, cudaMemcpyHostToDevice);
    cudaMemcpy(d_B, h_B, size, cudaMemcpyHostToDevice);

    // 设置线程块和网格的维度
    dim3 threadsPerBlock(TILE_SIZE, TILE_SIZE);  // 每个线程块 16x16 的线程
    dim3 blocksPerGrid((N + TILE_SIZE - 1) / TILE_SIZE, (N + TILE_SIZE - 1) / TILE_SIZE);  // 网格中块的数量

    // 启动内核进行矩阵乘法
    matrixMultiplyWithSharedMemory<<<blocksPerGrid, threadsPerBlock>>>(d_A, d_B, d_C, N);

    // 检查内核启动是否有错误
    cudaError_t error = cudaGetLastError();
    if (error != cudaSuccess) {
        std::cerr << "CUDA kernel launch failed: " << cudaGetErrorString(error) << std::endl;
        return -1;
    }

    // 等待设备上的所有计算完成
    cudaDeviceSynchronize();

    // 将结果从设备复制回主机
    cudaMemcpy(h_C, d_C, size, cudaMemcpyDeviceToHost);

    // 打印部分结果验证
    for (int i = 0; i < 10; ++i) {  // 打印前 10 个结果
        std::cout << "C[" << i << "] = " << h_C[i] << std::endl;
    }

    // 释放设备内存
    cudaFree(d_A);
    cudaFree(d_B);
    cudaFree(d_C);

    // 释放主机内存
    delete[] h_A;
    delete[] h_B;
    delete[] h_C;

    return 0;
}

先贴上代码。

第一步:申请主机内存

int N = 1024;  // 矩阵的大小 N x N
int size = N * N * sizeof(int);  // 每个矩阵的字节大小

// 在主机上分配矩阵 A, B, C 的内存
int *h_A = new int[N * N];
int *h_B = new int[N * N];
int *h_C = new int[N * N];

第二步:设备内存分配

cudaMalloc(&d_A, size);  // 在设备(GPU)上为矩阵 A 分配内存
cudaMalloc(&d_B, size);  // 在设备(GPU)上为矩阵 B 分配内存
cudaMalloc(&d_C, size);  // 在设备(GPU)上为矩阵 C 分配内存

        使用 cudaMalloc 在 GPU 上为矩阵 ABC 分配内存。每个矩阵的大小为 N * N * sizeof(int) 字节。

第三步:初始化数据

for (int i = 0; i < N * N; ++i) {
    h_A[i] = 1;  // 矩阵 A 初始化为 1
    h_B[i] = 2;  // 矩阵 B 初始化为 2
}

        这部分代码初始化了矩阵 A 和矩阵 BA 的每个元素都设置为 1,B 的每个元素都设置为 2。这里为了简单起见,矩阵值的初始化较为简单。

第四步:将数据从主机复制到设备

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

        使用 cudaMemcpy 将主机内存中的矩阵 AB 复制到 GPU 的设备内存中。这样,GPU 才能够访问这些数据进行计算。

第五步:内核函数编写

__global__ void matrixMultiplyWithSharedMemory(int *A, int *B, int *C, int N) {
    __shared__ int s_A[TILE_SIZE][TILE_SIZE];  // 用于存储矩阵 A 的共享内存
    __shared__ int s_B[TILE_SIZE][TILE_SIZE];  // 用于存储矩阵 B 的共享内存

    int row = blockIdx.y * TILE_SIZE + threadIdx.y;  // 当前线程处理的矩阵行
    int col = blockIdx.x * TILE_SIZE + threadIdx.x;  // 当前线程处理的矩阵列

    int value = 0;
    for (int i = 0; i < (N / TILE_SIZE); ++i) {
        // 将 A 和 B 的子块加载到共享内存
        s_A[threadIdx.y][threadIdx.x] = A[row * N + (i * TILE_SIZE + threadIdx.x)];
        s_B[threadIdx.y][threadIdx.x] = B[(i * TILE_SIZE + threadIdx.y) * N + col];

        __syncthreads();  // 确保所有线程都完成了共享内存加载

        // 执行子块的乘法
        for (int j = 0; j < TILE_SIZE; ++j) {
            value += s_A[threadIdx.y][j] * s_B[j][threadIdx.x];
        }

        __syncthreads();  // 等待所有线程完成本次子块计算
    }

    if (row < N && col < N) {
        C[row * N + col] = value;  // 将计算结果存储到矩阵 C 中
    }
}

        __shared__ 关键字表示该变量是在 共享内存 中分配的。共享内存是每个线程块(block)内部的高速内存,线程块内的所有线程都能访问共享内存。相比于全局内存(global memory),共享内存的访问速度快很多,因此在计算中合理利用共享内存可以大大提升性能。这里使用了 2 个 TILE_SIZE x TILE_SIZE 的二维数组 s_As_B 来存储矩阵 AB 的子块。这是因为 GPU 上的线程块(block)会负责处理矩阵的一个子块。

        blockIdx.xblockIdx.y 是当前线程块的网格坐标,表示线程块在网格中的位置。threadIdx.xthreadIdx.y 是当前线程在所在线程块中的位置。TILE_SIZE 是线程块的大小,这里设置为 16,表示每个线程块是 16x16 的线程矩阵。rowcol 用来计算每个线程对应矩阵 C 中的元素位置。每个线程负责计算结果矩阵中的一个元素。

        A[row * N + (i * TILE_SIZE + threadIdx.x)] 计算当前线程要从矩阵 A 中读取的元素的位置。B[(i * TILE_SIZE + threadIdx.y) * N + col] 计算当前线程要从矩阵 B 中读取的元素的位置。

第六步:内核调用与执行

dim3 threadsPerBlock(TILE_SIZE, TILE_SIZE);  // 每个线程块 16x16 的线程
dim3 blocksPerGrid((N + TILE_SIZE - 1) / TILE_SIZE, (N + TILE_SIZE - 1) / TILE_SIZE);  // 网格中块的数量

matrixMultiplyWithSharedMemory<<<blocksPerGrid, threadsPerBlock>>>(d_A, d_B, d_C, N);

        threadsPerBlock 设置每个线程块的线程数为 TILE_SIZE x TILE_SIZE,即每个线程块包含 16x16 个线程,共 256 个线程。blocksPerGrid 根据矩阵的大小和线程块的大小计算出所需的线程块数量,保证每个矩阵元素都有一个线程负责计算。内核函数 matrixMultiplyWithSharedMemory 被启动并执行。

        后面重复过程不再赘述。

三、一些问题的思考

1.在使用共享内存时,线程块内的所有线程都访问共享内存。如果多个线程同时访问共享内存的相同位置,会不会造成冲突?如果会,应该如何避免?

        多个线程同时访问共享内存的同一位置会导致冲突,称为 共享内存银行冲突。为了避免这种情况,CUDA 的共享内存被分为多个 银行,每个银行可以并行访问。但如果多个线程访问同一个银行的同一位置,就会发生冲突,影响性能。

        要避免共享内存银行冲突,可以通过调整内存访问模式,使得每个线程访问不同的内存位置。例如,通过改变 TILE_SIZE 的大小或者通过更精细的内存布局,确保不同线程访问不同的银行。

2.如果矩阵的维度 N 不是 TILE_SIZE 的倍数,如何处理那些无法被 TILE_SIZE 整除的部分?当前代码是否能有效处理这种情况?

        可以修改内核代码,在加载子块数据时,确保矩阵的边界被正确处理。例如,在加载 AB 子块时,检查 row < Ncol < N,并在矩阵 C 中写回计算结果时,确保只更新有效元素。

3.矩阵 AB 的加载顺序是否最优?是否有可能优化内存访问模式,减少不必要的内存访问延迟?

        对于矩阵 B,可以考虑对其进行 转置,使得每个线程加载相邻的内存位置,从而提高内存访问效率。转置后的 B 会使得线程块在加载数据时,线程按行访问,这可以减少不必要的内存访问延迟。对于矩阵 A,可以保持当前加载顺序,但对于矩阵 B,转置可能会提升性能。

4.内核的 for (int i = 0; i < (N / TILE_SIZE); ++i) 语句假设 NTILE_SIZE 的倍数。若 N 不能整除 TILE_SIZE,代码是否会遇到问题?如何处理这个问题?

        为了处理非 TILE_SIZE 倍数的情况,可以计算子块数量时向上取整(ceil)。例如,可以使用 (N + TILE_SIZE - 1) / TILE_SIZE 计算需要的子块数量,确保覆盖矩阵的所有部分。

5.当前代码每个线程块都需要从全局内存加载矩阵 AB 的子块到共享内存,是否有可能减少内存传输开销?

        减少全局内存访问的次数,可以通过将矩阵 AB 中的多个子块存储在共享内存中并重用,减少不必要的内存传输。另外,尝试通过合理设计 内存对齐内存访问模式 来进一步减少内存传输的开销。

5.可不可以将整个A和B矩阵整体传入共享内存然后每次取一个方块?

        CUDA 中的共享内存是有限制的。对于每个线程块(block),共享内存的大小通常是每个块的 16KB 或者 48KB,具体大小取决于 GPU 架构和计算能力(Compute Capability)。如果矩阵的大小 N 很大(例如 1024 x 1024),即使是存储一个完整的矩阵也会占用较多的共享内存。例如,N = 1024 时,每个矩阵 AB 的内存大小为 1024 x 1024 x sizeof(int),即 1024 x 1024 x 4 = 4MB,这显然远远超过了单个线程块的共享内存容量。因此,直接将整个矩阵加载到共享内存并不是一个可行的选择,除非矩阵的尺寸非常小,或者使用多个线程块来分担内存工作。

Logo

更多推荐