繁体   English   中英

如何在 cuda 中获得并行 arrays 的“总和”?

[英]How to get "sum" of parallel arrays in cuda?

我的问题是关于获得相同长度 arrays 的“总和”。 例如,我总共有一个 M*N(100 * 2000) 长度的浮点数组。 我想获得每个 N(2000) 浮点数的 M(100) 和值。 我找到了两种方法来完成这项工作。 一种是在 M 的 for 循环中使用 Cublas function,例如cublasSasum 另一种是自写的kernel function,循环加数字。 我的问题是这两种方式的速度以及如何在它们之间进行选择。

对于Cublas方法,无论N(4000~2E6)有多大,耗时主要取决于循环数M。

对于自写的狗窝function,速度随N变化很大。具体来说,如果N很小,低于5000,它比Cublas方式运行得快得多。 然后时间消耗随着N的增加而增加。

N = 4000 |10000 | 40000 | 80000 | 1E6 | 2E6

t = 254ms| 422ms | 1365毫秒| 4361ms| 5399 毫秒 | 10635毫秒

如果 N 足够大,它的运行速度会比 Cublas 方式慢得多。 我的问题是我怎么能用 M 或 N 来决定我应该使用哪种方式? 我的代码可能用于不同的 GPU 设备。 我必须比较扫描参数中的速度,然后“猜测”以在每个 GPU 设备中做出选择,还是可以从 GPU 设备信息中推断?

此外,对于 kernel function 方式,我在决定blockSizegridSize时也有问题。 我必须在这里指出,我更关心的是速度而不是效率。 因为 memory 是有限的。 例如,如果我得到 8G memory。 我的数据格式是 4 个字节的浮点数。 N是1E5。 那么 M 最多为 2E4,小于MaxGridSize 所以我有两种方法如下。 我发现有一个更大的 gridSize 总是更好,我不知道原因。 是关于每个线程的寄存器号的使用吗? 但我认为在这个 kernel function 中每个线程不需要很多寄存器。

任何建议或信息将不胜感激。 谢谢你。

库布拉斯方式

for (int j = 0;j<M;j++)
    cublasStatus = cublasSasum(cublasHandle,N,d_in+N*j,1,d_out+j);

自写kernel方式

__global__ void getSum(int M, int N, float* in, float * out)
{
    int i = threadIdx.x + blockIdx.x * blockDim.x;
    if(i<M){
        float tmp = 0;
        for(int ii = 0; ii<N; ii++){
            tmp += *(in+N*i+ii);
        }
        out[i] = tmp;
    }
}

更大的 gridSize 更快。 我不知道原因。

getSum<<<M,1>>>(M, N, d_in, d_out); //faster
getSum<<<1,M>>>(M, N, d_in, d_out); 

这是一个blockSize-time参数扫描的结果。 M = 1E4.N = 1E5。

cudaEventRecord(start, 0);
//blockSize = 1:1024;
int gridSize = (M + blockSize - 1) / blockSize;
getSum<<<gridSize1,blockSize1>>>...
cudaEventRecord(stop, 0);
cudaEventSynchronize(stop);
cudaEventElapsedTime(&time, start, stop);

看来我应该选择一个相对较小的blockSize ,比如 10~200。 我只是想知道为什么完全占用(blockSize 1024)比较慢。 我只是出于一些可能的原因在这里发帖,注册号码?延迟? 在此处输入图像描述

使用 CuBLAS 通常是一个非常好的主意,如果有专用的 function 可以直接满足您的需求,则应该首选,尤其是对于大型数据集。 话虽如此,对于在如此小的数据集上工作的 GPU kernel 来说,您的时间安排非常糟糕。 让我们理解为什么。

更大的 gridSize 更快。 我不知道原因。
getSum<<<M,1>>>(M, N, d_in, d_out);
getSum<<<1,M>>>(M, N, d_in, d_out);

调用 CUDA kernel 的语法是kernel<<<numBlocks, threadsPerBlock>>> 因此,第一行提交了一个 kernel,其中包含 M 个 1 个线程的块。 不要那样做:这是非常低效的。 事实上, CUDA 编程手册说:

NVIDIA GPU 架构围绕可扩展的多线程流式多处理器(SM) 阵列构建。 当主机 CPU 上的 CUDA 程序调用 kernel 网格时,将枚举网格的块并将其分发到具有可用执行能力的多处理器。 一个线程块的线程在一个多处理器上并发执行,多个线程块可以在一个多处理器上并发执行。 当线程块终止时,新块在空出的多处理器上启动。 [...]
多处理器以 32 个并行线程组(称为warp )的形式创建、管理、调度和执行线程。 [...]
一个 warp 一次执行一条公共指令,因此当一个 warp 的所有 32 个线程都同意它们的执行路径时,就可以实现完全的效率 如果 warp 的线程通过依赖于数据的条件分支发散,则 warp 执行所采用的每个分支路径,禁用不在该路径上的线程 分支分歧只发生在一个扭曲内; 不同的 warp 独立执行,无论它们是执行公共的还是不相交的代码路径。

结果,第一次调用创建了M个 1 线程块,浪费了每个 warp 中 32 个可用的 31 个 CUDA 内核。 这意味着您可能只会读取 GPU 峰值性能的 3%...

第二个调用创建一个M线程块。 由于M不是 32 的倍数,因此浪费了很少的 CUDA 内核。 此外,它仅使用 1 个 SM,而不是 GPU 上的许多可用模块,因为您只有一个块。 现代 GPU 有几十个 SM(我的 GTX-1660S 有 22 个 SM)。 这意味着您将仅使用 GPU 功能的一小部分(几 %)。 更不用说 memory 访问模式不是连续减慢计算速度......

如果您想更有效地使用您的 GPU,您需要提供更多并行性减少资源浪费 您可以从编写 kernel 开始,在 2D 网格上使用 atomics 执行归约 这并不完美,但比您的初始代码要好得多。 您还应该注意连续读取 memory (共享相同扭曲的线程应该读取/写入连续的内存块)。

在编写 CUDA 代码之前,请仔细阅读CUDA 手册或教程。 它很好、准确地描述了这一切。


更新:

根据新信息,您正在尝试使用blockSize的问题可能是由于kernel中的跨步 memory 访问(更具体地说是N*i )。 跨步 memory 访问模式很慢,并且当跨度变大时通常会更慢。 在您的 kernel 中,每个线程将访问 memory 中的不同块。 如前所述,GPU(实际上是大多数硬件计算单元)已针对访问连续数据块进行了优化。 如果你想解决这个问题并获得更快的结果,你需要在另一个维度上并行工作(所以不是M而是N )。

此外,BLAS 调用效率低下,因为 CPU 上循环的每次迭代都会调用 GPU 上的 kernel。 调用 kernel 会引入相当大的开销(通常从几微秒到 ~100 微秒)。 因此,在称为数万次的循环中执行此操作将非常慢。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM