为什么不同流中的内核执行不并行?

时间:2019-04-28 16:04:23

标签: c++ cuda gpu-programming

我刚刚在CUDA中学习了流技术,并尝试了它。然而,不希望的结果返回,即,流不是并行的。 (在GPU Tesla M6和OS Red Hat Enterprise Linux 8上)

我有一个大小为(5,2048)的数据矩阵,还有一个处理该矩阵的内核。

我的计划是分解“ nStreams = 4”个扇区中的数据,并使用4个流来并行执行内核。

我的部分代码如下:

int rows = 5;
int cols = 2048;

int blockSize = 32;
int gridSize = (rows*cols) / blockSize;
dim3 block(blockSize);
dim3 grid(gridSize);

int nStreams = 4;    // preparation for streams
cudaStream_t *streams = (cudaStream_t *)malloc(nStreams * sizeof(cudaStream_t));
for(int ii=0;ii<nStreams;ii++){
    checkCudaErrors(cudaStreamCreate(&streams[ii]));
}

int streamSize = rows * cols / nStreams;
dim3 streamGrid = streamSize/blockSize;

for(int jj=0;jj<nStreams;jj++){
    int offset = jj * streamSize;
    Mykernel<<<streamGrid,block,0,streams[jj]>>>(&d_Data[offset],streamSize);
}    // d_Data is the matrix on gpu

Visual Profiler结果显示4个不同的流不是并行的。流13是第一个工作的流,而流16是最后一个工作的流。流13和流14之间有12.378us。每个内核执行大约持续5us。在上面的“运行时API”行中,它显示为“ cudaLaunch”。

能给我一些建议吗?谢谢!

(我不知道如何在stackoverflow中上传图片,所以我只用文字描述结果。)

2 个答案:

答案 0 :(得分:5)

首先,不能保证在单独的流中启动的内容实际上将在GPU上并行执行。由于pointed out in the programming guide,使用多个流只是打开了可能性,因此您不能依靠它实际发生的事情。由驾驶员决定。

除此之外,如果我没记错的话,您的Tesla M6有12个多处理器。这12个Maxwell多处理器中的每一个最多可容纳32个驻留块。这使整个设备上驻留的最大块总数达到384。您正在启动320个块,每个块32个线程。仅此一项并不会留下太多的空间,您可能每个线程使用32个以上的寄存器,因此通过这些启动中的一个启动,GPU将非常满,这很可能是驱动程序选择不运行另一个内核的原因并行。

并行内核启动主要在您拥有时才有意义,例如,一堆执行不同任务的小内核可以在单独的多处理器上彼此并行运行。看来您的工作量可以轻松填满整个设备。您究竟希望通过并行运行多个内核来实现什么?为什么要使用这么小的模块?将整个程序作为具有更大块的大内核启动会更有意义吗?通常,您希望每个块至少有几个扭曲。例如,请参见以下问题,以了解更多信息:How do I choose grid and block dimensions for CUDA kernels?如果您正在使用共享内存,则每个多处理器还需要至少两个块,否则您将无法在某些GPU上使用所有块。 (例如,每个多处理器提供96 KiB共享内存,但每个块最多只能有48 KiB共享内存)……

答案 1 :(得分:3)

要添加到现有答案(完全正确)中,请考虑以下简单完整版本的问题代码:

__global__
void Mykernel(float* data, int size)
{
    int tid = threadIdx.x + blockIdx.x * blockDim.x;

    for(; tid < size; tid+= blockDim.x * gridDim.x) data[tid] = 54321.f;
}

int main()
{
    int rows = 2048;
    int cols = 2048;

    int blockSize = 32;
    dim3 block(blockSize);

    int nStreams = 4;    // preparation for streams
    cudaStream_t *streams = (cudaStream_t *)malloc(nStreams * sizeof(cudaStream_t));
    for(int ii=0;ii<nStreams;ii++){
        cudaStreamCreate(&streams[ii]);
    }

    float* d_Data;
    cudaMalloc(&d_Data, sizeof(float) * rows * cols);
    int streamSize = rows * cols / nStreams;
    dim3 streamGrid = dim3(4);

    for(int jj=0;jj<nStreams;jj++){
        int offset = jj * streamSize;
        Mykernel<<<streamGrid,block,0,streams[jj]>>>(&d_Data[offset],streamSize);
    }    // d_Data is the matrix on gpu


    cudaDeviceSynchronize();
    cudaDeviceReset();
}

请注意两个区别-通过将rows设置为2048,可以减少每个内核启动的块数,并增加每个线程的总计算量。内核本身包含一个网格步长循环,该循环允许线程来处理多个输入,无论启动多少个块/线程,都确保处理整个输入数据集。

在与您的设备类似的Maxwell GPU上进行配置显示如下:

enter image description here

即内核确实重叠。现在让我们将问题的大小减小到问题中指定的大小(行数= 5):

enter image description here

内核不再重叠。为什么?因为驱动程序和设备的等待时间足够长,并且每个内核的执行时间都足够短,所以即使设备资源允许,也没有执行时间重叠。因此,除了在另一个答案中描述的资源需求限制之外,计算量必须足够大以抵消与调度流中的内核启动相关的固定等待时间。

最后,我建议设置基于流的并发执行方案的正确方法应如下所示:

int blockSize = 32;
dim3 block(blockSize);
int blocksperSM, SMperGPU = 13; // GPU specific
cudaOccupancyMaxActiveBlocksPerMultiprocessor(&blocksperSM, Mykernel, blockSize, 0); // kernel specific
dim3 streamGrid = blocksperSM * (SMperGPU / nStreams); // assume SMperGPU >> nstreams   

这里,想法是在流中(大致)平均分配可用SM的数量,并通过占用API为内核获取针对所选块大小最大占用每个SM的块数量。

此配置文件如下:

enter image description here

rows = 2048的情况下,通过将内核的资源要求与GPU的容量正确匹配,既可以产生重叠,又可以缩短执行时间。