第二讲:硬件基础——GPU架构概览、NVLink与PCIe总线、内存层次结构与带宽特性
各位同学好,我是你们的老朋友。
这一讲,咱们聊聊硬件。别急着打哈欠,我知道很多人觉得硬件是搞底层的人该操心的。但说句实在话,做高性能计算,不懂硬件就像开车不看仪表盘——能跑,但跑不远。
我见过太多人,代码写得花里胡哨,一上GPU就跑不满。为什么?因为压根没搞懂数据是怎么在芯片里流动的。今天咱们就把这事掰扯清楚。
2.1 GPU架构概览——它到底长啥样?
先问个问题:CPU和GPU最大的区别在哪?
CPU像个全能运动员,什么都能干,但一次只能干几件事。GPU呢?它像个流水线工厂,每个工人只干一件事,但工人数量多到吓人。
拿NVIDIA的Ampere架构举例(就是A100那个架构),它的核心设计思路是这样的:
- SM(Streaming Multiprocessor):这是GPU的基本计算单元。一个A100有108个SM,每个SM里又有64个FP32 CUDA Core。你算算,总共多少?6912个核心。
- Warp:32个线程绑在一起,组成一个warp。这是GPU调度和执行的最小单位。记住这个数字——32,后面会反复用到。
- Tensor Core:专门为矩阵乘法设计的硬件单元。搞AI训练的人靠它吃饭。一个Tensor Core一次能做一个4x4矩阵乘法,效率是普通CUDA Core的几十倍。
关键点:GPU的并行度来自大量SM同时工作。但每个SM内部的资源(寄存器、共享内存)是有限的。你开的线程越多,每个线程分到的资源就越少。这是个trade-off。
我个人习惯,拿到一个新GPU,第一件事就是查它的SM数量和每个SM的资源上限。这决定了你能开多大的并行度。
2.2 NVLink与PCIe总线——数据怎么搬?
好,芯片内部搞清楚了。那多块GPU之间怎么通信?这就涉及到两个关键角色:PCIe和NVLink。
2.2.1 PCIe总线——老黄牛
PCIe是通用总线,什么设备都能往上插。但通用就意味着不专精。
PCIe Gen4 x16的理论带宽是32 GB/s(双向64 GB/s)。听起来还行?但实际用起来,延迟高、带宽共享,多卡通信时经常成为瓶颈。
我曾经在一个项目里,用4块V100做分布式训练。数据量一大,PCIe就成了堵车的高速公路。后来一测,通信开销占了总时间的40%。你说气不气人?
| PCIe版本 | x16单向带宽 | 典型延迟 |
|---|---|---|
| PCIe Gen3 | 16 GB/s | ~1 μs |
| PCIe Gen4 | 32 GB/s | ~0.5 μs |
| PCIe Gen5 | 64 GB/s | ~0.3 μs |
2.2.2 NVLink——专线快车
NVLink是NVIDIA自己搞的互联技术。说白了,就是给GPU之间修了条专用高速公路。
A100上的NVLink 3.0,每根链路带宽50 GB/s(单向),双向100 GB/s。一块A100有12根NVLink链路,总带宽高达600 GB/s。这比PCIe快了将近20倍。
实战技巧:如果你的多卡程序跑得慢,先看看是不是走了PCIe。用nvidia-smi topo -m命令可以查看GPU之间的拓扑结构。如果发现两个GPU之间没有NVLink直连,那通信效率会大打折扣。
嗯,这里要注意:NVLink虽然快,但它只在同一节点内的GPU之间有效。跨节点的通信,还是得走网络(比如InfiniBand)。
2.3 内存层次结构与带宽特性——数据住哪儿?
这是最容易被忽视,但也是最影响性能的部分。我把它叫做「数据的地产经济学」——位置决定价值,距离决定速度。
2.3.1 全局内存(Global Memory)
就是GPU上的显存。容量大(A100有40GB/80GB),但速度慢。带宽虽然高(A100约2 TB/s),但延迟也高(几百纳秒)。
你想想看,一个线程从全局内存读一个数,要等几百个周期。这期间GPU能干多少事?所以,尽量减少全局内存访问,这是优化的第一原则。
2.3.2 共享内存(Shared Memory)
每个SM内部有一小块高速缓存。A100每个SM有192 KB共享内存。速度极快,延迟只有几个周期。
共享内存是手动管理的——你得自己把数据从全局内存搬进来,用完再搬出去。麻烦吗?麻烦。但值得。
避坑指南:我曾经在项目里,为了省事,所有数据都直接从全局内存读。结果一个kernel跑了20毫秒。后来改成用共享内存做数据复用,同样的计算,只用了3毫秒。差距就是这么大。
2.3.3 寄存器(Register)
最快的内存,没有之一。每个线程都有自己的寄存器文件。A100每个SM有65536个32位寄存器。
但寄存器是稀缺资源。你开的线程越多,每个线程能用的寄存器就越少。一旦寄存器不够用,编译器会把数据「溢出」到本地内存(Local Memory),而本地内存其实是在全局内存里——速度瞬间掉到谷底。
2.3.4 缓存(Cache)
L1缓存和共享内存共用同一块物理存储。A100上,你可以配置成L1多、共享少,或者反过来。L2缓存是所有SM共享的,A100有40 MB。
缓存是自动管理的,但你可以通过数据访问模式来影响它的行为。比如,尽量让同一个warp内的线程访问连续的内存地址——这叫「合并访问」,能最大化利用缓存带宽。
2.4 带宽特性——别被理论值骗了
厂商宣传的带宽,都是理论峰值。实际能跑到多少,取决于你的代码怎么写。
举个例子:A100的HBM2e显存,理论带宽2 TB/s。但如果你让每个线程随机访问不同的地址,实际带宽可能连100 GB/s都不到。为什么?因为内存控制器忙不过来了。
我总结了几条实战经验:
- 合并访问是王道:同一个warp的32个线程,尽量访问连续的内存地址。这样一次内存请求就能搞定,带宽利用率最高。
- 避免bank conflict:共享内存被分成32个bank。如果多个线程同时访问同一个bank,就会冲突,导致串行化。解决办法?数据填充或者调整访问模式。
- 计算和通信要重叠:这是咱们这门课的核心。用CUDA Stream,让数据搬运和计算同时进行。别让GPU闲着等数据。
核心公式:实际带宽 = (数据量) / (传输时间)。别只看理论值,用cudaMemcpy实测一下,你就知道你的代码到底吃满了多少带宽。
好了,硬件基础就讲这么多。这些东西看着枯燥,但它们是后续所有优化技巧的根基。你想想看,不理解内存层次结构,你怎么知道该把数据放哪?不理解NVLink,你怎么设计多卡通信策略?
下一讲,咱们会动手写代码,把这些硬件特性用起来。到时候你会发现,原来性能瓶颈往往不在计算本身,而在数据搬运上。
想要更多资料,联系可可