公众号关注 “ML_NLP
设为 “星标”,重磅干货,第一时间送达!
来自 | 知乎 作者 | XZiar
链接|https://zhuanlan.zhihu.com/p/60627768
本文仅作学术分享,若侵权,请联系后台删文处理
长久以来都有这么一种说法
CPU是大学教授,做的是高数难题。GPU是一群小学生,做的是普通加减乘除。你出小学数学GPU当然算的快,你出高数GPU就懵了。
那么这种说法对吗?
这次用一张图来反驳吧。
Fermi架构一个SM的示意图
看到右侧的SFU了嘛,special function unit。它可以做sin/cos/sqrt/reciprocal这类运算,且每周期每线程吞吐一条指令(不过精度有限)。
而在CPU上,fsincos是x87指令集的东西,sse/avx里都没有现成的sin/cos运算指令,硬件上自然更不会有专门的运算单元。
更别提纹理单元和polymorph engine这种GPU专用的东西了。
那么,谁才是看见高数就懵逼的小学生?
其实我并不是想说文章开头的言论一文不值。只是我觉得有必要更深入去思考一下CPU和GPU的差异。
GPU的起源,与其说是从CPU分裂出去,不如说是从专用到通用的改变。
一开始GPU的概念也不存在,显示芯片主要意义在于输出视频信号。
就连2D兴起的时候,显示芯片的运算能力也没有得到重视——2D嘛,你在java里弄个Bitmap各种draw一下不也差不多?
直到3D时代来临,一个物体要在空间里经过变换,还要进行光照处理,数学运算能力的重要性才得以体现。而且一开始这些运算也是在CPU上完成的,显卡更大的意义在于光栅化。当时的OpenGL1.x跑在CPU上也是很正常的情况。
GPU这个词是NVIDIA发布Geforce256时提出的,当时内部集成了硬件固化的T&L单元,故名思意就是做空间变换与光照处理。至此,GPU的浮点运算能力算是有了开端。
虽然显卡的地位被稳固下来,但很长时间以来也没有人想要拿CPU和GPU进行对比。毕竟GPU不仅仅是运算那么简单,几何体的变换裁剪,光栅化,纹理的读取,后期AA……这些功能过于专用化,CPU只能模拟,显然是吃亏的。
当时的显卡把运算单元划分给顶点和像素两个阶段,不同显卡为两者分配的计算资源比例不同,也使其在不同游戏下表现出不同的性能。而你到现在还能看到图形API流水线里的VS、PS阶段。直到DX10开始,GPU的架构迈入通用架构,流处理器的概念被提出,GPGPU开始兴起,GPU这才终于有了和CPU比较的基础。
既然从历史角度上看,GPU是不断通用化,那为什么它和完全“通用”的CPU间的性能差异反而越来越大了呢?
这可以从几个角度去考虑:

1.硬件配置

GPU的一大特点就是“核多”。比如GTX760就有1152个CUDA core。
这意味着GTX760有1152个核心吗?那倒不是。CUDA core其实就是SP ALU,做单精度浮点运算罢了,并不能看作一个完整的核心。
而kepler架构的基本单位是一个SMX。每个SMX由192个CUDA core,32个SFU等等组成。而每个SMX又具有4个warp scheduler,每个warp scheduler又有2个dispatch unit。每个dispatch unit可以分派1个warp,每个warp包含32个thread……
这么算下来GTX760能同时跑32x2x4x6=1536个线程咯?怎么还比1152大了?
这可能有点乱,但其实这是可以一一对应到CPU上的。
首先1个wrap就是CPU上真正的线程,再往下细分的32个“线程”,其实就是SIMD32罢了(毕竟一个wrap同一时间只能跑在一条指令上)。2个dispatch unit其实就是超线程技术,而每个warp scheduler就是一个“推土机线程”。1个SMX即4个warp scheduler就是一个“推土机模块”(运算单元是一个模块里共享的)。
这样算下来GTX760其实也就是个带超线程的6模块24核48线程,支持AVX1024的加强版推土机CPU(AMD yes,笑)。
很多人还会谈及缓存,这部分差异也的确很大。
首先GPU有大量寄存器,每个SMX有64k个32位RF,共计2M大小。考虑到capability3.0的每线程32寄存器限制,相当于每个SMX同时利用4byte*32RF*32*8wrap=32k。而L1 cache和shared memory共享,共计64KB。L2是所有SMX共享的,对于GK104来说是512KB。
所以等效过来的CPU就是32K L1,64K L2, 512K L3。
缓存部分的差距倒是显而易见,不过更小的缓存应该意味着更低的性能才对啊……而且就算GTX760的硬件配置的确超出了主流CPU,也没有多了不起,毕竟企业级服务器里24C48T不少见。那为什么CPU还是打不过GPU?
别急,看下去。

