硬件的账只有三个数:算力、带宽、容量。 多数人只盯算力,可瓶颈常常在搬数据那一段。 这一节用一台没有显卡的机器,把「算力受限」和「带宽受限」分开算清楚。
13.1 讲怎么把活拆开,这一节讲单台机器的马力。拆得再漂亮,单机不够也是白搭。 上一节说「分布式多了一层网络」,这一节就把那层的账算出来: 多卡之间同步梯度走的是 PCIe,带宽只有十几 GB/s。
一、三个数,一个瓶颈
挑机器只看一个数,早晚会买错。硬件的账是三笔:
| 指标 | 单位 | 管什么 |
|---|---|---|
| 算力 | FLOPS | 每秒能算多少次 |
| 带宽 | GB/s | 每秒能搬多少字节 |
| 容量 | GB | 一次能装下多大的模型 |
| 这三个数里,只有一个卡住你。另外两个再高,也换不来速度。 | ||
| 麻烦的是:卡住你的是哪个,不看机器,看你要跑什么。 | ||
| 下面两段就是用来判断这件事的。 |
二、算力是怎么量出来的
办法很朴素:两个 512 乘 512 的单精度矩阵相乘。 一次要做 2 乘 n 的三次方次浮点运算,也就是 2.6844 亿次。 连跑二十次取最快:0.0011 秒。算下来 237.05 GFLOPS,也就是 0.24 TFLOPS。 GFLOPS 就是每秒十亿次浮点运算,这个数才是硬件的马力。
| 器件 | 算力 | 带宽 | 功耗 |
|---|---|---|---|
| 这台机器的 CPU | 0.24 TFLOPS | 约 50 GB/s | 65 瓦 |
| 入门级独显 | 约 8 TFLOPS | 约 300 GB/s | 120 瓦 |
| 训练级加速卡 | 约 20 TFLOPS | 约 900 GB/s | 300 瓦 |
| 沙箱里没有显卡,后两行是厂商规格,不是实测。 | |||
| 写清楚这一点,比编一个漂亮数字诚实。 | |||
| 但量级足够说明问题:算力差几十倍,带宽差十几倍。 | |||
| 所以「买张显卡就快了」这句话,只在瓶颈真的在算的时候成立。 |
三、算术强度:算一次要搬多少
算术强度=浮点运算次数除以搬运的字节数。这个比值决定瓶颈在算还是在搬。
| 操作 | 运算次数 | 搬的字节 | 算术强度 |
|---|---|---|---|
| 矩阵乘 512 乘 512 | 2.6844 亿 | 3.15 MB | 85.33 次/字节 |
| 向量加(逐元素) | 0.0026 亿 | 3.15 MB | 0.08 次/字节 |
| 逐元素比较大小 | 0.0026 亿 | 2.10 MB | 0.12 次/字节 |
| 矩阵乘 85.33 次每字节,向量加只有 0.08 次——差一千倍。 | |||
| 前者搬一个数能算很多次,搬运被计算掩盖;后者搬一个数只算一次,时间全花在路上。 | |||
| 拿算力除以带宽,得到这台机器的平衡点:4.74 次每字节。 | |||
| 算术强度高过它,算力是瓶颈;低过它,带宽是瓶颈。 | |||
| 所以同一张卡,跑矩阵乘是「算力受限」,跑逐元素操作是「带宽受限」。 | |||
| 调优之前先算这一个比值,能省掉一大半瞎试。 |
四、搬运的时间账
训练时每个 batch 都要从主机内存拷到显存,走的是 PCIe 3.0 乘 16,实测约 12 GB/s。 为了比得清楚,假设一个轻量模型每张图约一亿次浮点运算——粗估,只为看比例。
| batch | 数据量 | 拷贝 | CPU 算这批 | 拷贝占比 | 换 20 TFLOPS 卡 |
|---|---|---|---|---|---|
| 32 张 | 19.3 MB | 1.61 毫秒 | 13.50 毫秒 | 10.6% | 90.9% |
| 128 张 | 77.1 MB | 6.42 毫秒 | 54.00 毫秒 | 10.6% | 90.9% |
| 512 张 | 308.3 MB | 25.69 毫秒 | 215.99 毫秒 | 10.6% | 90.9% |
| 拷贝占比三行都是 10.6%:数据量和计算量都随 batch 线性涨,比值不变。 | |||||
| 所以调 batch 治不了这个瓶颈。能动的只有两件事:搬得更快,或者每张算得更多。 | |||||
| 换成 20 TFLOPS 的卡,同一个比例跳到九成以上。 | |||||
| 卡越快,数据管道越容易卡住——那些数据加载线程、预取、锁页内存,全是为这一行服务的。 |
五、动手 Demo:算得快不快,先看瓶颈在算还是在搬
五段:量 CPU 算力、显卡按规格算、算术强度与平衡点、PCIe 搬运的时间账、 配硬件时该问的三个问题。 没有显卡,所以第二段只能算不能量。 但「瓶颈在算还是在搬」这个问题,用 CPU 一样能讲透。
n = 512
A = np.random.default_rng(46).random((n, n)).astype(np.float32)
B = np.random.default_rng(47).random((n, n)).astype(np.float32)
best = 1e9
C = A @ B # 先空跑一次,把底层库初始化摊掉
for _ in range(20): # 连跑二十次取最快
t0 = time.perf_counter()
C = A @ B
t1 = time.perf_counter()
best = min(best, t1 - t0)
flops = 2.0 * n ** 3 # 一次乘加算两次运算
print(u"%.2f GFLOPS" % (flops / best / 1e9))
实跑输出:
--------------------------------------------------------------------
算得快不快,先看瓶颈在算还是在搬
--------------------------------------------------------------------
==============================================================
[1] 先量一下这台机器 CPU 的浮点算力
办法是最朴素的那个:拿两个 512 乘 512 的单精度矩阵相乘,数它做了多少次浮点运算。
一次 n 乘 n 乘 n 的矩阵乘,浮点运算次数是 2 乘 n 的三次方。
矩阵大小 512 乘 512
一次运算量 2.6844 亿次浮点运算
最快一次耗时 0.0011 秒
算力 237.05 GFLOPS
**GFLOPS 就是「每秒十亿次浮点运算」。这个数才是硬件的「马力」。**
**注意这是单精度、单线程、靠 numpy 调底层库跑出来的,**
**真实训练框架还能再快一些,但量级差不多。**
[2] 显卡的账:按规格算,不是实测
沙箱里没有显卡,所以这一段只能算,不能量。**按厂商公布的规格:**
器件 单精度算力 显存带宽 功耗
这台机器的 CPU 0.24 TFLOPS 约 50 GB/s 65 瓦
入门级独显 约 8 TFLOPS 约 300 GB/s 120 瓦
训练级加速卡 约 20 TFLOPS 约 900 GB/s 300 瓦
**算力差几十倍,带宽也差十几倍。**
**所以「买张显卡就快了」这句话,只在瓶颈真的在算的时候成立。**
**下面两段就来看瓶颈到底在哪。**
[3] 算术强度:算一次要搬多少数据
算术强度=浮点运算次数除以搬运的字节数。**这个比值决定瓶颈在算还是在搬。**
操作 运算次数 搬的字节 算术强度
矩阵乘 512 乘 512 2.6844 亿 3.15 MB 85.33 次/字节
向量加(逐元素) 0.0026 亿 3.15 MB 0.08 次/字节
逐元素比较大小 0.0026 亿 2.10 MB 0.12 次/字节
**矩阵乘是 85.3 次每字节,向量加只有 0.08 次——差一千倍。**
**差别在这里:矩阵乘每搬一个数要算很多次,搬运可以被计算掩盖;**
**向量加搬一个数只算一次,时间全花在搬运上。**
用 CPU 的带宽(约 50 GB/s)和算力(237.05 GFLOPS)算一下「每字节能算几次才打平」:
平衡点 4.74 次/字节
矩阵乘 远高于平衡点,算得动
向量加 远低于平衡点,等数据
**所以同一张卡,跑矩阵乘是「算力受限」,跑逐元素操作是「带宽受限」。**
**调优之前先算这个比值,能省掉一大半瞎试。**
[4] 数据从内存搬到显存要多久
训练时每个 batch 都要从主机内存拷到显存。按 PCIe 3.0 乘 16 的实测带宽约 12 GB/s 算:
为了比得清楚,假设一个轻量模型每张图约 1 亿次浮点运算(粗估,只为看比例)。
batch 数据量 拷贝 CPU 算这批 拷贝占比 换 20 TFLOPS 卡
32 张 19.3 MB 1.61 毫秒 13.50 毫秒 10.6% 90.9%
128 张 77.1 MB 6.42 毫秒 54.00 毫秒 10.6% 90.9%
512 张 308.3 MB 25.69 毫秒 215.99 毫秒 10.6% 90.9%
**拷贝占比那一列三行都是 10.6%——数据量和计算量都随 batch 线性涨,比值不变。**
**所以调 batch 治不了这个瓶颈。能动的只有两件事:搬得更快,或者每张算得更多。**
**换成快几十倍的卡,同一个比例会跳到九成以上——这就是「卡越快,数据管道越容易卡住」。**
**真实训练里那些数据加载线程、预取、锁页内存,全是为这一段服务的。**
[5] 配置硬件时该问的三个问题
**第一问:瓶颈在算还是在搬?**
拿算术强度和平衡点一比就知道。**逐元素多、矩阵乘少的模型,先加带宽不是加算力。**
**第二问:卡和卡之间怎么连?**
多卡训练时梯度要同步。**PCIe 走主机内存,带宽十几 GB/s;专用互联能到几百 GB/s。**
**八个卡挤一条 PCIe,同步时间能把加速比吃掉一半——这就是 8.4 那道通信墙。**
**第三问:内存和显存够不够装下模型?**
一个十亿参数的模型,单精度要 4 GB,**加上梯度和优化器状态,实际要三到四倍。**
**装不下就只能切分,而切分又要通信。三个问题是连在一起的。**
五段连起来:**先量 CPU 的算力当标尺**;**显卡按规格算,差几十倍**;
**算术强度决定瓶颈在算还是在搬**;**拷贝的时间会随卡变快而变得显眼**;
**所以配硬件不是挑最贵的,是先把瓶颈找准。**
第 1 段那个 0.0011 秒是「最快一次」,不是平均。 矩阵乘这种活受系统干扰大,取最快才接近这机器真实的上限。 第 3 段表格第三列(搬的字节)最容易被忽略。 同样一批数,矩阵乘每个数要读好几遍,算术强度的差别就差在这个分母上。 第 4 段的「拷贝占比」三行一模一样,这是这一节的核心: 比例跟 batch 无关,只跟「每字节能算几次」有关。 最后一段那三个问题是有顺序的。 先问瓶颈、再问卡怎么连、最后问装不装得下——装不下才需要切分,切分才引出通信。
30 秒实验
- 把第 1 段的 n 从 512 改成 1024,重跑。 运算量涨八倍,耗时不涨八倍——盘子越大越能喂饱底层库,GFLOPS 会往上走一点。
- 把第 3 段的向量加换成「同一个数组连做一千次逐元素乘法」,重跑。 算术强度会抬上去,因为搬一次能算很多次——这就从带宽受限挪回了算力受限。
- 把第 4 段的带宽从 12 GB/s 改成 300 GB/s(相当于卡内显存带宽),重跑。 拷贝占比掉到 1% 以下,瓶颈重新回到算力上,这时候加算力才有意义。
常见误区
- 以为「GPU 一定比 CPU 快」。逐元素的小操作在 GPU 上可能更慢—— 数据搬过去的时间比算的时间还长,算术强度太低。
- 以为「算力翻倍时间就减半」。先看算术强度。带宽受限的活,算力翻十倍也一秒不省。
- 以为「batch 越大越好」。大 batch 不改变拷贝占比,还会吃掉显存、影响收敛。 它是权衡,不是越大越优。
- 以为「显存只装权重就够了」。梯度和优化器状态常要三到四倍空间。 装不下就得切分,而切分又要通信——三个问题是连在一起的。
自测三问(点开对答案)
1. 为什么同一张卡跑矩阵乘快、跑向量加慢? —— 因为**算术强度不同**。矩阵乘 85.33 次每字节,远高于平衡点 4.74,算力是瓶颈; **向量加 0.08,远低于平衡点,主要在等数据。** **同一个硬件,两种瓶颈。** 2. 拷贝占比为什么调 batch 治不了? —— 因为**数据量和计算量都随 batch 线性涨,比值不变**,三行都是 10.6%。 **要动它只能提高带宽,或者让每张图算得更多。** 3. 「平衡点」这个数怎么用? —— **拿模型里主要操作的算术强度和它比。** **高于它就加算力,低于它就加带宽。** **先算这一个比值,比上来就换卡靠谱得多。**硬件配好了,接下来是装在上面的那层软件。 下一节:同一份代码为什么在两个人机器上跑出两种结果。 答案在依赖、版本和镜像里。
- ☐ (本节完,下一节 13.3 软件配置)