2026/10/26 13:05:00

大模型推理核心解析:Prefill与Decode的算力与带宽博弈

大模型推理核心解析:Prefill与Decode的算力与带宽博弈 如果你在本地跑过开源大模型大概率见过类似这样的日志先是 prompt eval time后面跟着 eval time。前者处理几百上千个提示词 token 只花一两秒后者每生成一个 token 都要花几毫秒甚至更久。同一个模型、同一份权重、同一个 forward 函数凭什么差距这么大答案就藏在 Prefill 和 Decode 这两种计算形态里。这篇我不打算写那种“看完还是不会用”的科普而是把 Prefill 和 Decode 各自的原理、瓶颈、指标和工程坑拆开讲清楚为什么 Prefill 快、Decode 慢为什么批处理对 Decode 那么重要为什么长上下文会让 Decode 越来越慢线上部署时到底该先优化哪个阶段。无论你是自己做推理服务、在 Windows 上折腾本地模型工具还是刚接触 Transformer 想弄明白日志里那两行数字这篇文章都比较适合你。先给一个最简版本Prefill 是模型“读题”的阶段把用户输入的所有 token 一次性并行算完同时把关键信息缓存下来Decode 是模型“答题”的阶段每次只能根据已经生成的 token 推出下一个 token。读题可以一目十行答题只能一个字一个字写。理解了这个朴素的区别后面所有细节都顺了。1. 两种计算形态从哪来自回归的必然结果1.1 自回归一句话说清为什么不能一口气生成几乎所有主流生成式大模型本质上都是自回归模型。所谓自回归就是“下一个 token 只由前面所有 token 决定”。你要生成一句话“今天天气真不错”实际上是按这个顺序完成的根据用户输入比如“用一句口语评价今天的天气”预测第一个 token“今天”。把“今天”接到输入后面重新预测下一个 token“天气”。把“今天天气”接上再预测“真”。每一步预测出来的 token 都会成为下一步的输入。这是模型的结构决定的它的输出概率分布只建立在“已经看到的内容”之上不可能跳过中间 token 直接生成整句。也正因为如此生成过程必然是串行的这就是 Decode 阶段“慢”的结构性根源。但你仔细想想第一步和第二步其实不太一样。第一步的输入是用户给的、长度已知的提示词几十个甚至几千个 token可以一次性全部塞进模型并行处理。第二步开始输入才真正变成“不断增长的上下文”。于是一次推理请求被天然劈成两种计算形态先把已知的提示词并行处理完再逐字串行生成。1.2 同一次前向推理输入形状完全不同从代码层面看Prefill 和 Decode 其实走的是同一个 Transformer 前向函数区别只在输入序列长度。Prefill 时输入的形状是 [batch, 提示词长度, hidden_size]。提示词 2000 个 token就是 2000 行向量一起过网络注意力在 2000×2000 的范围内两两交互。这本质上是几十次大矩阵乘法属于“形状规整、计算量大”的任务GPU 的算力可以被压得很满。Decode 时输入的形状变成 [batch, 1, hidden_size]也就是一次只喂一个 token。但注意力仍然要和前面所有 token 的 K、V 做交互——如果上下文已经积累到 2000 个 token那么这一个 token 要和 2000 个 K/V 向量逐个计算注意力分数。计算量不大但要把那 2000 个 K/V 全部从显存里搬一遍。搬数据的时间往往远大于算一个 token 的时间。同一个模型同一份权重因为输入形状不同瓶颈一个在“算”上一个在“搬运”上。这是整篇文章最核心的一句话后面的优化、踩坑、指标全部围绕它展开。2. Prefill一口气读完提示词2.1 Prefill 究竟算了一次什么Prefill 这个名字直译是“预填充”干的事就是把提示词一次性填进模型完成一次完整的前向计算。这次前向和训练时的前向非常像每个 token 都要经过多层 Transformer包括多头注意力、前馈网络、归一化。唯一不同的是推理不需要反向传播只保留前向结果。这次前向的产物有两个。第一个是最后一个 token 位置的 logits用来预测第一个输出 token第二个是所有层所有 token 的 K 和 V 向量它们被写进一块专门的缓存也就是后面 Decode 阶段每步都要读的 KV Cache。很多人误以为 Prefill 只是为了“拿到第一个 token”。其实它承担了绝大多数计算量上下文里所有 token 的注意力、所有前馈网络计算都在这一趟里完成。提示词越长这一趟就越贵这也是 RAG、长文档问答场景首字延迟高的主要原因——不是 Decode 慢而是 Prefill 还没算完。2.2 KV Cache 在这里被“写满”为什么需要 KV Cache举个例子Decode 第 100 步时模型需要让当前 token 和前面 100 个 token 都做注意力。如果每一步都重新算一遍前 100 个 token 的 K、V那就等于把 Prefill 的注意力部分重复上百次计算量直接爆炸。所以工程上的做法是在 Prefill 阶段一次性把所有已有 token 的 K、V 算好缓存下来Decode 阶段每次只算新 token 自己的 K、V然后追加进缓存读取旧缓存做注意力。KV Cache 的大小非常可观。以常见的 7B 规模模型为例假设 32 层、隐藏维度 4096、采用 8 个 KV 头、每头 128 维那么每个 token 每层需要存 8×128×2K 和 V 各一份等于 2048 个浮点数32 层就是 65536 个浮点数。用 FP16 存储每个 token 大约要 128KB 显存。上下文长度 4096 时一条请求的 KV Cache 就是 512MB上下文到 32K 时单请求直接突破 4GB。这就是为什么“长上下文”这个听起来很爽的功能实际部署时非常贵。Prefill 阶段把 KV Cache 写满只是一次性的开销但后面 Decode 每一步都要把这块越来越大的缓存读一遍越读越慢。顺便说一句现在很多新模型把 KV 头数从和查询头一样多MHA压缩到四分之一甚至更少GQA、MQA首要目的正是削减 KV Cache 的体积后面还会展开。2.3 算一笔长提示词的账我们粗略算一下 Prefill 的计算量。Transformer 前向的计算量大约等于 2×参数量×token 数这样估算和实际误差不算大。7B 模型处理 2048 个 token一次 Prefill 的浮点运算量大约是2 × 7×10^9 × 2048 ≈ 2.87×10^13 FLOPs约 28.7 TFLOPs。如果一块 80GB 显存的数据中心级显卡 FP16 算力在 300 TFLOPs 左右那么理想情况下 2048 token 的 Prefill 只需要约 0.1 秒。注意这只是模型参数部分的矩阵乘法注意力部分的开销还要另外算。批处理时更直观32 个并发请求、每个 2048 token总共 65536 个 token 一次处理计算量约 918 TFLOPs理想时间约 3 秒。这就是为什么带长上下文、高并发的服务首字延迟经常肉眼可见。Prefill 的耗时直接决定了 TTFT首 token 延迟用户在你输入完问题之后等了多久才看到第一个字。在很多知识库问答场景里系统提示词可能就占了几千 token加上检索出来的上下文一上来就是 8000 token 的 Prefill。这时候第一个字迟迟不出来大概率问题在 Prefill而不是生成太慢。2.4 Prefill 的优化重点矩阵大、显存紧Prefill 既然是“算力密集型”阶段优化方向就围绕两个点把大矩阵乘法算得更快以及不要让中间结果挤爆显存。注意力部分是个重头戏。朴素注意力要生成 [长度, 长度] 的分数矩阵2000 token 就是 400 万个分数再多请求一起批处理显存瞬间告急。工程上通用做法是算子融合与分块不把完整的分数矩阵存出来而是算一小块、用一小块、丢一小块在算子内部完成归一化和加权求和。这类 kernel 优化在推理框架里几乎是标配既省显存又提速。另一个常见手段是“切块”。如果一条请求的提示词很长比如 32K token一上来就整个算完会长时间占住 GPU把同批次的 Decode 请求全部堵死。于是很多推理框架把 Prefill 拆成若干小块见缝插针地和 Decode 步骤交错执行。后面我会专门说这个这里先记住Prefill 虽快但它是“吃独食”的在混合调度里是个麻烦制造者。3. Decode一个字一个字挤出来3.1 单步计算小搬运量却大Decode 每一步只生成一个 token但每步都必须完成一次完整的前向。这一步的矩阵乘法非常小输入就一个 tokenhidden 维度 4096和一个 4096×4096 的权重矩阵相乘一次矩阵乘才 1600 万次乘加对 GPU 来说连热身都算不上。但问题在于GPU 计算之前必须先“读数据”。模型权重有 14GB7B 参数 × 每参数 2 字节无论稀疏还是稠密每一步都要完整读一遍才能做乘法。读数据的时间取决于显存带宽而不取决于算力。这就像让一个搬运工和一个计算员同时干活计算员一秒能做上亿次加法但搬运工每秒只能从仓库里搬固定吨位的货。只要货物没到位计算员就得干等着。所以 Decode 阶段的瓶颈不是 FLOPs而是显存带宽。GPU 的算力再多也用不上因为数据搬运速度把整体节奏锁死了。很多人抱怨“模型越大越慢”本质就是模型大了每步要搬的字节数变多了而带宽只能固定额度供应。3.2 带宽账本14GB 权重每次都要搬一遍我们来算 Decode 的带宽账。假设 80GB 显存的数据中心级显卡显存带宽大约 2TB/s。7B 模型 FP16 权重 14GB每生成一个 token最乐观也要把这 14GB 读一遍才能算完14GB ÷ 2TB/s ≈ 7ms。也就是说单条请求下理论极限也就 140 token/s 出头。这还没算 KV Cache 的读取、写回和中间结果实际只能更低。真正常见的单条请求生成速度在几十到上百 token/s 之间这个经验值大家多多少少都见过。于是“量化为什么能明显加快本地推理”答案也在这里。7B 模型用 4-bit 量化后权重从 14GB 缩到约 4GB同样带宽下单步搬运时间从 7ms 降到 2ms解码上限能拉到四五百 token/s 量级。这也是大家在本地模型工具里感觉到“量化后明显变快”的根本原因——模型精度损失了一点但数据搬运减了三分之二速度收益立竿见影。你之前可能以为是量化优化了算法其实是优化了“搬运工的工作量”。3.3 批处理为什么能救吞吐却救不了延迟既然 Decode 被带宽锁死而带宽又是“按字节计费的固定资源”那最简单的思路就是让同一份权重被更多请求共用一个 Decode 步里同时生成 8 条、16 条请求的下一个 token。权重还是只读一遍但一次算出了 8 个、16 个 token相当于单位时间产出的 token 总数直接翻倍。这就是推理框架里连续批处理的基本逻辑不再等一批请求全部生成完才换下一批而是每步都检查哪些序列已经结束立刻把新请求加进来让 GPU 上的请求“流水线式”地进出。实践下来吞吐量可以比朴素批处理高出好几倍。这一步是线上推理性能拉开差距的关键很多号称吞吐几千 token/s 的服务靠的不是模型本身而是这种调度策略。但要注意批处理救的是“单位时间产出的 token 总量”不是“单个用户看到第一个字的速度”。批得越大单步计算时间越长每个请求反而可能变慢一点点。所以线上服务通常要做一个取舍在延迟可接受的范围内尽量加大 batch把吞吐顶上去。这也是为什么同一个模型有人部署出来每秒几千 token有人只有几百差别往往不在模型而在批次调度策略。3.4 架构层面的对策GQA 与 KV 缓存瘦身Decode 慢很大一部分原因是每步都要读 KV Cache。既然 KV Cache 体积正比于“KV 头数 × 上下文长度”那么最直接的架构级优化就是减少 KV 头数。早期 Transformer 用 MHA查询头数和 KV 头数一样多后来出现的 GQA分组查询注意力让多个查询头共享少数几个 KV 头KV Cache 体积直接缩小好几倍更激进的 MQA 只有一个 KV 头缓存体积最小但表达力略打折。现在主流的新开源模型几乎清一色选 GQA不是因为别的就是算过这笔账——长上下文下KV Cache 的带宽压力比想象中大得多。除了架构工程上还能给 KV Cache 本身瘦身用 8-bit 甚至更低精度量化缓存、按需分页管理显存碎片、用滑动窗口限制注意力范围。这些手段本质上都是在压缩“每一步要搬运的字节数”。理解 Decode 是带宽瓶颈之后你就能理解为什么这些优化会出现在每一个推理框架里而且几乎都是默认开启。4. 两张表看清二者的差异4.1 体验指标TTFT、TPOT、TGS做推理服务的人至少要分清三个指标TTFT、TPOT 和 TGS。TTFTTime To First Token是“首 token 延迟”从请求进入系统到吐出第一个字的时间。它主要由 Prefill 耗时决定也受排队时间影响。长提示词、高并发会导致 TTFT 飙升用户感知就是“问了问题半天没反应”。TPOTTime Per Output Token是“每个输出 token 的耗时”即 Decode 单步的延迟。它决定了用户看到文本“一个字一个字蹦出来”的速度主要由模型大小、量化、KV Cache 长度和硬件带宽决定。TGSTokens Generated per Second是生成吞吐单位时间产出的 token 总数既可以算单条请求也可以算整个 GPU。单条请求的生成速度约等于 1/TPOT整卡的吞吐则要乘上并发数。很多新手只盯着“每秒多少 token”其实那是一个混合指标如果工具把整个请求含 Prefill的耗时平均到每个 token长提示词会把数字拉低掩盖 Decode 的真实速度。正确做法是把两个阶段分开统计否则你根本不知道慢在哪。4.2 Prefill 与 Decode 全维度对比维度PrefillDecode输入长度等于提示词长度可能成百上千恒为 1 个 token计算形态大矩阵乘法并行度高小矩阵加注意力串行步进主要瓶颈算力FLOPs显存带宽搬运字节GPU 利用率高可接近峰值低算力大量闲置耗时特征一次性随提示词长度线性增长每步固定成本随步数线性累积决定性指标TTFTTPOT、TGS优化重点矩阵算子、长序列切块KV Cache 瘦身、批处理调度这张表值得贴在工位旁边。日常排查问题先判断现象落在哪一列再动手优化。首字慢查左边打字慢查右边两边混淆了通常白忙一场。5. 工程中的坑与排查思路5.1 首字延迟高先查 prefill最常见的现象是“用户问完问题界面转圈半天才出第一个字”。很多人下意识觉得是模型生成慢其实是 Prefill 还没算完。排查时先看推理日志里的 prefill 耗时和提示词长度。如果提示词包含超长的系统提示、few-shot 示例或者检索出来的一堆上下文Prefill 的 token 数可能已经上万。优化顺序我建议是先砍掉不必要的提示词内容再做前缀缓存、把相同系统提示的计算结果跨请求复用最后才考虑换更大的显存或量化模型。不要一上来就砸钱换卡很多时候是提示词设计的问题。5.2 生成速度忽快忽慢prefill 与 decode 抢资源线上服务还有一类怪问题同一个模型、同一批硬件某段时间生成速度稳定某段时间每步都卡一下。如果排除了网络和磁盘多半是 Prefill 在抢资源。原因在于如果一条长提示词的 Prefill 完整占用 GPU同批次的 Decode 步骤全都会被堵住。你观察到的“输出卡顿”其实是别的请求的 Prefill 插队了。对策是把 Prefill 切块和 Decode 交错执行或者限制单次请求的最大提示词长度从源头避免出现“巨无霸 Prefill”。如果你用的推理框架支持相关调度参数优先把它打开。5.3 显存爆掉KV Cache 的线性账单“上下文长度设到 32K 后并发稍微一高就 OOM”这是长上下文时代的常见事故。原因就是 KV Cache 随上下文线性增长。按前面算的 128KB/token 的例子32 条并发、每条 16K 上下文光 KV Cache 就要 64GB整卡显存几乎被吃光还怎么跑模型权重所以设置 max 上下文长度时一定要把并发数考虑进去。估算公式很简单单 token KV Cache 大小 × 平均上下文长度 × 并发数 KV Cache 总需求。跑不动就不要把上限拉到理论值更不要相信“支持 200K 上下文”就等于“能同时处理 100 路 200K 的请求”。厂商宣传的是单条请求的上限不是并发下的上限。另一个经验是优先选 KV 头少的模型或者打开 KV Cache 量化。这两个手段一个改架构、一个改精度目标一致减少每步搬运量和显存占用。我在部署长上下文服务时通常先把 KV Cache 量化打开再调并发最后才考虑换大显存卡。5.4 该优化哪一边先用数据说话在动手优化前强烈建议先花十分钟统计自己的流量特征平均提示词长度是多少平均输出长度是多少并发是多少。如果场景是“长文档问答”Prefill 占比会高得吓人优化重点放在 Prefill 侧比如前缀缓存、提示词压缩如果场景是“多轮聊天、代码生成”输出往往很长Decode 才是大头优化重点放在批处理调度和 KV Cache 瘦身上。不要听别人说“量化就完了”量化两边都受益但收益最大的永远是带宽受限的 Decode。先分清自己卡在哪一边再对症下药往往能省下不少折腾时间。6. 一些个人体会我自己踩过几次坑之后最大的体会是Prefill 和 Decode 明明是同一个模型但优化策略几乎要按两个不同系统来对待。你问“模型快不快”答案取决于你问的是“读题快”还是“写字快”这两个指标在很多场景下甚至此消彼长把批处理拉满吞吐上去了单用户延迟却可能变差把提示词压缩到极致首字快了但上下文信息少了回答质量又可能下降。最后再分享一个实用小技巧看工具日志时把“每秒 token 数”这类混合指标拆开看。Prefill 的 token/s 动辄上千Decode 的 token/s 通常只有几十这是正常的不代表你的模型出了问题。真正要盯的是你服务的真实场景里两阶段各占多少耗时然后把力气花在占比大的那一侧。想清楚这一点再和那些动不动就建议“换更大的卡”的人聊天你会发现大部分瓶颈其实一张性价比合适的卡加一点调度优化就能解决根本不用盲目堆硬件。