哪些问题很适合GPU计算?


84

因此,对于要解决的问题,最好的解决方法是串行解决,并且可以并行管理。但是现在,我对基于CPU的计算可以最好地处理什么以及应该将哪些内容卸载到GPU的想法还不多。

我知道这是一个基本问题,但是我的大部分搜索工作都被清楚地主张一个或另一个的人们所困扰,而他们并没有真正证明为什么或有些模糊的经验法则。在这里寻找更有用的回应。

Answers:


63

GPU硬件具有两个特殊优势:原始计算(FLOP)和内存带宽。最困难的计算问题属于这两类之一。例如,密集的线性代数(A * B = C或Solve [Ax = y]或Diagonalize [A]等)取决于系统大小而落在计算/内存带宽频谱上。快速傅里叶变换(FFT)也适合具有高总带宽需求的这种模型。与其他转换一样,基于网格/网格的算法,蒙特卡洛等也是如此。如果您查看NVIDIA SDK 代码示例,则可以了解最常解决的各种问题。

我认为对以下问题的回答更具指导性:“ GPU真正擅长于哪些问题?” 大多数不属于此类的问题都可以在GPU上运行,尽管有些问题比其他问题花费更多的精力。

不能很好地映射的问题通常太小或太不可预测。非常小的问题缺少使用GPU上所有线程所需的并行性,并且/或者可能适合CPU的低级缓存,从而大大提高了CPU性能。不可预测的问题有太多有意义的分支,它们可能阻止数据有效地从GPU内存流传输到内核,或者通过破坏SIMD范式来减少并行性(请参阅“ 发散扭曲 ”)。这些问题的示例包括:

  • 大多数图算法(太不可预测了,尤其是在内存空间中)
  • 稀疏的线性代数(但这对CPU也是不利的)
  • 小信号处理问题(例如,小于1000点的FFT)
  • 搜索
  • 分类

3
尽管如此,针对那些“无法预测的”问题的GPU解决方案仍然可能的,尽管目前通常不可行,但将来可能会变得越来越重要。
左右左转

6
我想专门向GPU性能破坏者列表添加分支。您希望所有(数百个)执行同一条指令(如在SIMD中)执行真正的并行计算。例如,在AMD卡上,如果任何指令流遇到分支并且必须发散-所有波前(并行组)发散。如果波前的其他单位不能发散,则必须执行第二遍。我猜这就是maxhutch的可预测性。
紫罗兰色长颈鹿2012年

2
@VioletGiraffe,不一定是真的。在CUDA中(即在Nvidia GPU上),分支分歧仅影响当前的扭曲,最多32个线程。尽管执行相同的代码,但不同的扭曲不会同步,除非显式同步(例如与同步__synchtreads())。
佩德罗(Pedro)

1
@Pedro:是的,但是一般来说分支确实会影响性能。对于高性能代码(不是什么GPU代码?),几乎必须考虑到这一点。
jvriesem 2015年

21

具有较高算术强度和常规内存访问模式的问题通常很容易在GPU上实现,并且在GPU上表现良好。

拥有高性能GPU代码的基本困难是您拥有大量的内核,并且您希望它们全部被充分利用。内存访问模式不规则或算术强度不高的问题使此问题变得很困难:要么花费很长时间交流结果,要么花费很长时间从内存中获取内容(这很慢!),并且没有足够的时间处理数字。当然,代码中潜在的并发性对于其在GPU上良好实现的能力也至关重要。


您可以指定常规内存访问模式的含义吗?
Fomite 2012年

1
maxhutch的答案比我的要好。我所说的常规访问模式是指以时间和空间局部方式访问内存。那就是:您不会在内存周围反复跳跃。这也是我注意到的一揽子交易。这也意味着可以通过编译器或程序员来预先确定您的数据访问模式,以使分支(代码中的条件语句)最小化。
Reid.Atcheson,2012年

15

