Answers:
GPU硬件具有两个特殊优势:原始计算(FLOP)和内存带宽。最困难的计算问题属于这两类之一。例如,密集的线性代数(A * B = C或Solve [Ax = y]或Diagonalize [A]等)取决于系统大小而落在计算/内存带宽频谱上。快速傅里叶变换(FFT)也适合具有高总带宽需求的这种模型。与其他转换一样,基于网格/网格的算法,蒙特卡洛等也是如此。如果您查看NVIDIA SDK 代码示例,则可以了解最常解决的各种问题。
我认为对以下问题的回答更具指导性:“ GPU真正擅长于哪些问题?” 大多数不属于此类的问题都可以在GPU上运行,尽管有些问题比其他问题花费更多的精力。
不能很好地映射的问题通常太小或太不可预测。非常小的问题缺少使用GPU上所有线程所需的并行性,并且/或者可能适合CPU的低级缓存,从而大大提高了CPU性能。不可预测的问题有太多有意义的分支,它们可能阻止数据有效地从GPU内存流传输到内核,或者通过破坏SIMD范式来减少并行性(请参阅“ 发散扭曲 ”)。这些问题的示例包括:
__synchtreads()
)。
具有较高算术强度和常规内存访问模式的问题通常很容易在GPU上实现,并且在GPU上表现良好。
拥有高性能GPU代码的基本困难是您拥有大量的内核,并且您希望它们全部被充分利用。内存访问模式不规则或算术强度不高的问题使此问题变得很困难:要么花费很长时间交流结果,要么花费很长时间从内存中获取内容(这很慢!),并且没有足够的时间处理数字。当然,代码中潜在的并发性对于其在GPU上良好实现的能力也至关重要。
这并不是要单独回答,而是maxhutch和Reid.Atcheson的其他回答的补充。
为了充分利用GPU,您的问题不仅需要高度(或大规模)并行,而且要在GPU上执行的核心算法应尽可能小。在OpenCL术语中,这通常称为内核。
更准确地说,内核应适合GPU 的每个多处理单元(或计算单元)的寄存器。寄存器的确切大小取决于GPU。
如果内核足够小,则问题的原始数据需要适合GPU的本地内存(读取:计算单元的本地内存(OpenCL)或共享内存(CUDA))。否则,即使GPU的高内存带宽也不够快,无法始终保持处理元素繁忙。
通常,此内存大约为16至32 KiByte 大。
可能是对先前答复的一种更为技术性的补充: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的问题类型。
到目前为止,没有提到的一点是,当前一代的GPU在双精度浮点计算上的性能不如单精度计算。如果必须以双精度完成计算,则可以预计运行时间将比单精度增加10倍左右。
GPU具有较长的延迟I / O,因此需要使用大量线程来饱和内存。要保持经纱繁忙,需要大量线程。如果代码路径是10个时钟,而I / O延迟是320个时钟,则32个线程应该接近使线程束饱和。如果代码路径为5个时钟,则将线程加倍。
具有一千个内核,需要寻找数千个线程来充分利用GPU。
内存访问是通过高速缓存行进行的,通常为32个字节。加载一个字节的成本相当于32个字节。因此,合并存储以增加使用位置。
每个扭曲都有很多寄存器和本地RAM,以允许邻居共享。
大集合的邻近度仿真应该可以很好地进行优化。
随机I / O和单线程是一种杀戮的乐趣...
想象一个可以通过很多蛮力解决的问题,例如Traveling Salesman。然后想象一下,您的服务器机架上各有8个spanky视频卡,每个卡具有3000个CUDA内核。
只需解决所有可能的售货员的路线,然后对时间/距离/某些度量进行排序。当然,您会浪费掉几乎100%的工作,但强力暴力有时是可行的解决方案。
通过研究许多工程学思想,我想说gpu是一种专注于任务,内存管理,可重复计算的形式。
许多公式可能很容易编写,但计算起来很麻烦,例如在矩阵数学中,您不会得到一个单一的答案,而是会有很多值。
这在计算中很重要,因为计算机计算值和运行公式的速度非常快,因为某些公式无法在没有所有计算值的情况下运行(因此速度变慢)。计算机不太清楚在这些程序中运行公式或计算值的顺序。它主要是通过蛮力以快速的速度运行,并将公式分解为块,以进行计算,但是如今,许多程序现在都需要这些已计算的块,并等待que(以及que和que que)。
例如,在模拟游戏中,应该首先在碰撞中计算碰撞的损坏,物体的位置,新的速度?这应该花多少时间?任何CPU如何处理此负载?而且,大多数程序都是非常抽象的,需要更多时间来处理数据,并且并非总是针对多线程而设计的,或者不是抽象程序中有效实现此目的的任何好方法。
随着cpu变得越来越好,越来越多的人开始草率编程,我们也必须为许多不同类型的计算机编程。一个gpu旨在通过许多简单的计算同时进行暴力破解(更不用说内存(次要/ ram)和加热冷却是计算的主要瓶颈)。一个cpu可以同时管理多个任务,或者被拉向多个方向,因此正在寻找无法执行的操作。(嘿,几乎是人类)
一个gpu是笨拙的工人的繁琐工作。CPU正在处理完全混乱的情况,无法处理所有细节。
那我们学到什么呢?一个gpu一次完成细节繁琐的工作,而一个cpu是一台多任务机器,无法很好地处理太多任务。(就像它同时具有注意力障碍和自闭症)。
工程中有想法,设计,现实和大量艰巨的工作。
在我离开时,请记住从简单开始,快速,快速失败,快速失败,并且永不停止尝试。