
先说一个具体的场景。你在本地搭了一个 LLM 推理服务平时用起来挺顺单次请求一两秒出结果。某天开始多人联调前端同学反馈“接口偶尔很慢有时候十几秒没反应”。你去看监控平均延迟其实不高4 秒左右感觉还能接受。但前端坚持说体验很差因为用户遇到的是那几次特别慢的请求而不是平均值。这个现象在 LLM 服务里太常见了它不是偶发也不是某台机器坏了而是典型的LLM 尾延迟问题。很多人以为尾延迟是“并发高了之后才出现的高级问题”实际上它从你第一次把 LLM 接入业务开始就存在只是单机、单人、短请求时被平均数据掩盖了。这篇文章不打算讲特别复杂的推理优化也不准备讨论多机部署、专家并行之类的工程重武器而是聚焦在一个更实际的问题上如果你的 LLM 服务已经开始出现明显的尾部延迟有没有一些改动成本低、理解成本低、但收益立竿见影的修复手段我的核心判断是LLM 尾延迟的“简单修复”不是一个开关、一个参数也不是换一台更大的 GPU。它是一组围绕请求长度、批处理节奏、KV 缓存、排队策略和客户端等待方式的小改动。把这些小改动按正确顺序做一遍大部分自建服务的尾部延迟都能显著改善。真正难的不是单个配置项而是理解这些配置项到底在解决哪一段延迟。1. 尾延迟不是“偶尔慢一下”而是服务质量的隐藏标准在开始动配置之前先花点时间理解尾延迟为什么值得专门拿出来讲。很多人对延迟管理的印象还停留在“平均响应时间”但 LLM 场景里平均值的欺骗性比普通 Web 服务更大。1.1 从 p50 到 p99为什么尾部比平均延迟更能影响用户普通 HTTP 请求的延迟通常是几百毫秒到几秒用户感知相对统一。LLM 推理请求的延迟跨度则大得离谱一个短 prompt、短输出的请求可能 1 秒完成一个长 prompt、长输出的请求可能要 30 秒甚至更久。把这两类请求混在一起统计平均延迟得出的数字没有太多参考价值。真正影响体验的是长尾分位点通常是 p95、p99。打个比方一个搜索接口如果平均延迟 300ms但 1% 的请求需要 3 秒用户并不会因为“平均还行”而满意只会记住那几次转圈。LLM 生成时间的波动比普通接口大得多所以长尾问题更明显。在聊天、写作辅助、代码生成这类交互式场景里尾延迟的杀伤力尤其明显。用户看到的不只是“慢”而是“为什么有时候快、有时候慢、有时候像卡死了”。这种不确定性比稳定慢更让人烦躁。更麻烦的是用户一旦不想等会频繁刷新或重复提交重复请求会继续占用推理资源反过来让尾部变得更长。这是一个自我强化的恶性循环。1.2 自回归生成为什么天生容易产生长尾LLM 模型的核心解码方式是自回归生成每生成一个 token都要把最新的 token 拼到已有序列后面重新做一次完整的前向计算。也就是说输出 100 个 token就要做大约 100 次前向传递输出 500 个 token就是 500 次。这就带来一个区别于普通服务的关键特点请求的延迟不是一次计算决定的而是“次数 x 单次计算时间”决定的。只要输出长度变化总延迟天然就会拉开差距。同一个模型输出 50 个 token 和输出 500 个 token耗时差 10 倍并不奇怪。同时GPU 上的批处理是动态的。服务端通常会攒一批请求一起算以提高 GPU 利用率。但批处理里如果混着一个很长的请求或者一个很大的 prompt这个 batch 的整体耗时就会被最长的那个请求拖住其他短请求虽然已经生成完了也得等整个 batch 这一轮计算结束才能返回。这也是尾部延迟的重要来源之一不是你的请求本身慢而是你被同一个批次里的“慢邻居”拖累了。再加上 KV cache 的显存占用会随着序列长度动态增长长请求会吃掉更多显存可能挤压其他请求的 batch 容量让整个服务的并发能力出现波动。这些因素叠加起来就让 LLM 服务的尾延迟不像普通服务那样稳定而是呈明显的“长尾分布”大部分请求还可以一小撮请求异常慢。1.3 大多数“慢请求”来自同一个原因等待 GPU而不是计算本身如果你真的去抓几个慢请求的日志通常会发现不是模型计算突然变复杂了而是请求在排队等待 GPU。常见的情况有这么几类调度器把请求塞进一个 batch但 batch 里有一个很长的请求短请求被顺带拖慢。请求长度超过默认预留的 KV cache触发显存动态分配或者被调度器放到下一个 batch。连续两个请求的 prompt 完全不同前缀缓存没有命中每次都要完整重新计算 prompt 部分。并发请求数已经超过 GPU 的吞吐能力但排队策略还是照单全收导致所有请求一起等待。客户端设置了过于宽松的超时时间服务端也没有快速失败机制一堆注定完不成的请求堆积在队列里。所以你会发现尾延迟的一个常见共性是“请求本身并不慢但它花了很多时间在等待资源上”。理解了这一点修复思路就清晰了不是盲目地把每个请求都优化得更快而是减少请求之间互相干扰控制等待时间让 GPU 的资源分配更可预期。2. 先定量再动手搭一条最小的延迟观测链路很多人一上来就调参数这是最容易绕弯路的方式。LLM 服务的延迟链路比普通 Web 服务长涉及模型加载、tokenize、prefill、decode、采样、detokenize、网络传输等多个环节。不先把问题量化改配置就是盲调。2.1 记录三个时间点而不是一个总耗时对 LLM 服务来说总耗时只是一个结果无法帮你定位问题。日常排障至少要拆成三段TTFTTime To First Token从请求发起到返回第一个 token 的时间。它反映的是 prompt 处理、排队、prefill 的综合速度。TPOT或单 token 平均耗时从第一个 token 之后每生成一个 token 的平均间隔。它反映 decode 阶段的吞吐情况。总生成时长从请求开始到最后一个 token 返回的时间等于 TTFT 加上生成阶段总时间。如果 TTFT 异常高问题多半在排队、输入长度或 prefill 阶段。如果 TTFT 正常但后续 token 间隔越来越大问题多半在 decode 阶段或显存、并发资源上。这样拆分后判断会快很多。在代码里记录这三个值不需要很复杂的组件一个中间件、一条结构化日志就能做。关键是要保证日志字段是齐全的至少包括模型名、输入 token 数、输出 token 数、TTFT、总耗时、是否流式、错误码。2.2 按维度分组而不是看一个平均值只有一条“平均耗时”或“p95 耗时”不够还要学会分组看数据。最实用的分组维度是输入长度段短 prompt、中等 prompt、长 prompt 分开统计。输出长度段短输出、长输出分开统计。并发状态低并发时和高并发时的延迟分开看。请求类型同一个模型如果服务多个业务方也建议按业务方分组。实际排查时你会发现很多“尾延迟”根本不是一个统一问题。它可能是某个业务方提交了超长文档也可能是某个应用把 max_tokens 设得特别大。如果不分组你只会看到 p99 变差但不知道是哪类请求在拖后腿。2.3 建立乐观基线确认尾部是否真的异常在并发压力测试之前先做一个单请求基线启动服务发送一次短输入短输出请求记录正常耗时再发送一次较长输入、较长输出请求记录正常耗时。这两组数据能让你知道“一条干净请求该跑多快”。有了基线之后再逐步加并发。比如先 1 个并发再 4 个、8 个、16 个。每个并发档位跑几十条请求记录 p50、p95、p99 和错误率。这个过程的目的是找到服务延迟从“平稳”到“恶化”的临界点。很多情况下你会发现问题不是从 0 到 100 匀速恶化而是到某个并发值后突然恶化这说明资源分配或批处理策略存在拐点。2.4 一条针对 LLM 延迟的排查链路当出现“有的请求特别慢”时建议按下面这个顺序排查而不是直接怀疑模型本身看请求输入和输出长度排除“长任务当短任务压测”的情况。看服务端返回的 TTFT 和 TPOT确认慢在 prefill 还是 decode。看请求进入队列到开始计算之间的排队时间确认是否在等 GPU。看同一时间段有多少并发请求确认 batch 里是否有其他长任务。看显存占用和 KV cache 命中情况确认是否因为显存不足触发了额外分配。最后才考虑模型、框架、驱动和硬件问题。这个排查顺序不需要多么高深的工具只要能区分“请求在等”“请求在算”“模型本身算得慢”这三个状态就行。注意单次跑通只能说明流程没有断并不能证明服务在真实负载下没有问题。尾延迟问题必须放到并发压力下观察。3. 五个被证明有效的简单修复按投入产出排序接下来进入正题。下面这五项是处理 LLM 尾延迟时投入产出比最高、理解代价最低的修复手段。它们没有一个是需要重写推理引擎的也没有一个是依赖最新硬件特性的。每一个都能在普通自建服务里落地。3.1 先限制输出长度一切生成服务的第一道护栏最简单的修复往往是很多人忽略的设置合理的 max_tokens 上限。LLM 生成是逐 token 进行的只要不到结束符模型就会一直生成下去。一个没限制输出长度的请求理论上可以生成几百上千个 token。如果业务场景是短回答但 API 把 max_tokens 设成了 4096那么即使模型本来想快速结束它也有可能在人为限制内持续生成或者生成长度远超需要的文本。输出长度一旦拉长直接带来两个后果这个请求本身耗时变长它占用的 KV cache 显存变多挤压其他请求的 batch 容量间接拉高别人的延迟。所以限制输出长度不只是保护自己也是在保护整个服务的尾部延迟。实操建议是不要给所有业务一个统一的 max_tokens而是按场景分开配置。短问题、分类任务、标题生成、意图识别这类场景给 128 或 256 就够了写作、代码生成、长文档总结可以给 1024 或 2048真正需要生成长篇文章的任务单独拆分或单独走一个队列。配置的方式很简单在 API 调用层加一个默认上限同时在服务端也做一层兜底限制避免某个上游调用方把参数传得过大。要注意的是限制输出长度不是“砍功能”而是给生成任务设定边界。通常对话助手的体验并不需要一次性生成 2000 个 token即使需要也可以用“持久化结果 查询进度”的方式来降低单次请求的等待压力。3.2 避免 batch 里长短请求混跑长度分桶或连续批处理很多推理框架默认会把同时到达的请求拼成一个 batch 计算这本意是提升吞吐。问题在于batch 里如果混入了一个特别长的请求这个 batch 的耗时就会被它拉长。短请求明明已经生成完毕也要等整轮计算结束。这就是同一个 batch 内的“慢邻居效应”。如果用的推理框架支持连续批处理或分代调度比如常见的 vLLM、TGI 等现代推理框架它们会按 token 完成情况动态调度让已经生成完的请求提前退出避免被长请求拖住。但如果你的方案是自己写的一个简单 API 封装套着 transformers 直接 generate那就需要更手动的处理方式。一个简单有效的做法是按请求长度分桶。把请求按预计输出长度分成几类比如“短输出”、中等输出、长输出”它们各自走不同的队列或不同的服务实例。即使在一个服务里也可以让调度逻辑优先处理短请求把长请求排到相对空闲的时段。如果你用的是成熟框架至少应该检查这几个参数max_num_batched_tokens、max_num_seqs、max_model_len。这些参数决定了 batch 的容量上限。把它们调得太小吞吐量会下降调得太大尾部延迟会失控。常见实践是先设一个保守值比如 batch 里最多同时处理 8 到 16 个请求观察并发上升时 p95 和 p99 的变化再逐步调整。3.3 保留好 KV cache而不是每次重建LLM 推理过程中模型需要缓存历史 token 的 Key 和 Value也就是 KV cache。这个缓存的设计直接决定了推理服务的延迟稳定性和显存占用。第一个常见问题是很多人没有复用前缀缓存。如果一个系统里有大量请求它们的 prompt 开头是一样的比如都带同一段系统提示词但每次请求都完整重新计算这些重复的 prompt那 prefill 阶段的耗时就会白白增加。现在不少推理框架支持前缀缓存或自动前缀缓存能把重复计算的部分直接复用。启用之后在系统提示词较长、知识库问答场景里TTFT 会明显下降。第二个常见问题是KV cache 显存预留不够。模型加载后剩余显存如果没有合理分配给 KV cache请求长度稍微变长就可能触发额外分配或重新排队。更稳的做法是启动一个推理服务之前先根据模型尺寸、显存大小、最大序列长度预留 KV cache 空间尽量避免运行中频繁申请显存。但这里有一个权衡max_model_len设得越大KV cache 能同时服务的请求数就越少。一些人为了让模型能处理超长上下文把 max_model_len 设到很大结果并发一高大批请求因为 KV cache 不够而排队尾延迟飙升。如果你实际业务里 95% 的请求只有几百到一两千 token那为了偶尔一次超长请求把整机配置拖垮并不划算。更稳妥的做法是把超长任务单独路由到另一个配置更高的实例而不是让它在公共服务里抢占资源。提醒不要让“偶尔一次的长上下文请求”拉高所有普通请求的尾部延迟。长任务和短任务分开比强行塞在同一个进程里更可控。3.4 预热和版本检查看起来小影响却很大LLM 服务第一次跑请求时通常比后续请求慢不少。模型权重需要加载进显存CUDA 图可能需要编译缓存是空的GPU 计算单元也需要一点时间进入稳定状态。如果在部署之后立刻上真实流量第一批请求的延迟会非常难看连 p50 都会很高。所以每次重启模型服务后应该先发几条预热请求把模型唤醒。常见做法是让服务启动时自动跑一个短输入、短输出的 dummy request等它正常返回后再对外提供服务。这一步虽然不能消除所有尾延迟但可以避免“冷启动导致的伪尾延迟”污染监控数据。版本检查也很关键。LLM 推理框架迭代速度很快同一个模型在不同框架版本里的显存管理、批处理策略、算子实现可能差别很大。有时候服务升级一个版本之后尾延迟突然变好或变差不一定是你配置错了而是框架行为变了。建议把推理框架、模型权重、推理脚本和 API 服务版本都记录清楚每次升级前先在一个小流量副本上做对比压测再切换正式流量。3.5 客户端不要傻等流式响应、快速失败、队列超时服务端优化做完了还有一半问题在客户端和接入层。很多 LLM API 服务默认是“等全部 token 生成完再一次性返回”这个模式对长输出任务极不友好。用户在交互式场景里会长时间看不到任何反馈主观感受非常差如果生成中途超时整次请求就白做了。改用流式响应能显著改善体验。即使总生成时间不变用户也能更快看到第一个 token并持续看到内容输出。从延迟指标上看TTFT 变得更有意义用户感知也从“等一个不确定的完成时间”变成“只要还在出字就知道没卡死”。第二个客户端策略是不要无限等待。每个调用方都应该设置合理的端到端超时。如果服务端已经明显过载继续排队等下去只会让链路更堵。很多框架支持请求排队但无限队列其实是最危险的设计所有调用都被接收然后一起变慢最后大家一起超时。更推荐的做法是当并发请求数超过服务能力上限时直接快速失败并返回“服务繁忙”让客户端走退避重试或者提示用户稍后再试。第三个策略是控制和削减重复请求。用户端的一次重试会再占一次推理资源。服务端可以通过幂等键或并发限制避免同一用户在同一时间发起多个相同请求。这一步不直接降低延迟但能减少请求放大效应从而间接改善尾延迟。4. 一个可以照着做的最小修复流程讲完五个修复方向接下来把它们串成一个可执行的最小流程。这个流程是我自己在一个内部服务里完整走过的思路不一定适合所有团队但它足够简单适合先跑通验证。4.1 先跑通单任务再做并发验证第一步不是直接改生产配置而是先在测试环境搭一个最小服务。加载你的模型写一个最简单的调用脚本发送一条短请求。单任务验证要检查几个点返回结果是否正常。TTFT 是否稳定。输出 token 速率是否正常。GPU 显存占用是否在预期范围。单任务正常之后再做并发验证。并发验证不需要一开始就上 50 并发可以从 4 并发开始每次增加 4 个并发每档跑几十条请求。记录每个并发档位的 p50、p95、p99 和错误率。这一步的核心目的是找到服务延迟开始加速变差的那个并发阈值。你不需要记录太多数据几组数据就足够说明问题。4.2 用三个指标验收而不是只看平均耗时优化前后建议固定看三个指标p50普通用户体验。如果这个数值很高说明大多数请求都已经不顺畅。p95 / p99尾延迟。如果这个数值和 p50 差距过大说明请求之间互相干扰严重。错误率和超时率包括服务端返回错误、客户端超时两个维度。一个相对合理的调优目标是在目标并发下p95 不要超过 p50 的 3 到 5 倍。如果差到 10 倍以上说明服务的调度和资源分配存在明显瓶颈。当然这个比例不是行业标准只是一个常见的经验观察实际要根据业务容忍度来定。4.3 什么情况是“简单修复”解决不了的这里必须写清楚边界。上面这组修复适合请求量中等、单卡或单机推理、业务主要是文本生成或对话的场景。如果你的服务已经出现以下情况简单修复就只能算打底需要更重的手段单张 GPU 已经长期跑满任何优化都无法降低请求计算量。需要服务的请求长度跨度极大从几百 token 到几万 token 都有而且都是真实业务需求。高峰期并发突增请求量远超单机吞吐上限。希望同时保证高吞吐和低 p99就必须引入多副本、自动扩容或多个推理微服务来分担。在这些场景里下一步通常要考虑的是增加推理节点做水平扩展按请求类型做路由分流启用投机解码或量化来降低单 token 延迟甚至在框架层面做更精细的调度策略。它们比“简单修复”复杂得多但前提是先把简单修复做好。4.4 长期维护每次升级都是新一轮排查LLM 基础设施有个特点它不是静态的。模型权重会更新推理框架会升级业务调用方式会变化。每一次变化都可能改变延迟分布。所以长期维护要做三件事环境升级后重新跑一遍单任务和并发压测对比历史 p95 和 p99不要因为“看起来能用”就直接上线。保存几个固定压测样本包括短输入短输出、中长输入中长输出、带共享 prefix 的请求。这样每次升级后都能快速回归。定期检查线上日志里的延迟分位数如果 p95 连续几天在爬升不要只看平均值要去分析是不是某类请求变多了。5. 理解这组小改动的底层逻辑控制变量而不是制造魔法最后把视角拉高一点。很多人期待“解决尾延迟”靠的是一个神奇参数但真实工程里这一类问题更接近“控制变量”的学问。你限制输出长度是为了缩短单请求占用你分桶调度是为了避免互相拖累你做 KV cache 配置是为了减少动态资源波动你设置快速失败是为了避免请求堆积。每一步都没有改变模型的推理能力但每一点都在减少系统里的不确定性。5.1 尾延迟的本质是“GPU 资源分配的不确定性”从系统角度看LLM 推理服务和普通 Web 服务有个重要差异。普通服务处理请求的时间尺度和资源占用相对固定而 LLM 请求的耗时会随输入长度和输出长度剧烈变化。资源分配一旦不公平短请求就会被长请求挤兑。尾延迟管理本质上就是把这种不确定性控制在一个可接受范围内。控制得越细延迟分布越集中用户体验越稳定。5.2 延迟优化既要看服务端也要看产品策略这里还要再强调一次产品层面的意识。有些“尾延迟”其实不是基础设施问题而是业务设计问题。比如产品非要让用户一次性生成一篇 3000 字的文章中间没有任何阶段性反馈那不管服务端怎么优化总有一批请求会落在尾部。改成流式输出、分阶段生成、先给大纲再展开细节延迟感受会完全不同。这类优化不需要 GPU不需要改推理框架只需要在设计请求时更谨慎地设置边界。尾延迟不是一个能完全消除的指标更不是单纯增加硬件就能解决的问题。如果你的系统里还存在“长任务和短任务混跑”“输出长度不设上限”“客户端无限等待”“请求失败后无限重试”这些现象那么再贵的 GPU 也只是把问题放大。先把这些偏工程和产品层的小事做好再谈更多资源和技术重器LLM 服务的体验就会稳定一个档次。