将矩阵复制到主机所花费的时间增加了矩阵的使用次数

时间:2017-09-01 00:05:32

标签: python cuda benchmarking pycuda

我使用PyCUDA,CUDAMat和Numba对GPU矩阵乘法进行基准测试,并遇到了一些我无法解释的行为。
我独立计算3个不同步骤所需的时间 - 将2个矩阵发送到设备存储器,计算点积,并将结果复制回主机存储器。
点积步骤的基准测试是在循环中完成的,因为我的应用程序将在发送结果之前进行多次乘法。

随着循环次数的增加,点积时间会按预期线性增加。但我无法理解的部分是,将最终结果发送回主机内存所需的时间也随循环计数线性增加,即使它只是将一个矩阵复制回主机内存。无论你做多少矩阵乘法循环,结果的大小都是常数,所以这没有意义。它的行为就好像返回最终结果需要在循环中的每一步返回所有中间结果。

需要注意的一些有趣的事情是,它所花费的时间的增加有一个高峰。当我在一个循环中超过~1000个点产品时,复制最终结果所需的时间达到峰值。 另一件事是,如果在dot产品循环内部我重新初始化保存结果的矩阵,则无论进行多少次乘法,此行为都会停止,复制时间也相同。
例如 -

for i in range(1000):
    gc = gpuarray.empty((MATRIX_SIZE, MATRIX_SIZE), np.float32)
    matrixmul(ga, gb, gc, grid=(MATRIX_SIZE // TILE_SIZE, MATRIX_SIZE // TILE_SIZE), block=(TILE_SIZE, TILE_SIZE, 1))
result = gc.get()

最后需要注意的是,PyCUDA和Numba都会发生这种情况,但CUDAMat不会发生这种情况。我可以做一百万次乘法,并且检索最终结果仍然需要相同的时间。 CUDAMat有一个内置矩阵乘法,这可能是原因,但对于PyCUDA和Numba,我使用他们自己的文档中提供的矩阵乘法代码。

这是我的PyCUDA代码

from __future__ import division
import numpy as np
from pycuda import driver, compiler, gpuarray, tools
import time
import pycuda.autoinit

kernel_code_template = """
__global__ void MatrixMulKernel(float *A, float *B, float *C)
{

  const int wA = %(MATRIX_SIZE)s;
  const int wB = %(MATRIX_SIZE)s;  

  // Block index
  const int bx = blockIdx.x;
  const int by = blockIdx.y;

  // Thread index
  const int tx = threadIdx.x;
  const int ty = threadIdx.y;

  // Index of the first sub-matrix of A processed by the block
  const int aBegin = wA * %(BLOCK_SIZE)s * by;
  // Index of the last sub-matrix of A processed by the block
  const int aEnd = aBegin + wA - 1;
  // Step size used to iterate through the sub-matrices of A
  const int aStep = %(BLOCK_SIZE)s;

  // Index of the first sub-matrix of B processed by the block
  const int bBegin = %(BLOCK_SIZE)s * bx;
  // Step size used to iterate through the sub-matrices of B
  const int bStep = %(BLOCK_SIZE)s * wB;

  // The element of the block sub-matrix that is computed
  // by the thread
  float Csub = 0;
  // Loop over all the sub-matrices of A and B required to
  // compute the block sub-matrix
  for (int a = aBegin, b = bBegin;
       a <= aEnd;
       a += aStep, b += bStep) 
    {
      // Shared memory for the sub-matrix of A
      __shared__ float As[%(BLOCK_SIZE)s][%(BLOCK_SIZE)s];
      // Shared memory for the sub-matrix of B
      __shared__ float Bs[%(BLOCK_SIZE)s][%(BLOCK_SIZE)s];

      // Load the matrices from global memory to shared memory
      // each thread loads one element of each matrix
      As[ty][tx] = A[a + wA * ty + tx];
      Bs[ty][tx] = B[b + wB * ty + tx];
      // Synchronize to make sure the matrices are loaded
      __syncthreads();

      // Multiply the two matrices together;
      // each thread computes one element
      // of the block sub-matrix
      for (int k = 0; k < %(BLOCK_SIZE)s; ++k)
        Csub += As[ty][k] * Bs[k][tx];

      // 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 the block sub-matrix to global memory;
  // each thread writes one element
  const int c = wB * %(BLOCK_SIZE)s * by + %(BLOCK_SIZE)s * bx;
  C[c + wB * ty + tx] = Csub;
}
"""


MATRIX_SIZE = 512
TILE_SIZE = 8
BLOCK_SIZE = TILE_SIZE
np.random.seed(100)
a_cpu = np.random.randn(MATRIX_SIZE, MATRIX_SIZE).astype(np.float32)
b_cpu = np.random.randn(MATRIX_SIZE, MATRIX_SIZE).astype(np.float32)

kernel_code = kernel_code_template % {
    'MATRIX_SIZE': MATRIX_SIZE,
    'BLOCK_SIZE': BLOCK_SIZE,
}
mod = compiler.SourceModule(kernel_code)
matrixmul = mod.get_function("MatrixMulKernel")


#copy to device memory
total = time.clock()
ga = gpuarray.to_gpu(a_cpu)
gb = gpuarray.to_gpu(b_cpu)
gc = gpuarray.empty((MATRIX_SIZE, MATRIX_SIZE), np.float32)
copy_to = time.clock() - total

#matrix multiplication
mult = time.clock()
for i in range(1000):
    matrixmul(ga, gb, gc, grid=(MATRIX_SIZE // TILE_SIZE, MATRIX_SIZE // TILE_SIZE), block=(TILE_SIZE, TILE_SIZE, 1))
mult = time.clock() - mult

#copy result back to host memory
copy_from = time.clock()
res = gc.get()
copy_from = time.clock() - copy_from
total = time.clock() - total

#print out times for all 3 steps and the total time taken
print(copy_to)
print(mult)
print(copy_from)
print(total)

1 个答案:

答案 0 :(得分:2)

GPU内核启动是异步。这意味着您认为围绕for循环捕获的测量(执行乘法所需的时间)并非如此。这只是将内核启动发送到队列所需的时间。

实际的内核执行时间正在被吸收&#34;进入设备的最终测量 - >主机复制时间(因为D-> H复制强制所有内核在开始之前完成,并且它会阻塞CPU线程)。

关于&#34;峰值&#34;行为,当你将足够的内核发送到队列中时,它最终会停止变为异步并开始阻塞CPU线程,因此你的执行时间会很长。测量开始上升。这解释了变化的峰值行为。

To&#34; fix&#34;这个,如果您在for循环后立即插入pycuda driver.Context.synchronize(),并在此行之前:

mult = time.clock() - mult 

当您增加for循环时,您将看到执行时间增加,并且您的D-> H复制时间将保持不变。