ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

不碰权重,四把刀砍掉77%首字延迟:TTFT优化实战

不碰权重,四把刀砍掉77%首字延迟:TTFT优化实战 做推理服务这一年多我最常被问的一句话是模型权重还是同一份凭什么你那边首字延迟能差出好几倍这里的首字延迟就是 TTFTTime To First Token用户按完回车之后到屏幕上吐出第一个字的耗时。它和总吞吐是两个维度的指标吞吐高不代表用户体验好很多优化把 QPS 拉上去了用户反而觉得转圈时间变长了。而 2026 年这一轮推理提速圈内讨论最多的恰恰是 TTFT、首字延迟、每秒输出 token 数这类过程指标而且是在一行权重都不改的前提下拿到的。我们有一组压测数据70B 级别的 MoE 模型典型的 RAG/Agent 场景10k 上下文的输入里前 8k 是固定系统提示与工具定义p95 首字延迟从 1850ms 降到 425ms降幅大约 77%。整个过程没有碰过权重文件没有重新训练没有微调连浮点表示都没动。这篇文章就把为什么提速都发生在权重之外这件事拆开讲哪些优化真正能吃下首字延迟哪些是权重的禁区以及怎么在自己的环境里复现类似的结果。适合正在搭推理服务、或者在做模型部署选型的人读读完你应该能自己判断你们团队的首字慢到底该上哪把刀。1. 先把首字延迟拆开它到底由什么构成1.1 TTFT 不是一锤子买卖而是四段路程的总和TTFT 你可以粗暴地理解为四段时间的累加排队时间请求到了网关到调度器真正接手它之前在队列里等待的时间。服务越忙这段越长。预填充时间模型把所有输入 token 从头到尾算一遍拿到第一个输出 token 的 logits 的时间。输入越长、模型越大这段越久。调度与显存分配包括要不要抢占别的请求、KV cache 够不够、要不要做前缀匹配等几十毫秒级别。网络与传输开销客户端到服务端的 RTT加上 tokenizer 解出第一个 token 的时间通常能控制在几毫秒到二十毫秒。很多团队在优化的时候只盯着第二段——预填充但真实生产环境里第一段排队时间和第三段调度时间往往才是 p95 毛刺的来源。我们的压测里基线配置下 p95 首字延迟 1850ms其中预填充占了大约 70%排队占 20%其余是调度与网络。所以后面几把刀有的砍预填充有的砍排队有的两边一起砍。为什么预填充这么重因为自回归模型要产出第一个输出 token必须让输入的所有 token 都过一次前向。10k 个输入 token对 70B 级别的模型来说前向计算量轻松超过千万亿次浮点运算10^15 级别。我在 H100 上实测FP16 精度下这种规模的预填充单请求在理想状态下也要一秒多到两秒才能跑完——这还是没有其他请求抢占的状态。所以你会发现首字延迟的物理下限其实很高能优化的空间全在怎么不让这份计算量重复发生以及怎么让它不被别的请求堵住。1.2 为什么权重成了禁区改不动也改不起2026 年大家讨论模型的方式有个明显变化权重几乎变成了下载即固定的商品。DINOv3 的权重、SAM3 的权重、YOLOv8 的预训练权重社区里最火的搜索词都是xx 权重下载拿到手之后第一件事是冻结第二件事是部署。这背后是个很现实的成本问题。改权重意味着什么要么重新训练要么微调要么量化。重新训练的成本不用多说以 70B 级别模型为例一次像样的继续训练是百万美元级的算力账单。微调看起来便宜但你要面对评估回归改了权重之后跑基准会有波动某些能力可能涨了另一些可能悄悄掉了团队得花大量人力去复测。量化更微妙很多人以为量化是无损提速但在长上下文、低比特场景下输出质量的变化很难用一两个指标说清楚。所以行业里逐渐形成了一种默契权重是结果不是手段。同一个模型权重别人能跑到 1000 tokens/s你只能跑到 400差距不在模型本身而在你围绕它搭的那套推理系统。这也是为什么 2026 年的推理提速几乎全部发生在权重之外——发生在调度器、缓存、显存策略和部署架构里。下面这些手段没有一行是改权重的但它们对首字延迟的杀伤力比换一个更快的模型权重还要大。2. 不碰权重减掉 77% 首字延迟的四把刀2.1 前缀缓存把已经算过的直接复用先讲贡献最大的一把刀前缀缓存Prefix Caching。它的原理特别朴素。一个 RAG 请求的输入往往由系统提示 工具定义 检索回来的文档 用户问题四段拼接而成。前三段对于同一系统里的绝大部分请求来说几乎一模一样。基线版本里每个请求都要把这 8k token 从头到尾重新算一遍预填充——这就是实打实的浪费。前缀缓存做的事情是把已经算过的 token 对应的 KV cache 按块存下来下一个请求如果输入的开头部分能匹配上直接复用缓存块只用计算没匹配上的那部分。vLLM 里这个能力叫 automatic prefix caching核心思路是对 KV cache 块做哈希用 token 序列作为 key命中就直接挂载。SGLang 做得更激进用 RadixAttention 把共享前缀组织成一棵前缀树支持任意层级的前缀复用不只是从第一个 token 开始匹配。实测下来在我们 10k 上下文的场景里系统提示占 8k命中之后实际需要新计算的只有 2k预填充耗时直接缩到原来的四分之一左右。但这把刀有个前提你的输入结构得稳定。如果每次请求都往系统提示里塞时间戳、随机 ID、会话标识这类动态内容前缀就永远匹配不上缓存命中率会掉得很难看。我们踩过这个坑后面在常见问题里细说。另外要记住前缀缓存不是免费的它会占显存缓存块的淘汰策略常见的 LRU也会影响命中率所以缓存开多大、淘汰多激进需要照着真实请求分布去调不能拍脑袋。2.2 分块预填充别让一个长请求堵死整条街第二个手段是分块预填充Chunked Prefill。它的目标不是砍单个请求的预填充算量而是砍排队时间。想象一条单车道一个 32k token 的长请求进站后整条路的资源都被它占住后面的请求只能干等。短请求体感上就是转圈半天一个字不出来。分块预填充把一个长预填充切成若干块比如每块 4k token算完一块就暂停让调度器把 GPU 让给其他请求的 decode 步骤再回来算下一块。这样长请求不再垄断资源短请求的平均等待时间大幅下降p95 首字延迟自然好看。有一点必须说清楚分块预填充并不会让那个 32k 长请求自己的首字更快。因为第一个输出 token 必须等全部输入算完才能生成切块只是改变了计算节奏没有减少计算总量。它的价值在混合负载下——大量短请求和少量长请求共存时长请求的队头阻塞被消解了。我们在压测里把它和前缀缓存配合使用后p99 的毛刺明显变平那种前面一个长请求导致全线超时的问题基本消失。注意如果你只关心单个长请求的绝对首字延迟分块预填充给不了多少收益它优化的是整条服务链路的稳定性和短请求的体感。定位要分清。配置上要注意块大小的选择。块太大和没切差不多块太小调度开销和 kernel 启动开销会反噬。我一般从 4096 起步根据短请求的 p50 首字延迟来微调。另外vLLM 较新版本里分块预填充的开关和 max-num-batched-tokens 参数耦合改一个要留意另一个具体参数名以你部署的版本文档为准。2.3 预填充与解码分离给两种计算各配一条流水线第三把刀是预填充/解码分离PD Disaggregation这也是 2025 年中后期开始普及、2026 年已经算标配思路的架构。为什么值得拆因为预填充和解码的硬件瓶颈完全不同。预填充是典型的计算密集型大批量矩阵乘GPU 的 FLOPS 是瓶颈batch 越大越划算。解码是典型的内存带宽密集型batch 再大每步也就读那几个 KV cache 块算力用不满显存带宽才是瓶颈。把两者放在同一批 GPU 上等于让计算型选手和带宽型选手抢同一块场地怎么排都会有人被拖累。分离的做法是把 GPU 池子分成两组一组专门吃预填充请求算出 KV cache 之后通过高速通道内存共享、RDMA 或者共享存储把 KV 传给另一组继续解码。这样预填充组可以按首字延迟 SLO 弹性扩缩容解码组可以专注把 batch 做大、把吞吐做高。对首字延迟的贡献很直接原来一个预填充要和其他请求抢卡现在它有一整个池子等着服务它。不过我得泼盆冷水PD 分离是这里面架构复杂度最高、运维成本最重的一把刀。你需要处理 KV cache 的跨机传输、失败重试、两组实例之间的负载均衡团队基础设施不够成熟的时候贸然上可能优化没吃到先把稳定性搭进去。我的建议是前三把刀都上完、测完如果 p95 首字延迟还是压不下来再考虑拆。我们给内部团队的建议顺序永远是先前缀缓存再分块预填充最后才动架构。2.4 投机解码与调度优化把等待变成并行第四把刀比较杂但每一下都能抠出几十毫秒。投机解码Speculative Decoding是其中一个代表。它用一个轻量草稿模型一次预测出 K 个 token再用大模型一次前向去验证。验证正确就一次吐出 K 个 token错误的部分丢弃回退。它主要改善的是首字之后的输出速度对 TTFT 本体帮助不大但如果你把用户体验指标定义成前几个字的到达时间而不是第一个字的到达时间它的价值就出来了。在 RAG 场景还有一种更轻的玩法叫 prompt-lookup decoding直接用输入文本里的片段当草稿不额外加载任何模型对我们这种前缀雷同的场景特别合适。调度优化则是另一层。很多人忽略了一个事实HTTP 长连接和连接复用能直接砍掉 TTFT 里的网络段。我们用 keep-alive 之后每请求省了 5-10ms 的握手时间看着不起眼但当你目标是把 p50 压到 200ms 以内时这点量级就很关键。再比如调度器对请求的公平性策略、等待队列的优先级设计、显存分配时的预占与回收这些都是几毫秒到几十毫秒级别的抠但叠加起来就是 p95 从偶尔飘红到稳定达标的区别。3. 一组可复现的实操把 77% 跑出来3.1 环境与基准先把毛病量化复现的前提是先把基线测准。我们用的环境是两台 H10080GB模型是 70B 级别的 MoE推理框架用 vLLM 和 SGLang 各跑了一轮压在同一个模型权重上对比不同框架的优化效果。这里强调一句对照组一定要保证模型、输入、请求分布、并发数完全一致只改你要测的那一个变量否则你根本说不清 77% 到底是谁贡献的。基准负载的设计比很多人想象的重要。我们在压测里用的是一组固定长度的请求输入 10k token8k 固定系统提示 2k 检索文档输出要求 256 token并发 32跑 200 个请求统计 p50、p95、p99 的首字延迟。为什么不随机长度因为随机长度会让方差变大一次测试里长请求和短请求混在一起p95 的数字很难稳定复现调参的时候你根本看不出改动是生效了还是纯粹运气。写一个最小可用的 TTFT 测量脚本并不复杂。关键点是用流式接口读到第一个有内容的 chunk 就掐表import time import numpy as np from openai import OpenAI client OpenAI(base_urlhttp://localhost:8000/v1, api_keyEMPTY) prompt 系统提示与检索文档的拼接文本…… * 2000 # 约 10k token def measure_ttft(n50): latencies [] for _ in range(n): t0 time.perf_counter() stream client.chat.completions.create( modellocal-model, messages[{role: user, content: prompt}], max_tokens1, # 只要第一个输出 token streamTrue, ) for chunk in stream: delta chunk.choices[0].delta.content if delta: latencies.append(time.perf_counter() - t0) break return latencies lat measure_ttft() print(p50: %.0f ms % np.percentile(lat, 50)) print(p95: %.0f ms % np.percentile(lat, 95)) print(p99: %.0f ms % np.percentile(lat, 99))注意压测前先跑两轮预热请求。冷启动状态非常误导人CUDA kernel 还没编译好、权重还在显存里热加载、图捕获没跑完这时候测出来的 p99 可能是热态的三倍。预热之后再测拿到的才是真实服务的常态表现。3.2 关键配置项与参数选择基线数据拿到之后我按先缓存、再分块、后调参的顺序逐步开刀。以 vLLM 为例开前缀缓存的启动命令大概是这样的具体版本参数名可能略有差异以官方文档为准vllm serve local-model \ --gpu-memory-utilization 0.90 \ --max-model-len 32768 \ --enable-prefix-caching \ --max-num-seqs 64 \ --max-num-batched-tokens 8192这里有个细节--max-num-batched-tokens 设成 8192等于变相启用了 chunked prefill因为单次前向最多处理 8k token超过的部分会被切块。想让分块更激进就调小这个值想让长请求更完整地跑就调大它。我们最终定在 8192短请求的 p50 最稳。SGLang 那边的对应配置是 --chunked-prefill-sizeRadixAttention 默认开启。它俩的取舍我在实际使用中的感受是vLLM 生态更成熟周边监控和工具链全SGLang 的 RadixAttention 在前缀复用的细腻度上更高尤其是多级前缀共享的场景。如果团队没有历史包袱我会建议两个都试用同一份压测脚本对比命中率和 TTFT 分布哪个数字好就留哪个。显存利用率也值得单独提。我们把 gpu-memory-utilization 从默认的 0.9 调到 0.92 后KV cache 能多放不少块前缀命中率涨了几个点。但别贪心留出的余量要覆盖 CUDA context、激活值和偶尔的峰值占用压到 0.95 以上就很容易 OOM尤其是长上下文并发一起来的时候。显存这事的本质是给 KV cache 让位因为缓存命中率直接决定预填充要重算多少这是整个提速逻辑里最核心的杠杆。3.3 测试结果与解读77% 是怎么算出来的下面是我们一组真实压测的对比模型、输入、并发都完全一致唯一变化的是服务端配置和架构调整阶段配置变化p95 TTFT降幅基线无前缀缓存无分块预填充1850ms- 前缀缓存开启 prefix caching命中 8k/10k720ms61% 分块预填充开启 chunked prefillbatched tokens 8192610ms67% 调度与网络调整keep-alive、优先级策略、显存调优540ms71% PD 分离两阶段预填充组 2 卡 解码组 2 卡425ms77%看这张表就知道77% 不是某一把刀的功劳是四把刀叠出来的。前缀缓存贡献最大直接砍掉六成后面每一项都是几十毫秒的累积。也不难看出收益越往后越边缘最后 PD 分离的边际收益只剩 100ms 出头但它把 p99 的稳定性彻底解决了。所以如果你的场景是追求平均快而不是稳定快前两把刀可能就够了如果 SLO 卡得死才需要把后面两把也上齐。这里必须提醒77% 是我们这个场景的数字。你的请求分布、模型大小、显存容量不同结果可能差出一倍。前缀缓存的高收益依赖共享前缀占比高如果你们的请求全是独一无二的长文本前缀缓存几乎吃不到肉可能只剩分块预填充在起作用。所以别把别人的数字当自己的目标照着同样的方法测一遍拿到自己的基线才算数。4. 常见问题与排查实录4.1 缓存命中率上不去前缀缓存形同虚设这是我们踩过最深的坑。上线前缀缓存后监控面板上的命中率只有 12%和预期的 80% 差了十万八千里。查到最后发现业务方在系统提示里拼了一个带时间戳的字段每次请求字符串都不一样哈希永远对不上。解决方法是把动态内容挪到输入的末尾用户问题那一段让系统提示保持纯静态实在需要在头部放动态信息就得自己实现去动态化把可变字段单独切出来。排查缓存类问题建议先做一件事把请求的完整输入 dump 出来肉眼对比几条。开头一模一样在字符串层面成立在 token 层面不一定成立因为 tokenizer 的分词边界可能因为拼接方式变化而不同。有时候你以为前缀没变实际只是多了一个空格哈希就崩了。4.2 开了投机解码首字延迟不降反升投机解码在小 batch、低并发时收益最大因为草稿模型的前向开销几乎可以忽略而大模型少跑几步的收益很实在。但并发一上来batch 变大草稿模型的前向也开始吃显存、吃算力而大模型每步验证的算力并没有减少太多收益逐渐被成本吃掉。我们在 batch 32 以下能看到明显加速batch 128 以上就基本持平甚至倒退。定位这类问题要会看阶段拆分指标。如果 TTFT 没变但 TPOT 变差了多半是投机解码的草稿模型太胖如果首字延迟变差先看是不是 KV cache 被草稿模型的显存挤占了。另外prompt-lookup 这类无草稿模型的方案在 RAG 场景更省心不需要额外模型文件值得优先试。4.3 长输入场景的首字延迟还是压不下来前缀缓存和分块预填充把能砍的都砍了但一个 30k token 的独有长文本进来预填充该算 30k 还是要算 30k首字延迟的物理下限摆在那里。这种情况下能做的其实是想办法别让用户在本地等。一个是把长输入拆到多个后端并行处理、只把关键结果拼回来另一个是对交互层做流式占位——先让用户看到已收到正在理解再用后台预取把真正的内容填充进去。这是产品层对物理限制的妥协但用户感知到的首字时间会明显变短。做推理服务的团队容易只盯技术指标忘了指标最终要翻译成用户体感长场景下这两者不一定等价。4.4 压测数据毛刺大p99 忽高忽低p99 毛刺最常见的原因是显存碎片和 KV cache 不足导致的强制抢占。当并发请求的 KV cache 需求超过可用显存调度器会踢掉一部分请求被踢的请求重新排队、重新预填充首字延迟直接翻倍。排查手段是看服务端日志里的 preemption 计数如果持续增长说明 KV cache 池子不够要么调低 max-num-seqs要么提高显存利用率要么上 PD 分离把预填充流量隔开。还有一个隐蔽的毛刺来源是 CPU 端的 tokenizer 和请求解析。我们用火焰图看过一次诡异的 p99 飙升最后发现是某一版框架在长文本输入下做了一次 O(n^2) 的字符操作CPU 成了瓶颈。这类问题不会总出现但一旦出现就非常难查建议压测时同时盯 CPU、内存、GPU 利用率四个指标任何一个接近 100% 都要警惕。为了方便排查我把上面几个问题整理成一张速查表症状常见原因排查方法解决方向命中率极低前缀含动态内容、拼接不一致dump 请求对比字符串和 token动态内容移到末尾、规范化拼接投机解码负收益batch 太大、草稿模型过重分 batch 梯度测试减小 batch、换轻量草稿或 prompt-lookup长输入 TTFT 高预填充物理下限拆分阶段指标后端并行、交互层流式占位p99 飘忽KV cache 不足触发抢占查 preemption 计数调显存利用率、降并发、上 PD 分离5. 权重之外几次实操换来的体会5.1 权重已经是商品系统才是护城河做了一整轮0 行权重改动的提速之后我最大的感受是权重时代大家站在同一条起跑线上真正的差距全在权重之外。下载一份 DINOv3 或 SAM3 的权重很容易难的是搞清楚你的请求长什么样、瓶颈卡在哪一段、该上哪把刀。我们内部现在每接到一个推理太慢的反馈第一反应永远是先看监控指标拆分——TTFT 拆成排队、预填充、调度、网络四段哪段红了打哪段而不是急着换模型或者改精度。5.2 先把杀器和补丁分开任何优化上线前先把杀器和补丁分清楚。前缀缓存是杀器收益大、见效快keep-alive、调度优先级这种是补丁每样只抠几十毫秒。先上杀器再做精细化最后用压测数据决定要不要为了最后那 100ms 去折腾 PD 分离——毕竟架构复杂度上去了后面每一行日志、每一个告警都要你亲自还。5.3 快是手段稳定可复现的快才是目的最后再分享一个一直管用的习惯每次压测完不仅记录 p50/p95/p99还要把当时的配置、请求分布、显存占用一起归档。因为推理系统的性能对配置极其敏感换个 batch 大小、改个缓存淘汰策略结果可能天翻地覆。没有完整记录你复现不了自己的成功实验更别说向别人解释 77% 到底怎么来的。2026 年的推理提速还会继续往外走但做工程技术的人应该明白快是手段稳定可复现的快才是目的。
返回列表