介绍如何根据实际吞吐量需求(tokens/秒)选择CPU或GPU推理,详解LLM的prefill和decode阶段差异,以及如何在评审会议中用数据论证推理硬件选型。
对于大多数团队来说,用什么硬件跑模型这个问题的答案靠的是习惯而不是计算。模型卡上写着 GPU,集群有 GPU 配额,上一个项目用了 GPU,所以这个也会用 GPU。然后账单来了——本来应该花几分钱的模型现在要花几块钱,因为没人问那个唯一重要的问题:这条 workload 实际需要多少 token 每秒?
这个数字在你购买任何硬件之前就存在。后台任务通宵总结文档只需要每秒几个 token。用户面前的聊天机器人需要几十个。批处理重排序器在无人值守时处理队列,需要的是队列增长速度不超过排空速度。这对应三种不同的硬件答案,其中两种根本不需要 GPU。本文展示如何在下单前算出你的上限、如何测量你已有的机器、以及如何将结果转化为能在评审会上站住脚的决策。
每次 LLM 生成运行都有两个阶段,且它们的工作性质不同。Prefill(预填充)一次性处理整个 prompt:每个 token 都要 attending 到其他所有 token,这是一整面矩阵乘法的墙。这个阶段是计算密集型的。每秒 FLOPs 越高越好,而 GPU 在这方面领先一个数量级。
Decode(解码)是第二个阶段。模型输出一个 token,将它喂回去,再输出下一个。每一步都要将整个权重矩阵从内存搬到计算单元,做相对少量的算术运算,然后输出一个 token。每个 token 的算术运算量很小。主导因素是内存带宽:权重必须在每个 token 时都穿越内存总线一次,直到生成停止。
这就是为什么在给定机器上 decode 速度由内存带宽决定,而不是由 FLOPs 决定。计算单元空闲着等待权重到达。GPU 在 decode 上仍然获胜,因为它的内存子系统更宽更快,但这个胜利是带宽的胜利,而带宽是 CPU 也可以拥有的属性。问题是 CPU 的带宽对你的 workload 来说是否足够。
上限是算术题,只需要一行。模型每个 token 读取一次权重,所以最大 tokens 每秒等于内存带宽除以权重大小(以字节为单位)。
def decode_ceiling(bandwidth_gb_s: float, weights_gb: float) -> float:
# pure arithmetic: bytes per second divided by bytes per token
return bandwidth_gb_s / weights_gb
代入典型数字看个大概。一个 70 亿参数、每个参数 4 位的模型在磁盘和内存中大约是 3.5 GB。一台有效内存带宽 40 GB/s 的笔记本电脑算出的上限约为每秒 11 个 token。带宽 100 GB/s 的机器将这个数字翻倍。500 GB/s 或更高的 GPU 让同一个模型超过 100。所有这些都不是任何特定产品的实测数据。它们是机器无法突破的理论上限,直接来自规格表。
两个结论立刻浮现。首先,公式中分母的模型大小和分子的硬件同样重要:一个 10 亿参数、4 位的模型大约是 0.5 GB,这给 CPU 带来每秒几十个 token 的上限。CPU 上跑小模型不是妥协,而是算术不再偏向 GPU 的那个临界点。其次,一条需要每秒 8 个 token 的 workload 根本不该为上限 200 的机器付费。GPU 过剩了 25 倍,而你为整个倍数买了单。
三种 workload 形态通常落在 CPU 上限之内。第一种是对吞吐量不敏感的工作:夜间摘要、文档分类、日志分拣、任何在后台运行且没有用户等待的任务。没人会注意到批处理任务是 4 分钟还是 40 分钟完成,只要它在早上之前跑完就行。第二种是低并发工作。单个用户或单个队列意味着一次只有一条生成流。GPU 在多条流共享硬件时才值回票价;单独一条流只用了一小部分。第三种是长上下文重读。一条把大文档喂给模型的任务大部分时间花在 prefill 上,而 CPU 上的 prefill 慢到足以产生影响——但如果同一文档每晚按计划处理,慢也仍然够快。
每一种都能映射到上限公式。写下需要的每秒 token 数。乘以你为尖峰和重试预留的 margin。如果结果仍在你已有 CPU 的实测上限之下,购买决策已经做出来了——答案是什么都不买。
规格表给的是上限。你的机器给的是现实,而现实才是决策依据。下面的工具直接测量 decode 吞吐量:用 llama-cpp-python 加载一个 GGUF 模型,生成固定数量的 token,然后用墙钟时间除以 token 数。无需外部服务器、无云账户、无厂商仪表盘。
import time
from llama_cpp import Llama
llm = Llama(model_path="model.gguf", n_ctx=8192, n_threads=8, verbose=False)
prompt = "Explain why a GPU is not always the right answer for inference."
start = time.perf_counter()
out = llm(prompt, max_tokens=512)
elapsed = time.perf_counter() - start
tokens = out["usage"]["completion_tokens"]
print(f"prefill+decode wall time : {elapsed:.2f}s")
print(f"decode throughput : {tokens / elapsed:.1f} tokens/s")
在同一模型文件和相同 prompt 长度下,在 GPU 机器和 CPU 机器上运行同样的脚本。两者的比率是购买问题的真正答案,而且通常比营销宣传的要小,因为 decode 在两台机器上都是带宽受限的。不要信任你没写过的基准测试套件打印出的数字;信任你自己的 workload 形态跑出来的数字。
实测吞吐量是一个事实,不是判决。把它转化为决策需要公式的需求端。聊天机器人需要首 token 快,后续 token 稳定:decode 速率在每次交互中都重要。批处理 worker 需要平均速率超过队列到达速率:短时突发不重要,持续速率才重要。每天运行一次的报告生成器需要总墙钟时间落在维护窗口内。
将需求与实测数据比较时要有明确的 margin。如果需求是每秒 8 个 token 而 CPU 实测是 14,决策很简单。如果 CPU 实测是 10 而需求是 9,margin 太薄了,GPU 靠的是 headroom 而不是峰值速度获胜。margin 是一个策略选择,不是物理常数,这正是它应该写进代码里的原因。
整个过程压缩成一个接收需求和实测数据、返回判决的小函数。写成代码迫使假设从对话中拿出来进入一个可以 review 的文件。
def choose_hardware(required_tokens_s: float, measured_cpu: float, margin: float = 1.5) -> str:
needed = required_tokens_s * margin
if measured_cpu >= needed:
return "cpu"
if measured_cpu >= required_tokens_s:
return "cpu, thin margin — recheck after quantization"
return "gpu"
判决故意写成自然语言。第一个分支是整篇文章的一行总结:实测吞吐量以 margin 胜过了需求,所以 GPU 是过剩的。第二个分支说算术足够接近,可以在花钱之前再测一个变量。第三个分支是唯一真正购买硬件的情况。
上限公式的分母是每个参数的字节数,而量化正是缩小它的方法。一个每参数 8 位的模型占用的内存是一个同模型 4 位的两倍,在相同带宽下 decode 上限减半。从 8 位降到 4 位会让上限翻倍,可能将一台机器从第三个分支移到第一个。
问题是量化用字节换精度,而这个交易是任务相关的。一个分类模板文档的模型可能在 4 位下存活而看不出差别。同一个模型做法律文本的精确抽取可能就不行。正确的测试既便宜又机械:把你的评估集跑一遍两种量化,对比输出,让对比结果说话。不要假设上限算术是屋子里唯一的算术。
只要机器和软件保持不变,实测数据才值得信赖。依赖升级可能悄无声息地改变运行时,共享机器可能因 noisy neighbor 丢失带宽,模型文件替换可能改变有效大小。廉价的保护是一个回归测试工具,在每次触及流水线的变更后重新测量并断言。
import json
from pathlib import Path
def assert_decode_ceiling(path: str, floor: float) -> None:
result = json.loads(Path(path).read_text())
measured = result["decode_tokens_per_second"]
assert measured >= floor, f"decode fell below {floor}: {measured:.1f}"
将这个工具接入 CI,紧挨着单元测试。它不会捕获每一种回归,但能捕获最贵的那种:模型仍然正确回答,只是账单增长了。
结论不是推荐放弃 GPU。结论是推荐停止猜测。需求是一个你可以写下来的数字。上限是一个你可以从规格表算出来的数字。实测吞吐量是一个你可以在已有机器上花五分钟跑出来的数字。当三个数字都存在时,硬件决策就不再是一种信念,而变成了一道算术题——而很多时候,算术的结果是什么都不买。
Originally published on Dispatch.