2.工作负载

现在该考虑一下工作负载的问题了。
GPU就是适合做并行量大的工作。渲染一帧画面,多边形数上百万,互不影响(理想条件)。最终画面1080p,像素数上百万,也互不影响(理想条件)。这种良好的并行负载使得GPU的架构能发挥出巨大实力。
CPU也并非没有这种使用场景,不然xeon的堆核核AVX512意义何在。
Xeon Platinum 8160就是一款可以拿来和GTX760相比较的产品。24C48T,支持AVX512,24核全开上限2GHz,对比GTX760的“48T,AVX1024, 1GHz”,纸面性能应该差不多(当然理论性能其实应该考虑ALU个数而不是表面的线程数)。
事实上两者的单精度运算能力的确差不多,都在2TFlops左右。从这一角度看,CPU的确能追上GPU,靠的也是同样的堆核。更别说kepler阉割了双精度运算,也不支持半精度运算。
但是换成几乎完全不能并行化的工作负载,例如48个在做不同工作的线程时,结果就不一样了,毕竟AVX起不了作用,最终变成主频的战争。Xeon能让每个线程跑在2.xGHz,而GTX760只能跑在1.xGHz,更别说消费级CPU频率更高。这也使得GPU再通用也难以和CPU匹敌。

3.设计理念

真的可以把GTX760和Xeon 8160拿来互相比较吗?
两者缓存大小有着巨大不同,这其实也体现着设计理念的区别,而这才是关键。
又是这张经典的图
GPU为了提高效率,可以阉割不怎么必要的双精度性能,可以大幅砍掉整数运算性能,也可以去掉大量缓存——在高度并行化的前提下,访存的延迟可以通过流水线隐藏。
举个例子,餐厅打饭需要选菜、盛菜、付款多个步骤,每个步骤1s需要3s。但如果大家排队紧密执行,由于每个步骤互不干扰,N个人只要N+2s,相当于每个人只需要花1s。
GPU也就是这个思路,砍了缓存可以拿来放ALU,降低功耗还提升运算能力。至于访存的延迟靠工作负载的高度并行与高度局部性来隐藏。这也使得并行度不够时这些问题都会暴露出来,带来更低的性能(高端卡在低分辨率下性能不佳也可以这么解释)。
同样被阉割掉的还有调度单元。CPU做了大量分支预测、乱序执行的组件,但GPU却大量依赖编译器。
前面我把GPU的寄存器对应到了CPU的L1,GPU的L1对应到了CPU的L2,降了一级。其实这是不对的。寄存器只能直接寻址,而缓存可以用offset来间接寻址。
换句话说,程序跑在GPU上需要编译时确定寄存器的使用,这对编译器提出了更大的挑战。如果编译器没处理好,那巨大的寄存器资源就会被浪费,其相对于缓存更快的访问速度也会被浪费。
对于纯运算的程序还好说,但日常程序充斥着各类分支跳转,这仿佛击中了GPU的死穴。
有了以上的基础,其他的一些问题也就可以解释了。从Fermi到Kepler到Pascal到Turing,GPU的缓存其实一直在加大。毕竟现在的shader也很复杂,纹理读取的压力也很大,缓存还是省不了,Intel的gen11也是加了缓存。CPU这边Intel新架构也是加大了缓存,苹果的A系列也一向是大核心大缓存。
再比如CppCon上的C++20无栈协程的演讲,其实可以理解为GPU上的SIMT,能够以更低的开销切换线程,从而更充分利用CPU的计算资源。
总而言之,CPU和GPU的区别真的不是教授和小学生这么简单。
比较纯理论计算性能,GPU无论在最大性能还是单位功耗上的确都更胜一筹。但这是GPU作为更专用化设备的先天优势。
如果真要举一个形象的例子,你可以把CPU当作自己开店的裁缝,GPU则是制衣流水线的工人吧。两者在技能上没什么差别,但前者手脚更麻利,还懂得应对顾客多变的需求,后者虽然人多力量大,还省去了许多经营开销,但做起生意来还是反应慢一拍吧。
点击下方卡片,关注公众号“机器学习算法与自然语言处理”,获取更多信息:
继续阅读
阅读原文