【问题标题】:CUDA: Tiled matrix-matrix multiplication with shared memory and matrix size which is non-multiple of the block sizeCUDA:具有共享内存的平铺矩阵-矩阵乘法,矩阵大小不是块大小的倍数
【发布时间】:2013-09-19 20:43:22
【问题描述】:

我正在努力让自己熟悉 CUDA 编程,并且玩得很开心。我目前正在查看this pdf,它处理矩阵乘法,无论是否使用共享内存。可以在here 找到这两个版本的完整代码。此代码与 CUDA 矩阵乘法示例中的代码几乎完全相同。尽管非共享内存版本能够以任何矩阵大小运行,无论块大小如何,共享内存版本必须使用块大小的倍数的矩阵(我设置为 4,默认值最初是 16) .

pdf 末尾建议的问题之一是更改它,以便共享内存版本也可以处理非块大小的倍数。我认为这将是一个简单的索引检查,就像在非共享版本中一样:

int row = blockIdx.y * blockDim.y + threadIdx.y;
int col = blockIdx.x * blockDim.x + threadIdx.x;
if(row > A.height || col > B.width) return;

但这不起作用。这是完整的代码,减去了 main 方法(有点乱,抱歉),我对它进行了一些修改:

void MatMul(const Matrix A, const Matrix B, Matrix C) { 
  // Load A and B to device memory 
  Matrix d_A; 
  d_A.width = d_A.stride = A.width; 
  d_A.height = A.height; 
  size_t size = A.width * A.height * sizeof(float); 
  cudaError_t err = cudaMalloc(&d_A.elements, size); 
  printf("CUDA malloc A: %s\n",cudaGetErrorString(err)); 
  err = cudaMemcpy(d_A.elements, A.elements, size, cudaMemcpyHostToDevice); 
  printf("Copy A to device: %s\n",cudaGetErrorString(err)); 

  Matrix d_B; 
  d_B.width = d_B.stride = B.width; 
  d_B.height = B.height; 
  size = B.width * B.height * sizeof(float); 
  err = cudaMalloc(&d_B.elements, size); 
  printf("CUDA malloc B: %s\n",cudaGetErrorString(err));
  err = cudaMemcpy(d_B.elements, B.elements, size, cudaMemcpyHostToDevice);
  printf("Copy B to device: %s\n",cudaGetErrorString(err)); 

  Matrix d_C; 
  d_C.width = d_C.stride = C.width; 
  d_C.height = C.height; 
  size = C.width * C.height * sizeof(float); 
  err = cudaMalloc(&d_C.elements, size); 
  printf("CUDA malloc C: %s\n",cudaGetErrorString(err));

  dim3 dimBlock(BLOCK_SIZE, BLOCK_SIZE); 
    dim3 dimGrid((B.width + dimBlock.x - 1) / dimBlock.x, (A.height + dimBlock.y-1) / dimBlock.y);
    MatMulKernel<<<dimGrid, dimBlock>>>(d_A, d_B, d_C); 
    err = cudaThreadSynchronize();
    printf("Run kernel: %s\n", cudaGetErrorString(err));

  // Read C from device memory 
  err = cudaMemcpy(C.elements, d_C.elements, size, cudaMemcpyDeviceToHost); 
  printf("Copy C off of device: %s\n",cudaGetErrorString(err));

  // Free device memory
  cudaFree(d_A.elements); 
  cudaFree(d_B.elements); 
  cudaFree(d_C.elements); 
} 

// Get a matrix element
__device__ float GetElement(const Matrix A, int row, int col) { 
  return A.elements[row * A.stride + col]; 
} 

// Set a matrix element 
__device__ void SetElement(Matrix A, int row, int col, float value) { 
  A.elements[row * A.stride + col] = value; 
} 

// Get the BLOCK_SIZExBLOCK_SIZE sub-matrix Asub of A that is 
// located col sub-matrices to the right and row sub-matrices down 
// from the upper-left corner of A 
__device__ Matrix GetSubMatrix(Matrix A, int row, int col) { 
  Matrix Asub; 
  Asub.width = BLOCK_SIZE; 
  Asub.height = BLOCK_SIZE; 
  Asub.stride = A.stride; 
  Asub.elements = &A.elements[A.stride * BLOCK_SIZE * row + BLOCK_SIZE * col]; 
  return Asub; 
}


