本机 BLAS 实测 120 GFLOPS,7B 每字要 15.2 GFLOP,按算力算上限是 8 tok/s; 按内存带宽算只有 5.5 tok/s。 真正卡住本地推理的不是算力,是带宽——这两个数差出来的部分,就是「为什么显卡快」。
上一节把模型下下来了。这一节不急着对话,先量一件事:这台机器跑这个模型,理论上能有多快。 量完再跑,心里有底。
一、「跑得动」其实有两个意思
第一个意思是装得下:权重加 KV 能塞进显存或内存。这一条在 2.1.1 已经算过了。 第二个意思是出得来:生成速度能跟上人的阅读速度。 这一条经常被忽略,但它是真正决定「能不能用」的那一条。 两个意思的瓶颈完全不在一个部件上:装得下看容量,出得来看带宽。 所以会出现「装得下但慢到没法用」这种情况——70B 塞进 64 GB 内存里跑,出字 0.6 tok/s,比手写还慢。 这一节要量的就是第二个意思。方法分两步:先量本机算力,再用两种方式估算速度,看哪个估得更接近现实。
二、先量本机算力:矩阵乘
衡量浮点吞吐的标准动作是矩阵乘法:两个 n×n 的矩阵相乘,运算量是 2n³ 次浮点运算。 取时间,除一下就是 GFLOPS。 几个细节要注意。要先预热一次,否则第一次的库加载和内存分配会算进时间里。 要取多次平均,单次测量的抖动很大。规模要够大,太小的矩阵乘测的是函数调用开销,不是算力。 Demo 用 2048×2048 单精度,跑三次取平均,实测 120 GFLOPS。 这个数在一台八核的普通机器上是合理的——numpy 背后是 BLAS,会自动用多线程和 SIMD 指令。 拿到 120 GFLOPS 之后,反推 7B 模型:每生成一个字要做 2 × 76 亿次浮点运算,也就是 15.2 GFLOP。 120 ÷ 15.2 ≈ 8 tok/s。
三、但这个数一定是偏高的
为什么?三个原因,一个比一个关键。 BLAS 会把所有核占满,而实际推理在解码阶段通常只用一个核在跑,或者只能用到一部分并行度。 大矩阵乘是最理想的形状:两个 2048×2048 相乘,计算量除以访存量很划算。 推理不一样——它是「瘦矩阵乘胖矩阵」,每读一个权重只做很少的运算,访存占比高得多。 最关键的一条:算力根本不是瓶颈。 解码每出一个字,全部权重都要从内存搬到计算单元一遍。 所以真正的上限是内存带宽 ÷ 权重体积,不是算力 ÷ 每字运算量。 按带宽重算:7B q4 权重 4.6 GB,双通道 DDR4 实测约 25 GB/s,25 ÷ 4.6 ≈ 5.5 tok/s。 比算力算出来的 8 更低——低的那一个才是现实。
四、这个估算解释了几乎所有现象
显卡为什么快? 不是因为算力高(虽然也高),主要是显存带宽高一个数量级。 500 GB/s 的显存带宽,同样 4.6 GB 权重能跑到 100 tok/s 以上。 量化为什么提速? 因为要搬的字节少了。q4 比 q8 体积小一半,带宽不变的情况下速度就快一倍。 这就是量化的真实收益机制。 批量为什么能提吞吐? 同一份权重被多个请求复用,搬一次算多次。 所以并发场景下每 token 的平均成本会掉——这也是 API 便宜的技术原因之一。 为什么加内存条有效、加 CPU 核没用? 因为瓶颈在总线。 单条内存条升级到双通道,带宽翻倍,速度就翻倍;核数从 8 加到 16,带宽不变,速度也不变。
五、动手 Demo:算力上限 8 tok/s,带宽上限 5.5 tok/s——低的那个才是现实
先用 2048×2048 的矩阵乘量本机浮点吞吐(预热一次、三次取平均), 再按 7B 模型的每字运算量反推速度上限;然后用「带宽 ÷ 权重体积」算一遍,对比两种估算的差距。
n = 2048
a, b = rg.normal(0, 1, (n, n)), rg.normal(0, 1, (n, n))
a @ b # 预热,别把首次开销算进去
t0 = time.perf_counter()
for _ in range(3):
a @ b
dt = (time.perf_counter() - t0) / 3
gflops = 2.0 * n ** 3 / dt / 1e9 # 2n³ 次浮点
# 换成带宽算:每出一个字,全部权重过一次内存
weight_gb = 7.6e9 * 4.8 / 8 / 1e9
tps = 25.0 / weight_gb
实跑输出:
--------------------------------------------------------------------
试运行:本机到底能跑多快,先量一把算力再说
--------------------------------------------------------------------
矩阵规模 2048 × 2048 单精度
单次耗时 143.1 毫秒
实测吞吐 120 GFLOPS
拿这个吞吐反推:一个 7B 模型每生成一个字,要做 2 × 76 亿次浮点运算。
每 token 浮点 15.2 GFLOP
按算力上限 8 tok/s
注意这是**纯算力的上限**,不是预期值。真实速度还要再乘一个效率系数,
因为下面三件事我这次都没算进去:
① BLAS 多线程 numpy 把核全占了,而推理常常只用一个核在解码
② 大矩阵乘效率高 推理是「瘦矩阵乘胖矩阵」,访存占比高得多
③ 没说内存 权重每出一个字都要从内存过一遍,带宽才是真瓶颈
所以再按带宽算一遍——这才是本地推理的实际天花板:
DDR4 双通道 5.5 tok/s(4.6 GB ÷ 25 GB/s)
DDR5 双通道 11.0 tok/s(4.6 GB ÷ 50 GB/s)
显存 GDDR6X 109.6 tok/s(4.6 GB ÷ 500 GB/s)
三行的差距说明一件事:**同一个模型,换的不是 CPU,是内存。**
这也解释了为什么显卡跑大模型快——不是算得快,是显存带宽高一个数量级。
实测结果:单次 142.7 毫秒,吞吐 120 GFLOPS。 这个数比很多人的直觉低——但它是真跑出来的,而且是在一个八核的普通机器上。 拿它反推:7B 每字 15.2 GFLOP,120 GFLOPS 对应 8 tok/s。 这个数已经不高了,可它还是上限中的上限。 再看带宽那三行:DDR4 双通道 5.5 tok/s、DDR5 双通道 11.0 tok/s、 显存 GDDR6X 109.6 tok/s。同一个模型,换的不是 CPU,是内存。 这两组数放在一起,就把「为什么显卡跑大模型快」讲清楚了:不是算得快,是搬得快。 显存带宽是双通道 DDR4 的二十倍,速度也就快二十倍。 要诚实说一句:Demo 里那 5.5 和 8 都是估算,不是实测出字速度——沙箱里没有 Ollama,跑不了真模型。 但它们两个谁高谁低这个关系是可靠的: 只要权重装得下,现实速度就贴着带宽那条线,离算力那条线很远。
30 秒实验
- 把矩阵规模从 2048 改成 512 和 4096,各测一次。 你会看到小规模测出来的 GFLOPS 明显偏低——那测的是调用开销,不是算力。
- 把模型从 7B 换成 1.5B(权重 0.9 GB),带宽那条线的速度变成 27.8 tok/s。 这就是为什么小模型在普通机器上体验还可以。
- 把带宽从 25 改成 3(机械硬盘的顺序读速度),速度掉到 0.65 tok/s。 这解释了「用 mmap 把模型放磁盘上跑」为什么慢到不能用。
常见误区
- 「CPU 核多就跑得快」。解码是访存密集型任务,瓶颈在内存带宽。加核数几乎没帮助,加带宽才有用。
- 「显卡快是因为算力高」。算力确实高,但主要原因是显存带宽高一个数量级。 推理在解码阶段,先卡带宽,再卡算力。
- 「量化会让模型变笨」。4 位量化的精度损失在多数任务上很小,而速度提升是实打实的。 代价存在,但通常小于收益。
自测三问(点开对答案)
1. 为什么算力估出来的 8 tok/s 比带宽估出来的 5.5 高? —— 因为算力估算假设「计算是瓶颈」,而实际解码时每出一个字都要把全部权重搬一遍,带宽先成为瓶颈。 **两个估算取小的那个才接近现实。** 2. 为什么加内存条有效,加 CPU 核没用? —— 因为解码的瓶颈是「每秒能搬多少字节」,也就是带宽。双通道换四通道、DDR4 换 DDR5 都能提带宽; 核数跟带宽无关。 3. 量化为什么能提速? —— 因为权重体积变小,每出一个字要搬的字节变少,在带宽不变的情况下速度就上去。 **本质是减少了访存量,不是提高了算力。**速度心里有数了,接下来给它配个能对话的界面。下一节看前端到底发的是什么请求。
- ☐ (本节完,下一节 2.1.6 配置Cherry Studio前端界面)