news 2026/10/1 13:29:28

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
不碰权重,四把刀砍掉77%首字延迟:TTFT优化实战

做推理服务这一年多,我最常被问的一句话是:"模型权重还是同一份,凭什么你那边首字延迟能差出好几倍?"

这里的"首字延迟"就是 TTFT(Time 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 环境与基准:先把"毛病"量化

复现的前提是先把基线测准。我们用的环境是两台 H100(80GB),模型是 70B 级别的 MoE,推理框架用 vLLM 和 SGLang 各跑了一轮,压在同一个模型权重上,对比不同框架的优化效果。这里强调一句:对照组一定要保证"模型、输入、请求分布、并发数"完全一致,只改你要测的那一个变量,否则你根本说不清 77% 到底是谁贡献的。

基准负载的设计比很多人想象的重要。我们在压测里用的是一组固定长度的请求:输入 10k token(8k 固定系统提示 + 2k 检索文档),输出要求 256 token,并发 32,跑 200 个请求,统计 p50、p95、p99 的首字延迟。为什么不随机长度?因为随机长度会让方差变大,一次测试里长请求和短请求混在一起,p95 的数字很难稳定复现,调参的时候你根本看不出改动是生效了还是纯粹运气。

写一个最小可用的 TTFT 测量脚本并不复杂。关键点是用流式接口,读到第一个有内容的 chunk 就掐表:

import time import numpy as np from openai import OpenAI client = OpenAI(base_url="http://localhost:8000/v1", api_key="EMPTY") prompt = "系统提示与检索文档的拼接文本……" * 2000 # 约 10k token def measure_ttft(n=50): latencies = [] for _ in range(n): t0 = time.perf_counter() stream = client.chat.completions.create( model="local-model", messages=[{"role": "user", "content": prompt}], max_tokens=1, # 只要第一个输出 token stream=True, ) 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-size,RadixAttention 默认开启。它俩的取舍我在实际使用中的感受是: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 prefill,batched 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 年的推理提速还会继续往外走,但做工程技术的人应该明白:快是手段,稳定可复现的快才是目的。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/1 13:28:58

大模型Agent开发实战:真正要学的五个核心能力

做了近两年的Agent开发,真正要学的就是这五件事。我是大模型开发工程师,这两年密集做了智能客服、浏览器自动化、代码生成类的Agent项目。很多人问我入行学什么,市面上流行的答案是LangChain、Prompt、RAG。但回头看,真正绊住我的…

作者头像 李华
网站建设 2026/10/1 13:28:54

Git下载安装与配置全教程:从零到首次提交的完整指南

人人都经历过那个阶段:项目文件夹里塞满了项目最终版.zip、项目最终版2.zip、项目最终版_再也不改.zip。我是从这种"文件备份大法"里逃出来的人,后来真正让我把版本管理这件事想明白的工具,就是 Git。这篇博文不讲虚的,…

作者头像 李华
网站建设 2026/10/1 13:27:47

AI工业控制系统落地实战:从数据采集到边缘推理的完整架构

1. 从零理解AI工业控制系统的真实边界1.1 这套系统到底在解决什么问题工业控制系统这个词听起来很重,但拆开看其实就三件事:采集现场数据、按规则做决策、把决策下发到执行机构。传统的PLC和SCADA已经把这三件事做了几十年,稳定可靠&#xff…

作者头像 李华
网站建设 2026/10/1 13:27:18

OnlyOffice HTTPS配置实战:解决混合内容拦截与白屏问题

你在用 OnlyOffice 自建在线文档服务吗?如果只在局域网里用 IP 访问,可能一直没被这个问题找上门。上周同事找我,说他把 OnlyOffice 文档服务器从内网搬到外网后,在线编辑器一直白屏,浏览器地址栏域名已经带上了上锁图…

作者头像 李华
网站建设 2026/10/1 13:27:04

C#火锅点菜系统实战:数据库设计、事务处理与厨打队列

简介:这是基于C#开发的火锅点菜系统完整项目,面向餐饮管理方向的学习者、高校课程设计以及需要参考WinForms桌面应用架构的开发者。系统覆盖菜品展示、点菜购物车、订单生成、支付结算与小票打印等完整业务链路,源码中体现了MVC分层、事件驱动…

作者头像 李华
网站建设 2026/10/1 13:26:50

gpt-image-1蒙版与Alpha通道实战:从局部重绘到生产落地

如果你已经在项目里接入了 OpenAI 的图像接口,大概率绕不开 gpt-image-1 这个模型。和早期 DALLE 那套流程相比,它最大的变化是把“生成、编辑、局部重绘”全部收拢到同一个接口里:你不再需要先抠图、再合成、再二次生成,只需要…

作者头像 李华