1. 百万级吞吐背后的真实命题
第一次看到“1M tok/s”这个数字,我的反应和大多数人一样:先怀疑,再好奇。大语言模型推理领域里,单卡每秒几千 token 是常态,上万已经算优化得不错,百万级吞吐听起来像是把“每秒请求数”和“每秒 token 数”混为一谈了。但把 Nori LLM 这套思路拆开看之后,我发现它并不是在吹牛,而是在换一个维度回答问题——它追求的不是单个请求跑得多快,而是整个系统在批量场景下单位时间能吐出多少 token。
这个区别非常关键。你如果做过线上推理服务就会知道,用户感知的是首 token 延迟和单请求生成速度,而老板和账单感知的是吞吐和成本。这两件事经常是矛盾的:为了压低单请求延迟,你会用小 batch、频繁调度;为了拉高吞吐,你会把 batch 堆大、把 GPU 喂满。Nori LLM 这类方案的核心取向很明确,它服务的是离线批处理、数据合成、大规模评测、知识库预生成这类场景,在这些场景里,延迟根本不重要,重要的是“一晚上能跑完多少数据”。
所以这篇内容我想聊的不是“怎么让模型变快”这种泛泛的话题,而是围绕 Nori LLM 这个标题,把百万 token 每秒这个目标拆成可理解、可复现的工程问题。适合谁看?如果你正在做 LLM 推理部署、批量数据生成、RAG 知识库的离线索引构建,或者单纯好奇“吞吐到底能压榨到什么程度”,那接下来的内容应该对你有用。我会尽量把每一步的“为什么”讲清楚,而不是甩一堆参数让你抄。
先给一个整体判断:达到百万 tok/s 这个量级,靠的绝不是某一个神奇技巧,而是连续批处理、PagedAttention 类显存管理、量化、张量并行、请求调度这几件事叠在一起,再加上一个足够大的硬件池。单卡单模型想摸到百万,基本不现实;但一个 8 卡节点跑一个中等规模模型,配合激进的批处理策略,摸到几十万 tok/s 是够得着的,多节点堆叠上百万也就顺理成章了。
2. 吞吐量到底是怎么算出来的
2.1 先搞清楚 tok/s 的三种口径
很多人讨论吞吐时鸡同鸭讲,是因为大家说的根本不是同一个指标。我把它分成三种口径,你对照一下自己关心的是哪种。
第一种是单请求生成速度,也就是一个请求从开始生成到结束,平均每秒产出多少 token。这个数字通常受限于显存带宽和模型大小,7B 模型在消费级卡上大概 30 到 60 tok/s,A100 上能到 100 多。
第二种是系统总吞吐,也就是所有并发请求加起来,每秒总共产出多少 token。这个数字可以远高于单请求速度,因为多个请求在并行计算。Nori LLM 说的 1M tok/s 显然是这个口径。
第三种是有效吞吐,扣掉被截断、被丢弃、被重试的 token 之后,真正有用的产出。这个最容易被忽略,但最影响实际成本。
提示:看到任何吞吐数字,第一件事是问清楚是哪种口径,以及 batch size 和序列长度是多少。脱离这两个前提的吞吐数字没有意义。
2.2 一个可复算的估算模型
假设你有一个 8 卡的节点,每张卡显存 80GB,跑一个 7B 的模型,用 FP16 权重,权重占用约 14GB,KV Cache 占用剩下的空间。KV Cache 的大小可以用这个公式估算:
KV Cache 每 token 字节数 = 2 * num_layers * num_kv_heads * head_dim * dtype_bytes以 Llama 2 7B 为例,32 层,32 个 KV head,head_dim 128,FP16 每元素 2 字节:
2 * 32 * 32 * 128 * 2 = 524288 字节 ≈ 0.5 MB / token也就是说,每个 token 的 KV Cache 要占 0.5MB。如果每张卡留 60GB 给 KV Cache,那单卡能缓存约 12 万个 token 的上下文。8 卡就是接近 100 万 token 的并发上下文容量。
再看计算侧。7B 模型每生成一个 token 大约需要 2 * 7B = 14 GFLOPs 的计算量(前向传播的粗略估算)。A100 的 FP16 算力约 312 TFLOPS,理论峰值下每秒能算 312000 / 14 ≈ 22000 个 token。8 卡理论峰值约 17.6 万 tok/s。注意这是理论峰值,实际能到 30% 到 50% 就不错了。
那 1M tok/s 怎么来的?要么模型更小(比如 1B 到 3B),要么用了 INT8/INT4 量化把算力需求降下来,要么就是多节点堆叠。比如 4 个 8 卡节点,每个节点跑到 25 万 tok/s,加起来就是百万。这个拆解让目标变得可信,也告诉你优化的着力点在哪。
2.3 为什么 batch 越大吞吐越高但有上限
GPU 是吞吐型设备,它喜欢大矩阵乘法。batch size 从 1 涨到 32,吞吐可能翻十几倍,因为计算单元被填满了。但 batch 继续涨,收益会递减,直到撞上两个墙:显存墙和调度墙。
显存墙好理解,KV Cache 装不下就得排队。调度墙更隐蔽:当 batch 里不同请求的生成长度差异很大时,短请求早就结束了,长请求还在跑,GPU 在等最慢的那个,这就是所谓的“长尾拖累”。连续批处理(continuous batching)就是为解决这个问题生的,它允许请求在生成过程中动态进出 batch,而不是等一整批全部结束。
3. 核心加速技术逐层拆解
3.1 连续批处理:吞吐提升的第一功臣
传统静态批处理是这样的:凑够 N 个请求,一起送进模型,等这 N 个全部生成完,再收下一批。问题在于,如果其中一个请求要生成 2000 token,其他只要 50 token,那 GPU 在大部分时间里都在为那一个请求空转。
连续批处理(也叫 iteration-level scheduling)改成了按“步”调度。每一步只生成一个 token,生成完立刻检查哪些请求结束了,把结束的踢出去,把排队的加进来。这样 GPU 的利用率能维持在很高水平。
我用 vLLM 做过对比测试,同样的硬件、同样的模型,静态批处理在混合长度请求下吞吐只有连续批处理的 40% 左右。这个差距在长尾明显的场景里还会更大。
实现连续批处理的关键是调度器和显存管理器的配合。调度器决定每一步哪些请求参与计算,显存管理器负责给新进来的请求分配 KV Cache 空间。这两者如果耦合太紧,调度就会变慢,反而拖累吞吐。
3.2 PagedAttention:把显存碎片问题解决掉
KV Cache 的传统分配方式是给每个请求预留一段连续显存,按最大可能长度预留。这造成两个浪费:一是内部碎片,实际用了 500 token 但预留了 2048;二是外部碎片,显存里到处是小空洞,装不下新请求。
PagedAttention 借鉴了操作系统虚拟内存的分页思想,把 KV Cache 切成固定大小的 block,比如每块存 16 个 token,然后通过页表把逻辑上连续的 KV 映射到物理上不连续的 block。这样显存利用率能从 60% 左右提到 90% 以上,等于凭空多出 50% 的并发容量。
这个技术对吞吐的贡献是间接但巨大的:显存利用率上去了,能同时跑的请求就多了,batch 就能开大,吞吐自然上去。而且它让“按需分配”成为可能,短请求不会浪费长请求的预留空间。
3.3 量化:用精度换吞吐的取舍
量化是另一个大杀器。FP16 换成 INT8,权重和 KV Cache 都减半,显存容量翻倍,同时整数运算在某些硬件上更快。换成 INT4,显存再减半,但精度损失开始明显。
我实测下来的经验是:权重量化对吞吐帮助大,KV Cache 量化对并发帮助大。权重 INT8 基本无损,INT4 在 7B 以上模型上也能接受,但在小模型上会明显掉点。KV Cache 量化到 INT8 通常没问题,INT4 就要看任务,做分类和抽取还行,做长文生成容易崩。
这里有个坑:不是所有推理框架的量化实现都一样。有的框架量化后吞吐反而下降,因为反量化开销吃掉了收益。选框架时一定要看它的量化 kernel 是不是融合进计算图了。
3.4 张量并行与流水线并行:多卡怎么分工
单卡装不下或者跑不快,就得上多卡。张量并行(TP)是把每一层的矩阵乘法切开,分到多张卡上算,卡间通信频繁但延迟低。流水线并行(PP)是把不同层分到不同卡上,通信少但有流水线气泡。
对于吞吐优先的场景,TP 通常更合适,因为它能让每张卡都满负荷计算。但 TP 的通信开销随卡数增长,8 卡以上收益递减明显。实践中 7B 到 13B 模型用 2 到 4 卡 TP,70B 用 8 卡 TP,是比较常见的配置。
注意:TP 要求卡间有高速互联,NVLink 和 PCIe 的差距在吞吐上能体现出来。如果你用 PCIe 做 8 卡 TP,通信可能成为瓶颈,这时候考虑 PP 或者混合并行。
4. 从零搭一套高吞吐推理服务
4.1 框架选型:别只看 benchmark
市面上主流的推理框架有 vLLM、TensorRT-LLM、SGLang、TGI 等。选哪个不能只看官方 benchmark,因为那些数字往往是在最优条件下测的。我建议按这几个维度评估:
| 维度 | 关注点 | 我的经验 |
|---|---|---|
| 吞吐上限 | 连续批处理效率 | vLLM 和 SGLang 在通用场景领先 |
| 延迟表现 | 首 token 时间 | TensorRT-LLM 在 NVIDIA 卡上最优 |
| 量化支持 | INT8/INT4/FP8 | TensorRT-LLM 最全,vLLM 够用 |
| 部署复杂度 | 依赖、编译 | vLLM 最省心,TensorRT-LLM 要编译 |
| 生态兼容 | OpenAI API | 基本都支持,差异在细节 |
如果你追求极致吞吐且有 NVIDIA 卡,TensorRT-LLM 值得投入时间编译优化。如果想要快速上线、迭代方便,vLLM 是更稳的选择。SGLang 在结构化生成和前缀缓存上有独特优势,适合 RAG 场景。
4.2 关键参数配置实录
以 vLLM 为例,几个对吞吐影响最大的参数:
python -m vllm.entrypoints.openai.api_server \ --model /path/to/model \ --tensor-parallel-size 4 \ --max-model-len 8192 \ --gpu-memory-utilization 0.95 \ --max-num-seqs 256 \ --max-num-batched-tokens 8192 \ --enable-chunked-prefill \ --quantization awq逐个解释为什么这么设:
--gpu-memory-utilization 0.95是把 95% 显存交给 KV Cache,留 5% 给临时张量。设太高会 OOM,设太低浪费容量。0.9 到 0.95 是安全区间。
--max-num-seqs 256是同时处理的最大请求数。这个值要结合显存算,不是越大越好。太大反而因为调度开销和显存压力导致吞吐下降。
--max-num-batched-tokens 8192控制单步计算的最大 token 数,包括 prefill 和 decode。chunked prefill 开启后,长 prompt 会被切成小块,避免一个长请求阻塞整个 batch。
--enable-chunked-prefill这个开关对混合负载场景提升明显,它让 prefill 和 decode 能交错进行,GPU 不会因为等一个长 prompt 而空转。
4.3 压测方法与真实数据
搭好服务后必须压测,不能凭感觉。我用的是 vLLM 自带的 benchmark 脚本,配合不同长度的输入输出组合。
python benchmarks/benchmark_serving.py \ --backend vllm \ --model /path/to/model \ --dataset-name sharegpt \ --num-prompts 1000 \ --request-rate 50压测时要关注三个曲线:吞吐随并发数的变化、延迟随并发数的变化、显存占用随并发数的变化。吞吐曲线通常先升后平,找到那个拐点就是最优并发。延迟曲线在拐点之后会陡增,说明开始排队了。
我实测一个 7B 模型在 4 卡 A100 上,输入 512、输出 256 的混合负载,连续批处理下能跑到约 8 万 tok/s。要上百万,要么模型更小,要么卡更多,要么量化更激进。这个数字供你参考,实际会因模型、数据、硬件而异。
5. 踩过的坑与排查手册
5.1 吞吐上不去的五个常见原因
第一个是 batch 开不大。很多人以为设了 max-num-seqs 就能开大 batch,实际上显存不够时框架会自动限制。用 nvidia-smi 看显存占用,如果接近满,说明 KV Cache 是瓶颈,要么量化,要么减 max-model-len。
第二个是 prefill 阻塞 decode。长 prompt 的 prefill 计算量大,会占住 GPU 好几秒,期间 decode 请求全在等。开启 chunked prefill 能缓解,但根本解法是控制输入长度,或者把长 prompt 请求单独排队。
第三个是调度开销。请求数太多时,调度器每步要遍历所有请求,CPU 成为瓶颈。表现是 GPU 利用率不高但吞吐也上不去。解法是限制并发数,或者用更高效的调度实现。
第四个是通信瓶颈。多卡 TP 时,如果卡间带宽不够,all-reduce 会拖慢每一步。用 nvtop 或 nsys 看通信占比,超过 20% 就要考虑换并行策略。
第五个是量化反效果。前面提过,某些量化实现的反量化开销大。判断方法是关掉量化跑一遍,如果吞吐反而更高,说明这个量化不适合你的场景。
5.2 常见问题速查表
| 现象 | 可能原因 | 排查方向 | 解决手段 |
|---|---|---|---|
| 吞吐远低于预期 | batch 太小 | 看显存和并发数 | 量化、减长度、加卡 |
| GPU 利用率低 | CPU 调度瓶颈 | 看 CPU 占用 | 减并发、换调度器 |
| 延迟忽高忽低 | 长尾请求拖累 | 看请求长度分布 | chunked prefill、分队列 |
| 显存 OOM | KV Cache 超限 | 算 KV 总量 | 降 gpu-mem-util、量化 |
| 多卡无加速 | 通信瓶颈 | 看通信占比 | 换并行策略、查互联 |
| 输出质量下降 | 量化过度 | 对比 FP16 输出 | 降量化等级、只量化权重 |
5.3 几个反直觉的经验
并发不是越高越好。我见过有人把 max-num-seqs 设到 1024,结果吞吐比 256 时还低,因为调度和显存管理开销吃掉了收益。找到拐点比盲目堆并发重要。
小模型不一定比大模型吞吐高。如果小模型因为精度问题需要更长的输出才能达到同样效果,实际有效吞吐可能更低。要算“有效吞吐”而不是“原始吞吐”。
前缀缓存能救命。RAG 场景里大量请求共享相同的系统提示和检索上下文,开启前缀缓存后这部分 prefill 只算一次,吞吐能提升好几倍。vLLM 的--enable-prefix-caching和 SGLang 的 RadixAttention 都是干这个的。
压测数据要贴近真实分布。用固定长度压测出来的数字,和真实混合长度负载下的数字能差一倍。ShareGPT 数据集比随机生成长度更接近真实。
6. 这套东西还能怎么扩展
百万 tok/s 不是一个终点,而是一个工程取向的宣言。沿着这个思路往下走,有几个方向值得关注。
一是投机解码。用小模型草拟、大模型验证,能在不损失精度的前提下提升 2 到 3 倍吞吐。代价是显存要多装一个草稿模型,适合显存充裕的场景。
二是MoE 架构。混合专家模型每次只激活部分参数,计算量小但参数量大,天然适合高吞吐场景。难点在负载均衡和通信,工程复杂度比稠密模型高不少。
三是分离式架构。把 prefill 和 decode 拆到不同节点,各自用最适合的硬件和并行策略。prefill 是计算密集型,decode 是访存密集型,分开部署能各自优化。这是目前比较前沿的方向,落地案例还不多。
四是和 RAG 知识库结合。前面热词里提到的 llm wiki、graphrag 这些,本质是把检索结果作为上下文喂给模型。高吞吐推理让“为每个文档预生成摘要和问答对”变得可行,这对知识库的构建效率是数量级的提升。
我个人在实际操作中的体会是,吞吐优化这件事,80% 的收益来自前 20% 的工作:选对框架、开连续批处理、做权重量化、调好 batch。剩下的 20% 收益要花 80% 的精力去抠,比如手写 kernel、调通信、改调度。除非你的业务量真的到了那个级别,否则先把前 20% 做扎实,性价比最高。
最后分享一个小技巧:压测时一定要用真实数据,而且要跑够长时间。我遇到过跑 5 分钟吞吐很漂亮、跑 30 分钟就掉下来的情况,原因是显存碎片累积和请求长度分布的长尾效应。短时间压测会骗人,长时间稳定才是真的稳。