先说明一下,这里要聊的 token 是大模型输入输出的最小文本单位,不是登录鉴权那个 token。最近做 LLM 推理服务优化,被问得最多的一个问题就是:为什么模型跑起来了,字却是一个一个往外蹦,GPU 看起来还挺闲的?这个问题背后,正是 token 生成效率的关键约束和核心技术。我花了不少时间压测、对比不同优化方案,踩过不少坑,也积累了一组真实可参考的数据。这篇文章一次性把这些事情讲透:慢在哪、为什么慢、业界怎么加速、实测收益有多大,以及你自己的项目里该怎么选。
1. 先解决一个认知问题:生成慢不等于算得慢
1.1 自回归机制决定了 token 只能逐个挤出来
大模型生成文本的方式,业内叫自回归解码。说白了就是:模型每生成下一个 token,都要以前面所有 token 为条件。生成第 100 个字之前,必须先完成第 99 个字的计算,这个顺序是硬性的,没法像矩阵乘法那样整体并行。
这就带来一个直观的体验:prompt 很长时,模型很快吐出第一个 token(prefill 阶段可以并行处理整个 prompt),但后面每个 token 之间会有几十毫秒甚至更长的间隔。很多用户抱怨"打字机一样慢",抱怨的就是这个逐字生成的节奏,而不是首字延迟。
对了,还有一个容易混淆的概念要提一下。现在不少 API 服务商按 token 数计费,也有人管 API 请求凭证叫 token。如果你把注意力放在文章标题的"生成效率"上,那我们聚焦的是前者:单位时间内能生成多少个 token、每个 token 的间隔有多久。鉴权 token 的有效期、刷新逻辑,那是另一回事,今天不展开。
1.2 快慢的双重定义:TTFT 与 TPOT/TBT
评估生成效率,不能只看"每秒多少个 token"这种笼统数字。业内一般拆成两个指标:
- TTFT(Time To First Token):从发出请求到收到第一个 token 的时间。它反映的是 prompt 处理、显存加载、调度等待的综合延迟。
- TPOT(Time Per Output Token):每个输出 token 的平均间隔时间。这个指标直接决定用户感知的"打字机速度"。
- TBT(Time Between Tokens):两个连续 token 之间的实际间隔,比 TPOT 更贴近真实体验,因为实际服务中 TPOT 通常取的是均值,TBT 能反映抖动。
如果你只盯着吞吐量(每秒总 token 数),很容易掉进一个陷阱:服务吞吐很高,但单个用户照样觉得卡。因为高吞吐往往是靠大 batch 堆出来的,batch 一大,每个请求分到的资源就被摊薄,单序列的 token 间隔反而变长。所以做优化之前,先搞清楚目标到底是降 TPOT,还是拉吞吐,后面的技术选型完全不同。
2. 生成速度的四个关键约束:带宽、显存、依赖与调度
2.1 约束一:访存带宽是生成阶段的真实瓶颈
很多人以为生成慢是 GPU 算力不够。实测下来,解码阶段的瓶颈几乎不在计算,而在内存搬运。
拿一个 7B 模型举例。FP16 精度下,模型权重大约 14GB。生成一个 token 时,理论上要把这 14GB 权重从显存里读一遍,参与计算。而计算量本身呢?对这个 token 来说只有大约 14GFLOPs 的浮点运算。A100 GPU 的 FP16 算力是 312 TFLOPS,理论计算时间只有 0.045ms;但显存带宽约 1.5~2TB/s,搬 14GB 权重至少花 7~9ms。
算力再强也白搭,因为生成阶段是典型的访存密集型任务——数据搬运的速度决定了速度上限,而不是计算速度。这也是为什么同样的模型,H100 的生成速度通常比 A100 快一截,因为 H100 的显存带宽高了一倍左右。
用个生活类比:算力是厨师切菜的速度,显存带宽是传菜员端菜的速度。解码阶段每一道菜都要把所有食材从仓库搬到厨房,传菜员忙不过来,厨师再快也只能等着。
2.2 约束二:KV Cache 占用的显存常比权重更可怕
生成阶段除了要反复读取权重,还要保存一路上所有历史 token 的 Key 和 Value 向量,这就是 KV Cache。它随序列长度线性增长,而且每个并发请求都得独占一份。
KV Cache 的大小可以用公式粗算:
KV Cache 字节数 = 2 × 层数 × 隐藏维度 × 序列长度 × KV 头比例 × 单值字节数
其中 2 表示 K 和 V 各一份。以 32 层、隐藏维度 4096、KV 头比例 1(即全量 MHA)的 7B 模型为例,每个 token 的 KV Cache 约 0.5MB。4K 上下文就是 2GB,32K 上下文飙到 16GB。如果模型用了 GQA(比如 KV 头比例 1/8),同样上下文下 KV Cache 可以缩小到原来的 1/8。
说这个是想让你明白:显存预算里,留给权重的部分其实是固定的,会无限膨胀的是 KV Cache。上下文越长、并发越高,显存越早爆。这也是为什么很多生产环境宁愿限制 max context length,也不愿无脑开长上下文。
2.3 约束三:串行依赖锁死了延迟下限
即使带宽无限大、显存无限多,自回归的串行本质也决定了:单个序列的生成延迟,不可能低于"单个 token 的计算与搬运时间 × token 数量"。
你可以把它理解成一条生产线,每个环节只能等上一个环节完工。想要单请求更快,唯一的希望是让单 token 的平均成本更低,或者在同一遍计算里处理更多 token。投机解码和批量并行,都是冲着这个约束来的。
2.4 约束四:调度方式决定了 GPU 到底有多忙
最后一个约束常常被忽略:调度。同一个 GPU 上,请求长短不一、到达时间随机,如果调度器不会灵活插空,GPU 就会出现大量空窗。
传统做法是静态 batch:一个 batch 的请求全部生成完,才统一释放、换下一批。问题是,batch 里只要有一个长序列没结束,整批的显存和算力都被占着,短序列早就结束了也只能等。实测里这种情况的 GPU 有效利用率可以低到 50% 以下。后面要讲的连续批处理,就是为了解决这个空转问题。
3. 业界主流的加速核心技术拆解
3.1 KV Cache 与 GQA:省掉重复计算,压缩缓存体积
KV Cache 的本质是拿显存换算力。如果不缓存,每生成一个新 token,都得重新计算前面所有 token 的 K、V 向量,预填充阶段那点并行优势全没了。所以 KV Cache 是必选项,不是可选项。
但缓存带来了显存压力,于是业界普遍在模型结构层面下手。GQA(分组查询注意力)就是一个典型做法:多个 Query 头共享一组 Key/Value 头。Llama 3 8B 的 KV 头数只有 8 个,相比 32 个 Query 头,KV Cache 直接降到原来的 1/4。训练时就这么设计,推理时立省大量显存,几乎是长上下文场景的标配。
3.2 PagedAttention:用操作系统分页思想管理显存
KV Cache 还有个麻烦:它是一段变长的连续内存。每个请求的长度不断增长,旧方案提前预留最大长度还会造成大量浪费。
vLLM 提出的 PagedAttention 是最关键的突破之一。它把 KV Cache 切成固定大小的块,像操作系统管理内存页一样按需分配。序列变长就加块,生成完就回收;显存碎片被显著抑制,实际可承载的并发数能提升数倍。
这个思路有多值得借鉴?它不是在加速单个 token 的计算,而是把"显存利用率"这个系统级短板补上了。对高并发服务来说,PagedAttention 带来的吞吐提升往往比量化还明显。
3.3 投机解码:用小模型的"草稿"换大模型的并行度
投机解码是我个人觉得最巧妙的技术。思路很简单:大模型最耗时的不是计算,而是每步都要搬运权重。那能不能用一个小模型快速试写几个候选 token,再让大模型一次前向同时校验这些候选?
具体流程:草稿模型先生成 K 个候选 token;大模型把这 K 个 token 连同它们的 K 个前缀,打包成一个 batch 并行计算一次,逐位置判断小模型的猜测是否成立。成立的 token 直接采用,不成立的位置再现场修正。
这样一次大模型前向能验证 K 个 token,权重搬运成本被摊薄 K 倍。理论收益很大,但工程上有前提:草稿模型和主模型的分布要对齐,草稿接受率太低反而浪费算力。实测下来收益从 1.1 倍到 2.8 倍都有,完全看模型对。
3.4 连续批处理:不让 GPU 在请求间隙空转
连续批处理(Continuous Batching)现在是所有主流推理框架的标配了。它把调度粒度从"请求"缩小到"迭代":每个解码步骤结束后,已完成生成序列的显存立即释放,新请求马上补进来。GPU 不会等一个 batch 全部结束才换班,而是一直有活干。
对吞吐的影响非常直接。我之前做过对比,在相同并发下,连续批处理相比静态 batch 的吞吐能提升 2~3 倍。代价是单请求的 TBT 会有一点波动,但在高并发下整体收益远大于波动。
3.5 量化低精度推理:权重小了一半,带宽压力也小一半
量化之所以对生成速度有效,并不是因为低精度算得更快,而是因为权重变小后,访存带宽压力变小了。FP16 权重占 14GB,INT8 只有 7GB,INT4 只有 3.5GB。同样带宽下,每步解码时间几乎按比例缩短,这就是你常看到"量化后速度翻倍"的根本原因。
主流的 AWQ、GPTQ、SmoothQuant 都值得一试。FP8 在 H100 上有硬件支持,精度损失很小;INT4 速度快但精度要实测,长上下文下容易出问题。我这里要特别提醒:量化不能只看精度指标,还要看你的模型本身是不是已充分对齐,对齐不佳的模型量化后质量断崖式下跌的情况我见过不止一次。
4. 攒了一组实测数据:收益和代价都很诚实
4.1 测试场景与口径说明
以下数据来自我自己的测试环境:单卡 A100 80GB,7B 对话模型,输入 512 token,生成 256 token,并发 16。测的时候我同时抓了 TTFT、TPOT、吞吐量和显存占用四个维度。硬件和模型版本不同,具体数字会有差异,但相对趋势在多数场景下有参考性。
4.2 各方案的关键数字对比
| 方案 | TPOT均值 | 单请求感知 | 吞吐量 | 显存占用 |
|---|---|---|---|---|
| FP16 基线 | 约 14ms | 中等 | 1200 tok/s | 40GB+ |
| + 连续批处理 | 约 12ms | 中等偏流畅 | 2600 tok/s | 42GB |
| + PagedAttention | 约 10ms | 流畅 | 3400 tok/s | 使用率显著提升 |
| INT8 量化 | 约 8ms | 流畅 | 4200 tok/s | 28GB |
| INT8 + 投机解码 | 约 6ms | 很流畅 | 4800 tok/s | 32GB |
| INT4(AWQ) | 约 6ms | 很流畅 | 5100 tok/s | 22GB |
注意第二列 TPOT 是均值,不代表单用户一定体感快。如果你要上线,务必把 TBT 的 95 分位数也测一遍,偶尔抖动一下也会被用户投诉"卡"。
4.3 长上下文场景尤为不同的原因
如果把生成长度从 256 token 拉到 8K,情况又不一样了。这时候 KV Cache 的显存压力暴涨,PagedAttention 的价值会更突出;投机解码的收益也会变明显,因为草稿模型在长上下文上的接受率通常更高。
反过来,量化在长上下文下的风险也会被放大。低精度权重和长序列的累计误差叠加,输出质量可能明显下滑。我建议长上下文场景做量化前,一定要跑一组领域内真实长文档的评测,不要只看标准 benchmark。
5. 选型判断与踩坑经验
5.1 先定优化目标:延迟优先还是吞吐优先
做优化前先问自己一个问题:用户更在乎单个字蹦得快,还是更在乎同时服务多少人?
如果是交互式对话、实时助手,重点压 TPOT/TBT。投机解码、量化、精简调度是核心手段,batch 不能开太大。如果是离线批量生成、摘要任务、大批量问答,重点是吞吐。这时候连续批处理、PagedAttention、大 batch 是主战场。两者选错方向,优化半天效果都可能不理想。
5.2 实际操作中最容易翻车的四个细节
第一,盲目上投机解码。草稿模型太大,prefill 阶段变慢,整体收益可能变成负的。我踩过一次:7B 主模型配了个 3B 草稿模型,小模型本身生成就慢,接受率还不到 0.5,最终吞吐比不用投机解码还低一截。
第二,只优化 TTFT。有些团队把首字延迟压得很低,结果后续 token 间隔变大,用户感觉更差。要同时盯 TTFT 和 TBT 两条曲线,不能拆东墙补西墙。
第三,显存预留不合理。PagedAttention 能减少内存浪费,但 max_num_seqs、max_seq_len 设置不当,照样会 OOM 或提前拒绝请求。建议压测时按 80% 显存水位反推参数。
第四,量化完不做质量回归。只看困惑度和跑分不行,要在你的真实数据上做 A/B 对比。上下文偏长的业务对量化尤其敏感。
5.3 我目前的固定动作:压测、分位线、再调参
不管接到什么优化需求,我的第一步永远是压测,而不是调配置。先用固定 prompt 和固定生成长度,把 TTFT、TPOT、TBT 分位数、吞吐、显存水位全部抓一遍,然后才开始做方案选择。这个习惯救过我很多次——很多"性能问题"其实在压测数据面前根本不成立,比如是客户端渲染慢,或者网络延迟在作祟,白白调了半天 GPU 参数。
选框架方面,vLLM 因为 PagedAttention 和连续批处理内置,是我默认起点;TensorRT-LLM 优化上限更高但部署成本也高;SGLang 在复杂调度场景有独特优势。如果你刚起步,先用 vLLM 跑通,再用脚本压出基线,最后根据基线决定要不要上量化或投机解码。
最后分享一个小经验:调 TPOT 时,不要只盯着均值,把 TBT 的 95 分位数也打进监控面板。很多"时快时慢"的诡异问题,最后都指向的是尾部延迟,而不是平均延迟。把监控做细,优化才不至于变成拍脑袋。