// Matrix multiplication kernel called by MatMul() 
__global__ void MatMulKernel(Matrix A, Matrix B, Matrix C) { 
  // Block row and column 
  int blockRow = blockIdx.y; 
  int blockCol = blockIdx.x; 

  int rowTest = blockIdx.y * blockDim.y + threadIdx.y;
  int colTest = blockIdx.x * blockDim.x + threadIdx.x;
  if (rowTest>A.height || colTest>B.width)
    return;
  // Each thread block computes one sub-matrix Csub of C
  Matrix Csub = GetSubMatrix(C, blockRow, blockCol); 

  // Each thread computes one element of Csub 
  // by accumulating results into Cvalue 
  float Cvalue = 0.0; 
  // Thread row and column within Csub 
  int row = threadIdx.y; 
  int col = threadIdx.x; 
  // Loop over all the sub-matrices of A and B that are 
  // required to compute Csub 
  // Multiply each pair of sub-matrices together 
  // and accumulate the results 
  for (int m = 0; m < (BLOCK_SIZE + A.width - 1)/BLOCK_SIZE; ++m) {
    // Get sub-matrix Asub of A 
    Matrix Asub = GetSubMatrix(A, blockRow, m); 

    // Get sub-matrix Bsub of B 
    Matrix Bsub = GetSubMatrix(B, m, blockCol); 

    // Shared memory used to store Asub and Bsub respectively 
    __shared__ float As[BLOCK_SIZE][BLOCK_SIZE]; 
    __shared__ float Bs[BLOCK_SIZE][BLOCK_SIZE]; 

    // Load Asub and Bsub from device memory to shared memory 
    // Each thread loads one element of each sub-matrix 
    As[row][col] = GetElement(Asub, row, col); 
    Bs[row][col] = GetElement(Bsub, row, col); 

    // Synchronize to make sure the sub-matrices are loaded 
    // before starting the computation 
    __syncthreads(); 

    // Multiply Asub and Bsub together 
    for (int e = 0; e < BLOCK_SIZE; ++e) 
    {
      Cvalue += As[row][e] * Bs[e][col];
    }
    // Synchronize to make sure that the preceding 
    // computation is done before loading two new 
    // sub-matrices of A and B in the next iteration 
    __syncthreads();  
  }
  // Write Csub to device memory 
  // Each thread writes one element 
  SetElement(Csub, row, col, Cvalue); 
}

我改变的值得注意的事情:我在 MatMulKernel 中添加了一个检查,检查我们当前的线程是否正在尝试在 C 中不存在的位置上工作。这似乎不起作用。虽然它确实改变了结果,但这些变化似乎没有任何模式,除了后来(更高的 x 或 y 值)条目似乎受到更大的影响(而且我得到了更多的非整数结果)。我还更改了给定的 dimGrid 计算方法和 MatMulKernel 中 m 的循环条件(之前它只是宽度或高度除以块大小,这似乎是错误的)。

即使我为本指南找到的解决方案指南似乎也暗示它应该只是一个简单的索引检查,所以我认为我错过了一些真正基本的东西。

【问题讨论】:

    标签: c matrix cuda


    【解决方案1】:

    当矩阵尺寸不是图块尺寸的倍数时,可能会发生某些图块仅部分覆盖矩阵的情况。落在不完全重叠的瓦片之外的瓦片元素应正确归零。因此,将您的代码扩展到任意大小的矩阵很容易,但并不意味着简单的索引检查。下面,我正在复制并粘贴我的平铺矩阵-矩阵乘法内核版本,其中包含任意大小的矩阵

    __global__ void MatMul(float* A, float* B, float* C, int ARows, int ACols, int BRows,
        int BCols, int CRows, int CCols)
    {
        float CValue = 0;
    
        int Row = blockIdx.y*TILE_DIM + threadIdx.y;
        int Col = blockIdx.x*TILE_DIM + threadIdx.x;
    
        __shared__ float As[TILE_DIM][TILE_DIM];
        __shared__ float Bs[TILE_DIM][TILE_DIM];
    
        for (int k = 0; k < (TILE_DIM + ACols - 1)/TILE_DIM; k++) {
    
             if (k*TILE_DIM + threadIdx.x < ACols && Row < ARows)
                 As[threadIdx.y][threadIdx.x] = A[Row*ACols + k*TILE_DIM + threadIdx.x];
             else
                 As[threadIdx.y][threadIdx.x] = 0.0;
    
             if (k*TILE_DIM + threadIdx.y < BRows && Col < BCols)
                 Bs[threadIdx.y][threadIdx.x] = B[(k*TILE_DIM + threadIdx.y)*BCols + Col];
             else
                 Bs[threadIdx.y][threadIdx.x] = 0.0;
    
             __syncthreads();
    
             for (int n = 0; n < TILE_DIM; ++n)
                 CValue += As[threadIdx.y][n] * Bs[n][threadIdx.x];
    
             __syncthreads();
        }
    
        if (Row < CRows && Col < CCols)
            C[((blockIdx.y * blockDim.y + threadIdx.y)*CCols) +
               (blockIdx.x * blockDim.x)+ threadIdx.x] = CValue;
    }
    

    【讨论】:

    • 我针对普通的 cuda 实现(不使用共享内存)运行了这段代码,惊讶地发现这两种方法所花费的时间几乎相同。我期待一个很好的加速,因为共享内存的使用通常会缩短执行时间。
    • @rajeerc 我们正在讨论的代码只不过是一个练习,说明如何将共享内存用于具体的测试用例(平铺矩阵-矩阵乘法)。它并不代表实现矩阵-矩阵乘法的最佳方式:cuBLAS 做了一些更复杂的事情。也就是说,您没有观察到任何改进的原因是,对于您正在运行的 GPU 架构,L1 缓存已经完成了共享内存的全部工作。请记住,除了非常古老的架构之外,共享内存可以被视为受控缓存。
    • 无论如何我可以使用这个共享内存方法来超过正常的 cuda 实现的运行时间吗? (不去 cuBLAS)
    • 可能有点晚了,但是如果你仍然有兴趣看看 Winograd Algorithm for Gemm,CLblast 也是一个很好的 Open CL Gemm 库。
    • 嗨,我正在尝试修改此内核,以便将矩阵 B 替换为 A 并像访问它一样访问它,以便执行 A*A_T 。有谁知道我该怎么做?
    猜你喜欢
    • 1970-01-01
    • 2017-03-29
    • 2013-12-20
    • 2018-05-20
    • 2020-12-22
    • 1970-01-01
    • 2011-05-02
    • 2021-06-02
    相关资源
    最近更新 更多