这并不是要单独回答,而是maxhutchReid.Atcheson的其他回答的补充

为了充分利用GPU,您的问题不仅需要高度(或大规模)并行,而且要在GPU上执行的核心算法应尽可能小。在OpenCL术语中,这通常称为内核

更准确地说,内核应适合GPU 的每个多处理单元(或计算单元)的寄存器。寄存器的确切大小取决于GPU。

如果内核足够小,则问题的原始数据需要适合GPU的本地内存(读取:计算单元的本地内存(OpenCL)或共享内存(CUDA))。否则,即使GPU的高内存带宽也不够快,无法始终保持处理元素繁忙。
通常,此内存大约为16至32 KiByte


每个处理单元的本地/共享内存是否不是在单个内核集群中运行的所有数十个线程之间共享的?在这种情况下,您是否真的不需要为了减少GPU的全部性能而使工作数据集显着缩小的情况?
Dan Neely 2012年

处理单元的本地/共享存储器只能由计算单元本身访问,因此只能由该计算单元的处理元件共享。所有处理单元均可访问图形卡的全局内存(通常为1GB)。处理单元与本地/共享内存之间的带宽非常快(> 1TB / s),但是到全局内存的带宽要慢得多(〜100GB / s),需要在所有计算单元之间共享。
Torbjörn

我不是在问主要的GPU内存。我以为on-die内存仅分配给核心级别的群集,而不是每个核心。例如nVidia GF100 / 110 gpu;对于16个SM群集中的每个群集,不是512个cuda内核。每个SM最多可并行运行32个线程,以最大化GPU性能,需要将工作集保持在1kb /线程范围内。
Dan Neely 2012年

@Torbjoern您想要的是使所有GPU执行管道保持繁忙,GPU实现了以下两种方式:(1)最常见的方式是通过增加并发线程数来增加占用率,或者换句话说(小内核使用更少的并发线程)。共享资源,以便您可以拥有更多活动线程);也许更好的方法是(2)增加内核中的指令级并行度,以便您可以拥有占用率相对较低(活动线程数较少)的较大内核。参见bit.ly/Q3KdI0
fcruz 2012年

11

可能是对先前答复的一种更为技术性的补充:CUDA(即Nvidia)GPU可描述为一组处理器,它们各自独立地在32个线程上工作。每个处理器中的线程都以锁步方式工作(请考虑使用长度为32的向量的SIMD)。

尽管使用GPU的最诱人的方法是假装绝对一切都以步调一致的方式运行,但这并不总是最有效的处理方式。

如果您的代码不能很好地/自动地并行处理成百上千的线程,则可以将其分解为可以很好并行化的单个异步任务,并仅以锁步方式运行32个线程来执行这些任务。CUDA提供了一组原子指令,这些原子指令使实现互斥锁成为可能,而互斥锁又允许处理器在它们之间进行同步并处理线程池范式中的任务列表。然后,您的代码将以与多核系统上相同的方式工作,只是要记住,每个核都拥有自己的32个线程。

这是一个使用CUDA的小例子,说明它是如何工作的

/* Global index of the next available task, assume this has been set to
   zero before spawning the kernel. */
__device__ int next_task;

/* We will use this value as our mutex variable. Assume it has been set to
   zero before spawning the kernel. */
__device__ int tasks_mutex;

/* Mutex routines using atomic compare-and-set. */
__device__ inline void cuda_mutex_lock ( int *m ) {
    while ( atomicCAS( m , 0 , 1 ) != 0 );
    }
__device__ inline void cuda_mutex_unlock ( int *m ) {
    atomicExch( m , 0 );
    }

__device__ void task_do ( struct task *t ) {

    /* Do whatever needs to be done for the task t using the 32 threads of
       a single warp. */
    }

