资讯动态

大模型推理端到端流程详解:从请求接入到流式返回的性能优化实践

发布时间:2026/10/20 6:58:55 来源:尧图企业网站定制
1. 从一次线上延迟抖动说起为什么需要端到端视角很多人第一次接触大模型推理都是从一行model.generate()开始的。本地跑个小模型输入一句话几秒钟后吐出结果感觉这事儿挺简单。可一旦把它搬到线上面对几十路并发、几百毫秒的延迟预算、还有按 token 计费的成本压力你会发现原来那行代码背后藏着一整条链路任何一个环节掉链子用户端感受到的就是卡。我印象很深的一次是帮一个团队排查推理服务的延迟抖动问题。监控上看 P99 延迟时不时飙到两三秒但 GPU 利用率却不高显存也没打满。一开始大家怀疑是模型太大、算力不够差点就去申请加卡。后来把整条链路拆开逐段打点才发现真正的瓶颈根本不在 GPU 计算上而是请求排队和 KV Cache 的显存碎片导致的反复分配释放。这件事让我彻底意识到大模型推理从来不是一次前向计算这么简单它是一个端到端的系统工程。所谓端到端流程指的是从用户发出请求到最终拿到完整回复中间经过的每一个环节。它大致包括请求接入与排队、输入预处理与分词、Prefill 预填充阶段、Decode 逐 token 解码阶段、KV Cache 管理、批处理调度、采样与后处理、流式返回以及贯穿全程的显存与算力资源调度。这条链路上有的环节吃算力有的环节吃显存带宽有的环节纯粹是 CPU 和内存的活儿还有的环节是调度策略的博弈。理解这条链路的价值在于当性能出问题时你能快速定位到底是哪一段的锅而不是盲目加卡。加卡能解决算力瓶颈但解决不了调度不当、显存碎片、分词拖慢这些问题。反过来一个懂端到端流程的人往往能在不加硬件的前提下把吞吐提升一大截。这篇内容适合几类人看刚入门想搞懂推理服务到底怎么跑起来的开发者已经在做推理优化、但总觉得优化点抓不准的工程师还有做技术选型和容量规划、需要理解各环节资源消耗的架构同学。我会尽量把每个环节讲透既说清楚是什么也讲明白为什么这么设计再补上一些实际踩坑的经验。全程不堆术语能用生活例子讲清楚的就不绕弯子。2. 请求进来之后接入、排队与预处理里藏着的细节2.1 请求接入层为什么不能只是转发很多人以为推理服务的入口就是个反向代理把 HTTP 请求转给后端 GPU 就完事了。实际上接入层承担的责任比想象中重得多。它要做协议解析、鉴权、限流、请求体校验还要把不同格式的输入统一成内部结构。更关键的是它决定了请求以什么顺序、什么粒度进入后面的调度队列。举个实际场景如果接入层不做任何缓冲来一个请求就直接丢给调度器那么当瞬时流量突增时调度器会被大量细碎请求淹没批处理根本攒不起来GPU 利用率上不去。反过来如果接入层做了合理的排队和微批聚合就能把同一时刻到达的多个请求打包成一个批次一起送进去吞吐立刻不一样。这就是为什么很多推理框架会在接入层和调度层之间加一个队列缓冲。提示队列不是越长越好。队列太长会让请求的排队延迟不可控用户等半天才轮到体验反而更差。通常要结合延迟预算反推队列深度。2.2 分词与输入预处理被低估的 CPU 瓶颈分词Tokenization这一步看起来只是把文本切成 token但它经常成为隐藏的 CPU 瓶颈。尤其是当并发量上来之后分词、padding、attention mask 构造这些操作都在 CPU 上跑如果实现得不够高效GPU 就会空等数据出现GPU 利用率上不去但请求也处理不快的尴尬局面。我见过一个案例服务用的是比较重的分词实现单条请求分词要几毫秒QPS 一高CPU 直接打满GPU 反而闲着一半。后来换成更轻量的分词方案并做了批量化分词整体吞吐提升了将近三成。这个教训是别只盯着 GPUCPU 侧的预处理同样能决定上限。预处理还包括几个容易忽略的点Padding 策略把不同长度的输入补齐到同一长度才能组批。补齐到批次内最长还是补齐到固定长度直接影响计算浪费。补齐到批次内最长更省但批次内长度差异大时短请求会被长请求拖累。位置编码的准备不同模型对位置信息的处理方式不同有的在预处理阶段就要算好有的在模型内部动态生成这会影响预处理的复杂度。多模态输入的处理如果涉及图像、音频预处理阶段还要做特征提取这部分往往比文本分词重得多需要单独规划资源。2.3 排队策略先来先服务未必是最优解请求排队的策略直接决定了延迟分布。最常见的先来先服务FIFO实现简单但在混合负载下表现一般。比如一个长请求排在前面后面一堆短请求就得干等这就是所谓的队头阻塞。实际生产中更常用的是带优先级的调度或者按预估输出长度做排序。有些系统会把短请求优先处理因为短请求占用资源时间短能快速释放整体平均延迟更低。还有的会做连续批处理Continuous Batching也就是不等整批做完某个请求一结束就立刻把新请求塞进空出来的位置让 GPU 始终处于忙碌状态。这里有个反直觉的点追求单请求最快和追求整体吞吐最高往往是矛盾的。单请求最快意味着它独占资源、不等待组批整体吞吐最高意味着要攒批、要等待。线上服务通常要在两者之间找平衡而这个平衡点取决于你的业务是延迟敏感还是吞吐敏感。3. Prefill 与 Decode推理计算的两个性格完全不同的阶段3.1 Prefill 阶段一次性把输入读进去Prefill 阶段处理的是用户输入的整段 prompt。它的特点是一次性把所有输入 token 一起送进模型做完整的前向计算生成第一个输出 token同时把每一层的 Key 和 Value 缓存下来供后续解码使用。这个阶段的算力特征很鲜明计算密集矩阵乘法规模大GPU 利用率容易打满。因为输入的所有 token 可以并行计算天然适合批处理和并行加速。也正因如此Prefill 阶段对算力的需求是爆发式的——一个长 prompt 进来瞬间就要吃掉大量算力。Prefill 的耗时和输入长度基本成正比。输入越长计算量越大耗时越长。这就解释了一个常见现象为什么用户输入一大段文字后首 token 的等待时间明显变长。首 token 延迟Time To First TokenTTFT主要就是被 Prefill 阶段决定的。注意长 prompt 的 Prefill 不仅慢还会占用大量显存来存 KV Cache。如果并发里有好几个长 prompt显存压力会陡增甚至触发 OOM。3.2 Decode 阶段一个 token 一个 token 地挤出来Decode 阶段是逐 token 生成的过程。每生成一个 token都要基于之前所有 token 的 KV Cache 做一次前向计算然后采样出下一个 token再把它追加到序列里循环往复直到遇到结束符或达到长度上限。这个阶段的算力特征和 Prefill 截然不同它是显存带宽密集型的而不是算力密集型的。因为每次只处理一个 token矩阵乘法的规模很小GPU 的算力根本用不满真正的瓶颈在于把庞大的 KV Cache 从显存里读出来。换句话说Decode 阶段 GPU 大部分时间在搬数据而不是算数据。这就带来一个关键认知优化 Decode 阶段重点不是提升算力而是减少显存访问、提高显存带宽利用率。这也是为什么各种 KV Cache 量化、稀疏化、分页管理技术层出不穷——它们的目标都是让 KV Cache 更小、读取更快。Decode 阶段的另一个特点是它决定了输出 token 的生成速度也就是所谓的 TPOTTime Per Output Token。用户感受到的打字机式流式输出节奏就是由 Decode 速度决定的。3.3 两个阶段的资源画像对比把两个阶段放在一起对比能更清楚地看到它们的差异维度Prefill 阶段Decode 阶段处理对象整段输入 prompt单个输出 token计算特征计算密集显存带宽密集并行度高token 间可并行低token 间串行依赖GPU 利用率容易打满通常偏低主要瓶颈算力显存带宽与 KV Cache 读取对应指标首 token 延迟 TTFT每 token 延迟 TPOT显存影响写入 KV Cache读取并追加 KV Cache理解这张表很多优化思路就顺理成章了。比如 Prefill 和 Decode 混在一起跑时会互相干扰Prefill 抢占算力拖慢 Decode 的节奏Decode 占用显存带宽拖慢 Prefill。于是就有了Prefill 与 Decode 分离部署的思路——把两类负载放到不同的资源池各取所需互不干扰。这个思路在近两年被广泛讨论本质就是承认这两个阶段性格不合硬凑在一起效率低。4. KV Cache推理服务里最值得精打细算的一块显存4.1 KV Cache 到底缓存了什么要理解 KV Cache得先回到注意力机制。在自注意力计算里每个 token 都会生成 Query、Key、Value 三个向量。生成新 token 时它需要和之前所有 token 的 Key 做点积算注意力权重再对 Value 加权求和。如果每生成一个 token 都把之前所有 token 的 Key、Value 重算一遍计算量会随序列长度平方增长完全不可接受。KV Cache 的思路就是把已经算过的 Key 和 Value 存下来下次直接复用不重算。这样每生成一个新 token只需要算它自己的 Q、K、V然后和缓存的 K、V 做运算即可。计算量从平方级降到线性级这是自回归生成能实用的关键。代价是显存。KV Cache 的大小和几个因素成正比层数、注意力头数、头维度、序列长度、批大小以及数据类型。粗略估算一个中等规模模型在长序列、大 batch 下KV Cache 占用几个 GB 到几十 GB 都很正常。这就是为什么显存经常是推理服务的头号约束。4.2 显存碎片那个让 P99 延迟飙升的隐形杀手回到开头提到的那个延迟抖动案例。当时排查到最后问题就出在 KV Cache 的显存管理上。服务用的是朴素的按最大长度预分配策略每个请求一进来就按可能的最大序列长度预留显存。结果短请求浪费了大量预留空间长请求又可能因为找不到连续大块显存而等待显存碎片越来越严重分配释放的开销也越来越大延迟自然就抖。解决思路是引入更细粒度的显存管理比如分页式管理Paged Attention 的思路。它把 KV Cache 切成固定大小的小块按需分配不要求连续。这样一来显存利用率大幅提升碎片问题也缓解了。实测下来同样的显存能支撑的并发数明显增加P99 延迟也稳定了很多。提示如果你的服务在并发升高时出现延迟抖动、显存利用率却不高优先怀疑显存碎片和分配策略而不是急着加卡。4.3 给 KV Cache瘦身的几种常见手段既然 KV Cache 是显存大户围绕它的优化自然很多。常见的几类量化把 KV Cache 从 FP16 降到 INT8 甚至更低。显存占用直接减半甚至更多代价是精度可能略有损失需要评估对输出质量的影响。稀疏化与淘汰不是所有历史 token 都同等重要。有些方案会淘汰掉注意力权重低的 token 的 KV或者只保留关键 token 的缓存从而压缩占用。共享与复用多个请求如果有相同的 prompt 前缀比如相同的系统提示词可以共享这部分 KV Cache避免重复存储和计算。这在有大量相同前缀的场景下收益巨大。分层存储把不常用的 KV 放到更慢但更大的存储里需要时再换回来。这是拿延迟换显存的思路适合对延迟不那么敏感的场景。每种手段都有取舍没有银弹。量化省显存但可能掉精度稀疏化省显存但实现复杂共享复用收益大但依赖请求特征。选哪种取决于你的业务更在意什么。5. 批处理与调度把 GPU 喂饱的艺术5.1 静态批处理为什么不够用最早的批处理是静态的攒够一批请求一起送进 GPU等整批全部生成完再处理下一批。这种方式实现简单但效率低。因为一批里各个请求的输出长度不一样短的早就生成完了却要等最长的那个结束GPU 在等待期间白白空转。这就是长尾拖累问题。一批里只要有一个超长输出整批的完成时间就被它拉长其他请求的资源被浪费。静态批处理在输出长度差异大的场景下效率损失非常明显。5.2 连续批处理让 GPU 一刻不停连续批处理Continuous Batching是解决这个问题的关键思路。它的核心是不等整批结束任何一个请求生成完就立刻退出空出来的位置马上填入新请求。这样 GPU 的批次始终是满的利用率大幅提升。打个比方静态批处理像一辆公交车必须等所有乘客到终点才发下一班连续批处理像流水线有人下车就有人上车车一直在跑。实测中连续批处理能把吞吐提升数倍尤其在输出长度参差不齐的对话场景下效果显著。实现连续批处理需要调度器精细管理每个请求的状态还要处理 KV Cache 的动态分配释放复杂度不低。但收益摆在那里所以主流推理框架基本都支持。5.3 调度策略里的取舍延迟、吞吐与公平性调度器要同时照顾好几个目标而这些目标经常打架延迟优先让每个请求尽快开始、尽快结束可能需要牺牲批次大小吞吐上不去。吞吐优先尽量攒大批次让 GPU 满载但单个请求的等待时间变长。公平性避免某些请求被无限期饿死尤其在混合长短请求时。实际系统里常见的做法是分优先级队列或者按预估输出长度排序。还有的会做抢占式调度高优先级请求来了可以打断低优先级请求。这些策略没有绝对优劣关键看业务场景。在线对话服务通常延迟敏感批处理规模要克制离线批量生成任务吞吐敏感可以大胆攒批。注意调度策略的调整一定要配合压测验证。纸面上算出来的最优参数放到真实流量下经常不是那么回事因为真实请求的长度分布、到达模式都和测试假设不同。6. 采样、后处理与流式返回用户真正感知到的部分6.1 采样策略如何影响输出质量与速度模型前向计算输出的是每个位置上的 logits也就是词表上的分数分布。真正决定生成哪个 token 的是采样策略。常见的几种贪心解码每次选概率最高的 token。速度快、结果确定但输出容易重复、缺乏多样性。温度采样用温度参数调节分布的平滑程度。温度高更随机温度低更保守。Top-k 采样只在概率最高的 k 个 token 里采样过滤掉长尾噪声。Top-p 采样核采样在累积概率达到 p 的最小集合里采样动态调整候选范围。采样本身的计算量不大但它影响输出长度进而间接影响总耗时。比如温度设得高模型可能生成更发散、更长的内容Decode 阶段就要多跑很多步。所以采样参数不只是效果调优也是性能调优的一部分。6.2 后处理别让最后一步拖了后腿后处理包括 detokenize把 token 还原成文本、特殊字符清理、格式化、敏感内容过滤等。这些操作看起来轻量但在高并发下同样可能成为瓶颈尤其是 detokenize 和字符串拼接如果实现不当CPU 开销不容小觑。还有一个容易被忽略的点流式返回时的分块策略。如果每生成一个 token 就立刻推给客户端网络往返次数太多开销大如果攒一大块再推用户又觉得不够实时。通常要在两者之间找个平衡比如按固定 token 数或固定时间间隔推送。6.3 流式返回对整体体验的影响流式返回是提升用户感知速度的利器。虽然总生成时间没变但用户能很快看到第一个字心理上就觉得快。这也是为什么首 token 延迟TTFT比总生成时间更受关注——它直接决定了用户的第一印象。实现流式返回需要整条链路都支持增量输出模型逐 token 生成后处理逐块处理网络层用流式协议推送。任何一环不支持流式整条链路就退化成等全部生成完再返回体验大打折扣。7. 把整条链路串起来一次请求的完整旅程7.1 从请求到响应的逐段拆解现在把前面讲的串起来看一次请求到底经历了什么接入请求到达接入层完成鉴权、限流、格式校验进入排队队列。预处理分词、构造 attention mask、准备位置信息可能还要处理多模态输入。调度调度器根据策略决定这个请求何时、和哪些请求一起组批。Prefill整段输入一次性前向计算生成首 token写入 KV Cache。Decode 循环逐 token 生成每步读取 KV Cache、计算、采样、追加缓存。采样与后处理每个 token 经过采样策略确定再 detokenize、清理、过滤。流式返回按分块策略把结果推给客户端直到生成结束。资源回收请求结束释放 KV Cache 和占用的批次位置供新请求使用。这条链路上每一段都有自己的耗时和资源消耗。端到端优化的本质就是找到当前瓶颈在哪一段然后针对性地解决。7.2 各环节的典型瓶颈与排查方向环节典型瓶颈排查方向接入与排队队列积压、限流误伤看队列深度、排队时长分布预处理CPU 打满、分词慢看 CPU 利用率、预处理耗时Prefill首 token 延迟高看输入长度分布、算力利用率Decode每 token 延迟高看显存带宽、KV Cache 大小显存管理碎片、OOM、抖动看显存利用率、分配释放频率调度GPU 空转、批次不满看批次大小、GPU 利用率曲线后处理CPU 开销、推送频繁看后处理耗时、网络往返次数这张表可以当作排查清单用。遇到性能问题先定位是哪一段再顺着对应方向查比盲目加卡高效得多。7.3 端到端优化的优先级怎么排优化要有优先级不能眉毛胡子一把抓。我的经验是先解决显存问题。显存是硬约束OOM 了什么都白搭。优先上分页管理、量化、前缀共享这些手段。再优化调度和批处理。这是提升吞吐性价比最高的地方连续批处理往往能带来数倍收益。然后针对 Prefill 和 Decode 分别优化。长 prompt 场景重点优化 Prefill长输出场景重点优化 Decode。最后抠预处理和后处理的细节。这些是锦上添花但在高并发下也能积少成多。顺序很重要。如果显存没管好就去调调度很可能调着调着就 OOM 了前面的努力全白费。8. 几个容易踩的坑和我的实际体会8.1 只盯 GPU 利用率忽略整条链路最常见的误区就是GPU 利用率低就是没优化好。其实 GPU 利用率低可能是好事也可能是坏事。如果是因为请求少、负载轻那正常如果是因为 CPU 预处理慢、调度不当导致 GPU 空等那就是问题。判断的关键是看 GPU 空等的原因而不是单纯看数字。我一般会同时看几个指标GPU 利用率、CPU 利用率、显存利用率、队列深度、各阶段耗时。单看一个指标很容易误判。8.2 盲目追求大 batch大 batch 能提升吞吐但不是越大越好。batch 太大单请求等待时间变长延迟上升显存占用也增加可能触发 OOM。而且 batch 大到一定程度收益递减因为 GPU 已经饱和了。找到那个刚好喂饱 GPU 又不浪费的 batch 大小比一味求大更重要。8.3 忽视请求长度分布的影响很多优化方案在测试时用的是均匀长度的请求效果很好上线后却拉胯。原因是真实请求的长度分布往往是长尾的——大部分短少数极长。长尾请求会拖累批处理效率也会占用大量显存。所以压测时一定要用贴近真实的长度分布最好直接采样线上流量回放。8.4 我个人的一点体会做了这么多推理优化我最大的体会是端到端流程里没有哪个环节是孤立的。你优化了 Prefill可能给 Decode 带来压力你增大了 batch可能加剧显存碎片你上了量化可能影响输出质量进而改变生成长度。所有环节都是联动的改一处要观察全局。所以我的习惯是每次调整只动一个变量然后完整观察整条链路的指标变化确认没有副作用再继续。这种小步快跑、全程监控的方式比一次性大改要稳得多。另外别迷信任何最佳实践因为每个业务的流量特征、延迟预算、成本约束都不一样适合别人的参数未必适合你。多压测、多观察、多迭代才是正道。最后分享一个小技巧如果条件允许给推理服务加上细粒度的分段耗时打点从接入到返回每一段都记录。平时可能觉得这些日志没用但一旦出问题这些数据就是定位瓶颈的救命稻草。我排查过的绝大多数性能问题靠的都是这些分段数据而不是猜。

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

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

免费获取报价 →
↑