资讯动态

大模型推理全链路拆解:从分词到流式返回的端到端优化实践

发布时间:2026/10/20 16:22:34 来源:尧图企业网站定制
1. 从一次线上延迟抖动说起为什么端到端视角比单点优化更重要很多人第一次接触大模型推理注意力会立刻被某个具体环节吸走——有人盯着量化精度有人死磕KV Cache的显存占用还有人一上来就研究连续批处理continuous batching的调度策略。这些当然都重要但我在实际排查问题时发现真正让人抓狂的往往不是某个单点不够快而是整条链路里各环节的假设对不上上游按一种方式算token下游按另一种方式切分调度器以为请求是均匀到达的实际流量却是脉冲式的。结果就是单看每个模块的指标都挺漂亮端到端一跑就抖得厉害。这篇内容想做的事情是把大模型推理从请求进来到结果返回的完整链路拆开讲一遍。它不是某篇论文的复述也不是某个框架的官方文档翻译而是一个做过若干次推理服务搭建和调优的人把这条链路上真正需要理解的环节、容易踩的坑、以及各环节之间的耦合关系讲清楚。适合的读者是已经跑通过至少一个推理Demo、想搞清楚背后到底发生了什么的人或者正在做推理服务、被延迟和吞吐折腾过的人。如果你完全没接触过Transformer建议先补一下注意力机制的基础否则读起来会有点吃力。我先把整条链路的一句话版本放在这里后面再逐段展开请求进来 → 分词与预处理 → 调度与批处理 → 模型前向含KV Cache管理→ 采样与后处理 → 流式返回。这六步听起来平平无奇但每一步都有它自己的隐藏假设而这些假设正是端到端性能的胜负手。2. 请求进入推理引擎之前分词、模板与预处理里被低估的开销2.1 分词不是顺手做一下那么简单分词tokenization在很多人眼里是个无足轻重的步骤毕竟它就是个查表加切分。但实测下来当请求量上去之后分词环节的CPU占用会变得相当可观尤其是当输入文本很长、或者用了那种基于BPE的复杂词表时。我见过一个场景模型前向只占了总延迟的六成剩下四成里有一大半耗在了分词和模板拼接上。这里的关键在于分词是纯CPU操作而模型前向通常在GPU上。如果分词和推理串行执行GPU就会周期性地空转等数据。合理的做法是把分词放进独立的进程池或线程池让它和前向计算形成流水线。Python里因为GIL的存在多线程对CPU密集任务帮助有限所以更稳的是用多进程或者干脆用Rust/C写一个分词服务。另一个容易被忽略的点是词表与模型的匹配。有些团队为了省事直接拿一个通用分词器配一个微调过的模型结果就是同样的文本被切成了和训练时不一样的token序列输出质量肉眼可见地下降。分词器必须和模型训练时用的完全一致这不是可选项。2.2 对话模板一个字符的差异就能毁掉输出如果你用的是对话模型那在分词之前还有一步把多轮对话按模型要求的模板拼成一段文本。这个模板通常包含角色标记、分隔符、以及特殊的起止token。不同模型的模板差异极大有的用|user|有的用[INST]有的靠换行和特定前缀区分。我踩过的一个坑是模板里多了一个空格模型就开始胡言乱语。原因是这个空格改变了token序列让模型进入了一个它训练时没见过的分布。所以模板拼接这件事一定要用模型官方给出的格式并且写测试用例去校验拼接结果和预期完全一致。别凭感觉。2.3 预处理阶段值得单独计时的几个指标为了后面排查问题方便我建议在预处理阶段至少埋这几个点指标含义关注点分词耗时从原始文本到token id序列长文本时是否线性增长模板拼接耗时多轮对话组装轮数多时是否退化队列等待时间进入调度器前的排队是否成为瓶颈输入token数分布请求长度分布决定批处理策略这些指标单独看没什么但当你发现端到端延迟抖动时它们能帮你快速定位是数据没准备好还是算力不够。3. 调度与批处理吞吐和延迟在这里做交易3.1 静态批处理为什么在真实场景里不够用最早的推理服务用的是静态批处理攒够一批请求一起送进模型等这批全部算完再返回。这个方案在离线评测里很漂亮吞吐数字很好看。但真实场景里请求是陆续到达的而且每个请求的输出长度差异巨大。静态批处理会导致两个问题一是短请求要等长请求算完才能返回延迟被拉长二是GPU在等待新批次凑齐时处于空闲。这就是连续批处理continuous batching出现的背景。它的核心思想是不按批次为单位而按迭代步为单位。每一步只算当前还在生成的那些序列某个序列生成完了就立刻退出腾出的位置马上给新来的请求。这样GPU的利用率能维持在高位同时短请求的延迟也不会被长请求拖累。3.2 连续批处理的调度器到底在做什么决策调度器每一步要回答几个问题这一轮哪些请求参与计算新请求能不能插进来显存够不够放新的KV Cache如果不够该驱逐谁这里有个关键概念叫分页注意力PagedAttention它把KV Cache切成固定大小的块像操作系统管理内存页一样管理显存。好处是显存碎片少而且不同序列可以共享块比如相同的系统提示词前缀。调度器在决定接纳新请求时实际是在问还有没有空闲的块如果没有能不能通过驱逐低优先级序列来腾出块调度策略直接影响用户体验。一个常见的做法是给请求分优先级比如交互式请求优先于批量任务。但优先级不能滥用否则低优先级请求可能饿死。我一般会设一个最大等待时间超过就提升优先级保证公平性。3.3 批大小不是越大越好很多人有个直觉批越大吞吐越高。这在某个范围内成立但超过临界点后单个token的生成延迟会上升因为每一步要算的序列变多了。对于延迟敏感的在线服务这个临界点通常比想象中低。我的经验是先测出在可接受延迟下的最大批大小然后留20%的余量。同时要监控每步耗时和队列长度如果队列持续增长说明吞吐跟不上到达率这时候要么加机器要么优化单步耗时而不是盲目调大批大小。4. 模型前向KV Cache、注意力计算与那些省不掉的成本4.1 自回归生成为什么是一步一个token大模型生成文本是自回归的给定前面的token预测下一个然后把预测结果接到后面再预测下一个。这意味着生成N个token需要做N次前向。每次前向都要重新计算注意力但前面token的Key和Value其实没变所以可以缓存起来复用这就是KV Cache的由来。没有KV Cache的话生成第N个token要重新算前N-1个token的K和V计算量是平方级的。有了缓存每步只需要算当前token的K和V然后和缓存的拼接。这是推理能跑起来的基础。4.2 KV Cache的显存账要提前算清楚KV Cache的大小是可以估算的公式大致是KV Cache大小 2 × 层数 × 注意力头数 × 头维度 × 序列长度 × 批大小 × 数据类型字节数举个例子一个70亿参数的模型32层32个头头维度128用FP162字节序列长度2048批大小162 × 32 × 32 × 128 × 2048 × 16 × 2 ≈ 17.2 GB这还没算模型权重本身。所以显存规划必须把KV Cache算进去否则跑到一半OOM显存溢出就很尴尬。这也是为什么长上下文场景下即使用了PagedAttention也要限制并发数。4.3 注意力计算的两种形态预填充与解码前向其实分两个阶段。**预填充prefill**是处理输入prompt所有token一次性并行计算计算密集GPU利用率高。**解码decode**是逐个生成token每步计算量小但受限于显存带宽GPU计算单元大量空闲。这两个阶段的特性完全不同所以现在很多引擎会把它们分开调度甚至放在不同的硬件上。预填充适合大batch并行解码适合小batch低延迟。理解这个区别才能明白为什么有些优化对预填充有效、对解码无效。4.4 量化省显存和保精度之间的平衡量化是把权重或激活从FP16降到INT8、INT4甚至更低。好处是显存占用下降、带宽压力减小解码阶段尤其受益因为解码是带宽瓶颈。但量化不是免费的。权重量化相对成熟INT8基本无损INT4在多数模型上也能接受。激活量化就麻烦一些因为激活的分布是动态的需要校准。我的一般建议是先做权重量化如果显存还是不够再考虑激活量化并且一定要用真实任务评测别只看困惑度perplexity。5. 采样与后处理生成策略如何影响最终体验5.1 贪心、束搜索与采样的取舍模型前向输出的是每个位置上的logits也就是词表上的分数。怎么从这些分数里选token就是采样策略的事。贪心解码每步选分数最高的结果确定但容易重复。束搜索维护多个候选序列适合翻译这类任务但计算开销大。随机采样按概率分布抽配合温度temperature控制随机性。温度高则多样温度低则保守。实际服务里我见过最多的是top-p核采样加温度的组合。top-p只从累积概率达到p的最小token集合里采样能动态调整候选范围比固定top-k更灵活。5.2 重复惩罚与存在惩罚的实际效果模型有时会陷入重复循环尤其是长文本生成。重复惩罚repetition penalty对已经出现过的token降低分数存在惩罚presence penalty则是对出现过的token统一降分不管出现几次。这两个参数调起来很微妙。惩罚太弱没效果太强会导致输出不连贯甚至语法错误。我的经验是从1.1左右的重复惩罚开始试观察输出质量再微调。别一上来就设1.5那样模型会开始说胡话。5.3 流式返回让用户感觉快的关键端到端延迟里用户感知最强的其实是首token时间TTFT而不是总生成时间。流式返回就是每生成一个token就立刻推给客户端用户看到文字一个个蹦出来主观上觉得很快。实现流式返回要注意一是要处理好token到文本的转换因为一个词可能被切成多个token要等拼完整了再显示二是要处理网络中断和超时三是要考虑背压如果客户端消费慢服务端不能无限缓冲。6. 端到端串联那些只有跑起来才会暴露的耦合问题6.1 预处理慢会伪装成模型慢前面提过分词和模板拼接的开销。实际排查时如果只监控GPU指标会看到GPU利用率不高然后误以为是模型或调度的问题。正确的做法是在请求入口和模型入口各打一个时间戳算出预处理耗时。我遇到过预处理占了总延迟35%的情况优化分词器之后端到端直接降了三成。6.2 批处理策略和显存管理的冲突连续批处理想多塞请求提高吞吐但KV Cache的显存是有限的。如果调度器不感知显存压力就可能接纳了太多请求导致频繁的块驱逐和重计算反而拖慢整体。所以调度器必须和显存管理器联动把显存作为一等公民来考虑。6.3 采样参数不一致导致的玄学问题有时候同一个请求两次返回的结果差异很大用户会以为是模型不稳定。其实可能是采样参数没固定比如温度默认值在不同接口里不一样。生产环境里对于需要确定性的场景应该显式设置温度接近0并固定随机种子。6.4 一个可复现的排查顺序当端到端延迟异常时我一般按这个顺序查看请求入口到模型入口的耗时排除预处理问题。看队列等待时间判断是吞吐不足还是到达率突增。看每步解码耗时判断是计算瓶颈还是带宽瓶颈。看KV Cache的块使用率和驱逐次数判断显存是否吃紧。看采样和后处理耗时这部分通常小但流式返回的网络开销不能忽略。这个顺序的逻辑是从外到内从便宜到贵。先排除那些改起来成本低的环节再动模型和调度。7. 我在实际部署中攒下的几条经验第一条别在没测过端到端的情况下调单点参数。我见过有人把量化精度调到最低单看模型前向确实快了但输出质量崩了用户投诉一堆最后还得回滚。端到端评测要包含真实任务的成功率和延迟分布不能只看平均值。第二条首token时间和总生成时间要分开优化。前者靠预填充优化和调度优先级后者靠解码效率和批处理。混在一起谈推理慢是没法定位的。第三条显存规划要留余量。KV Cache的实际占用会随着序列长度动态变化峰值可能比估算高。留20%到30%的余量能避免很多半夜被报警叫醒的情况。第四条流式返回的边界情况要测。客户端提前断开、网络抖动、超长输出这些在Demo里不会遇到但上线后一定会遇到。提前写好超时和清理逻辑比事后补要省心得多。第五条监控要覆盖全链路。只监控GPU利用率是不够的预处理耗时、队列长度、块驱逐次数、采样耗时这些都要有指标。出问题时数据比直觉可靠。这条链路看起来环节不多但每个环节都有自己的脾气。把它们的假设对齐把耦合关系理清端到端才能稳。我自己的体会是推理优化到最后拼的不是某个单点技术多先进而是对整个链路的理解有多深。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