__global__ void main ( struct task *tasks , int nr_tasks ) {

    __shared__ task_id;

    /* Main task loop... */
    while ( next_task < nr_tasks ) {

        /* The first thread in this block is responsible for picking-up a task. */
        if ( threadIdx.x == 0 ) {

            /* Get a hold of the task mutex. */
            cuda_mutex_lock( &tasks_mutex );

            /* Store the next task in the shared task_id variable so that all
               threads in this warp can see it. */
            task_id = next_task;

            /* Increase the task counter. */
            next_tast += 1;

            /* Make sure those last two writes to local and global memory can
               be seen by everybody. */
            __threadfence();

            /* Unlock the task mutex. */
            cuda_mutex_unlock( &tasks_mutex );

            }

        /* As of here, all threads in this warp are back in sync, so if we
           got a valid task, perform it. */
        if ( task_id < nr_tasks )
            task_do( &tasks[ task_id ] );

        } /* main loop. */

    }

然后,您必须使用调用内核,main<<<N,32>>>(tasks,nr_tasks)以确保每个块仅包含32个线程,从而适合单个线程束。在此示例中,为简单起见,我还假定任务没有任何依赖关系(例如,一个任务取决于另一任务的结果)或冲突(例如,在同一全局内存上工作)。如果是这种情况,那么任务选择会变得有些复杂,但是结构基本相同。

当然,这不仅比在一大批单元上完成所有操作还要复杂,而且还大大拓宽了可使用GPU的问题类型。


2
从技术上讲这是正确的,但是需要很高的并行度才能获得较高的内存带宽,并且异步内核调用的数量受到限制(当前为16个)。Thee也是与当前版本中的调度有关的大量未记录的行为。我建议暂时不要依赖异步内核来改善性能……
Max Hutchinson

2
我要描述的内容可以在一个内核调用中全部完成。您可以制作N个块,每个块包含32个线程,这样每个块可放入一个扭曲中。然后,每个块从全局任务列表(使用原子/互斥体进行访问控制)中获取任务,并使用32个锁步线程来计算任务。所有这些都在单个内核调用中发生。如果您想要一个代码示例,请告诉我,我会发布一个。
2012年

4

到目前为止,没有提到的一点是,当前一代的GPU在双精度浮点计算上的性能不如单精度计算。如果必须以双精度完成计算,则可以预计运行时间将比单精度增加10倍左右。


我不同意。大多数(或所有)较新的GPU都具有本机双精度支持。几乎每个此类GPU都报告双精度计算,其运行速度约为单精度的一半,这可能是由于所需内存访问/带宽的简单加倍。
Godric Seer 2013年

1
诚然,最新,最出色的Nvidia Tesla卡确实提供了峰值双精度性能,是峰值单精度性能的一半,而对于更常见的费米架构消费级卡,该比例为8:1。
Brian Borchers

@GodricSeer SP和DP浮点数的2:1比例与带宽几乎没有关系,几乎与存在多少硬件单元来执行这些操作无关。通常将寄存器文件重用于SP和DP,因此浮点单元可以执行2倍的SP op作为DP op。此设计有许多例外,例如IBM Blue Gene / Q(不具有SP逻辑,因此SP的运行速度约为DP的1.05倍)。一些的GPU具有比2其他比率,例如在3和5
杰夫

自从我写出这个答案已经四年了,而NVIDIA GPU的当前情况是对于GeForce和Quadro系列,DP / SP比率现在是1/32。NVIDIA的Tesla GPU具有更强的双精度性能,但价格也要高得多。另一方面,AMD并没有以相同的方式削弱其Radeon GPU的双精度性能。
Brian Borchers

4

从隐喻的角度来看,gpu可以看作是一个躺在指甲床上的人。躺在上面的人是数据,每个钉子的底部都有一个处理器,因此钉子实际上是从处理器指向内存的箭头。所有指甲都呈规则图案,例如网格。如果身体散布良好,则感觉良好(性能良好),如果身体仅接触指甲床的某些部位,则疼痛较差(性能较差)。

这可以作为上述出色答案的补充答案。


4

这是个老问题,但我认为2014年的答案(与统计方法有关,但对任何知道什么是循环的人都可以推广)特别具有说明性和参考价值。


2

GPU具有较长的延迟I / O,因此需要使用大量线程来饱和内存。要保持经纱繁忙,需要大量线程。如果代码路径是10个时钟,而I / O延迟是320个时钟,则32个线程应该接近使线程束饱和。如果代码路径为5个时钟,则将线程加倍。

具有一千个内核,需要寻找数千个线程来充分利用GPU。

内存访问是通过高速缓存行进行的,通常为32个字节。加载一个字节的成本相当于32个字节。因此,合并存储以增加使用位置。

每个扭曲都有很多寄存器和本地RAM,以允许邻居共享。

大集合的邻近度仿真应该可以很好地进行优化。

随机I / O和单线程是一种杀戮的乐趣...


这是一个真正令人着迷的问题。我正在与自己争论一个问题:当每个任务花费约0.06秒但有约180万个任务要执行时,是否有可能(或值得努力)“并行化”一个相当简单的任务(航空影像中的边缘检测)。每年6年的数据价值:这些任务绝对是可分离的)...因此,一个内核的计算时间约为7.5天。如果每个计算在GPU上都更快,并且可以将作业并行化为每nGPU核心1个(n个小),那么作业时间实际上是否有可能下降到大约1小时?似乎不太可能。
GT。

0

想象一个可以通过很多蛮力解决的问题,例如Traveling Salesman。然后想象一下,您的服务器机架上各有8个spanky视频卡,每个卡具有3000个CUDA内核。

只需解决所有可能的售货员的路线,然后对时间/距离/某些度量进行排序。当然,您会浪费掉几乎100%的工作,但强力暴力有时是可行的解决方案。


我一周可以访问一个由4个这样的服务器组成的小型服务器场,并且在过去5天中,与过去十年相比,我做了更多的分布式.net块。
Criggie '16

-1

通过研究许多工程学思想,我想说gpu是一种专注于任务,内存管理,可重复计算的形式。

许多公式可能很容易编写,但计算起来很麻烦,例如在矩阵数学中,您不会得到一个单一的答案,而是会有很多值。

这在计算中很重要,因为计算机计算值和运行公式的速度非常快,因为某些公式无法在没有所有计算值的情况下运行(因此速度变慢)。计算机不太清楚在这些程序中运行公式或计算值的顺序。它主要是通过蛮力以快速的速度运行,并将公式分解为块,以进行计算,但是如今,许多程序现在都需要这些已计算的块,并等待que(以及que和que que)。

例如,在模拟游戏中,应该首先在碰撞中计算碰撞的损坏,物体的位置,新的速度?这应该花多少时间?任何CPU如何处理此负载?而且,大多数程序都是非常抽象的,需要更多时间来处理数据,并且并非总是针对多线程而设计的,或者不是抽象程序中有效实现此目的的任何好方法。

随着cpu变得越来越好,越来越多的人开始草率编程,我们也必须为许多不同类型的计算机编程。一个gpu旨在通过许多简单的计算同时进行暴力破解(更不用说内存(次要/ ram)和加热冷却是计算的主要瓶颈)。一个cpu可以同时管理多个任务,或者被拉向多个方向,因此正在寻找无法执行的操作。(嘿,几乎是人类)

一个gpu是笨拙的工人的繁琐工作。CPU正在处理完全混乱的情况,无法处理所有细节。

那我们学到什么呢?一个gpu一次完成细节繁琐的工作,而一个cpu是一台多任务机器,无法很好地处理太多任务。(就像它同时具有注意力障碍和自闭症)。

工程中有想法,设计,现实和大量艰巨的工作。

在我离开时,请记住从简单开始,快速,快速失败,快速失败,并且永不停止尝试。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.