2.1 一个请求的一生:请求链路拆解


文档摘要

2.1 一个请求的一生:从 HTTP 到首 Token 的链路拆解 部署高并发服务,第一个要回答的问题是"慢在哪里"。很多团队一上来就调 、加卡、加显存,结果发现延迟纹丝不动——因为他们根本没看请求的时间都花在哪一环。这一节我们沿着一个请求的真实生命周期,把每一段延迟拆开,给你一张"延迟地图"。读完你应该能指着监控图说出"我们服务的首 token 慢,主要慢在调度等待和 prefill",而不是只会说"服务器好慢"。 2.1.

2.1 一个请求的一生:从 HTTP 到首 Token 的链路拆解

部署高并发服务,第一个要回答的问题是"慢在哪里"。很多团队一上来就调 tensor-parallel-size、加卡、加显存,结果发现延迟纹丝不动——因为他们根本没看请求的时间都花在哪一环。这一节我们沿着一个请求的真实生命周期,把每一段延迟拆开,给你一张"延迟地图"。读完你应该能指着监控图说出"我们服务的首 token 慢,主要慢在调度等待和 prefill",而不是只会说"服务器好慢"。

2.1.1 七个阶段,每一段都能被量化

当一个客户端向 vLLM 的 /v1/completions/v1/chat/completions 发来一个请求,到它收到第一个 token(TTFT,Time To First Token),中间大致经历七个阶段:

  1. 网络与入口(Network + API Server):请求经负载均衡到达 API Server。这一段延迟取决于客户端到服务器的 RTT、以及 API Server 事件循环的拥塞程度。高并发下,如果入口进程被同步阻塞(比如用错部署方式),这里会积压。
  2. 分词(Tokenization):API Server 把原始文本交给 Tokenizer Manager,切成 token id。这一步在 vLLM 里是异步、可并行的,通常很快,但在处理超长 prompt(几十 K token)时会占一定时间,因为分词本身要跑一遍 tokenizer 模型的前处理。
  3. 入队与调度等待(Queue + Schedule Wait):请求进入调度器的等待队列,直到某一轮迭代被选中进入运行批次。这一段就是"排队时间",高并发、批预算不足时它是大头,也是最容易被忽视的隐性成本。
  4. KV 块分配(KV Cache Block Allocation):调度器为这个请求分配显存里的 KV Cache 页块。PagedAttention 的分页机制在这里发挥作用,分配是 O(1) 的块操作,正常极快,但如果空闲块池紧张触发了抢占清理,这一段会被拉长。
  5. 前缀/模型前向(Forward / Prefill):GPU 真正干活,把整个 prompt 走一遍 transformer,生成第一个 token 的 logits。这是 TTFT 里最贵的一段,且随 prompt 长度近似线性增长,也是 MoE 模型访存瓶颈的主要发生地。
  6. 采样(Sampling):从 logits 里按温度、top-p 等策略选出 token。极快,但采样策略复杂(如重复惩罚、logit 偏置)时会略微增加,通常可忽略。
  7. 解码与回传(Detokenize + Stream back):把 token id 还原成文字,经流式接口推回客户端。这一段受网络和服务端事件循环影响。
```mermaid flowchart TD R[请求到达] --> N[网络+入口] N --> T[分词] T --> Q[调度等待队列] Q --> K[KV 块分配] K --> F[Prefill 前向] F --> S[采样] S --> D[解码回传] D --> O[首 Token 到达] ```

2.1.2 用一张图看懂"延迟都去哪了"

假设你部署 DeepSeek V4 处理一段 2K token 的 prompt,在 4 卡 A100 上测到 TTFT 约 800ms。粗略分解可能是:网络+入口 20ms、分词 30ms、调度等待 250ms、KV 分配 5ms、Prefill 450ms、采样 10ms、解码回传 35ms。注意——调度等待占了近三分之一。这意味着在不碰 GPU 的情况下,把批预算调大、把等待队列理顺,就能直接砍掉这 250ms。

这正是"看链路"的价值:很多人以为 TTFT 全是 GPU 算的,于是盲目加卡;但实测发现调度等待才是瓶颈,加卡毫无用处,反而应该调 max_num_seqsmax_num_batched_tokens。我见过一个真实案例:某团队把 8 卡扩到 16 卡,TTFT 一点没降,后来发现是 max_num_seqs 默认 256 但他们的流量峰值远超此值,等待队列常年几百个请求。把预算调到 512 并配合 max_num_batched_tokens 后,TTFT 直接砍半——这钱本来可以省下来。

```mermaid pie title TTFT 延迟构成示例(约800ms) "Prefill 前向" : 450 "调度等待" : 250 "网络+入口" : 20 "分词" : 30 "解码回传" : 35 "采样" : 10 "KV 分配" : 5 ```

2.1.3 为什么 MoE 模型让"Prefill"更特殊

DeepSeek V4 是 MoE(混合专家)架构:每一层不是所有参数都激活,而是路由出少数几个专家。这带来两个对链路的影响,部署者必须心里有数:

  • 计算密度低但访存高:MoE 每层只激活部分专家,FLOPs 不算夸张,但权重搬运(从显存读取被选中的专家权重)非常吃显存带宽。所以 Prefill 阶段的瓶颈常常不是算力,而是 HBM 带宽。
  • 张量并行切分方式不同:专家维度通常沿专家轴切,而注意力和共享专家沿隐藏维切。这影响你 tensor-parallel-size 的选择和单机多卡 vs 多机多卡的决策,也影响 prefill 时卡的通信量。
```mermaid flowchart TD P[Prefill 输入 prompt] --> R{路由到哪些专家?} R --> E1[专家A GEMM] R --> E2[专家B GEMM] R --> E3[共享专家] E1 --> BW[权重从HBM搬运 吃带宽] E2 --> BW E3 --> BW BW --> OUT[输出 logits] ```

一个实用判断:如果你的 prefill 慢且 GPU 的"计算利用率"看起来不高、但"显存带宽利用率"很满,那基本就是 MoE 的访存瓶颈,而不是你卡不够——此时更应该优化批大小让每次搬运更"值",或考虑用更宽的卡间互联,而不是无脑加卡。这个判断省下了我们团队当初一大笔无效的扩容预算。

2.1.4 测量,而不是猜:如何拿到真实分段

不要凭感觉定位瓶颈。vLLM 提供了多种可观测手段,部署当天就该把这几条接上:

  • 请求级指标:记录每个请求的 queue_time(调度等待)、prefill_timeinference_time,这是最直接的"延迟地图"数据源。
  • 通过日志或 Prometheus 指标观察 vllm:time_to_first_tokenvllm:num_requests_waiting 等,前者看延迟分布,后者看队列是否成为瓶颈。
  • 用 benchmark 脚本在固定 prompt 长度下压测,画出"prompt 长度 vs TTFT"曲线,斜率就反映了 prefill 成本,而截距里藏着固定的调度/网络开销。
```mermaid xychart-beta title "Prompt 长度 vs TTFT 关系(示意)" x-axis [512, 1024, 2048, 4096, 8192] y-axis "TTFT(ms)" 200 --> 2000 line [220, 350, 800, 1500, 1900] ```

这张示意图表达的是:TTFT 随 prompt 长度近似线性上升。当你发现长 prompt 把整批拖慢时,就应该考虑"分块 prefill"或限制单请求最大 prompt,把长任务和普通任务分流——这是第三章进阶内容,这里先埋下伏笔。重要的是,这条曲线要你自己测出来,因为不同模型、不同硬件的斜率天差地别,别直接套用别人的数字。

2.1.5 给工程实践的三条铁律

总结本节的落地建议,也是我反复踩坑后的结论:

  1. 先测分段,再动参数。上线前用 benchmark 拿到 queue_time / prefill_time 的真实占比,避免"加卡玄学"。我的一条经验法则:任何延迟优化前,先花半小时把延迟曲线画出来,往往答案自己就显现了。
  2. TTFT 和 TPOT 分开看。TTFT(首 token)主要被 prefill 和调度等待支配;TPOT(每 token 间隔)主要被 decode 阶段和批大小支配。两者优化方向不同,不要混为一谈,否则你会用错药方。
  3. MoE 重点看带宽不看算力。DeepSeek V4 的瓶颈常在 HBM 带宽与专家路由,调参围绕"让每次显存搬运更高效"展开,而非单纯堆 tensor parallel。用 nvidia-smi dmon 或 DCGM 看的是带宽利用率而不是 SM 利用率,这两个指标会骗你。

2.1.6 两个真实踩坑案例:延迟不是你想的那样

光讲原理不够,讲两个我们团队真金白银换来的教训,帮你少走弯路。

案例一:system prompt 每次都变,前缀缓存全失效。 我们最初在前端拼 system 时,顺手加了一个时间戳和请求 id,导致每个请求的 system 前缀都不一样。结果前缀缓存完全没命中,每个请求都要重做完整 prefill,TTFT 比预期高一倍。改成固定 system 后,缓存命中率升到 90%+,prefill 成本骤降。教训:任何你以为"无害"的差异(空格、顺序、随机串)都会让共享块失效,想用前缀缓存就必须让前缀字节级一致。

案例二:客户端不开流式,误判 TTFT。 有个同事抱怨首 token 要 3 秒,但我们服务端日志显示 prefill 只用了 600ms。排查发现他们的测试脚本用的是非流式调用,要等服务端把整段生成完、一次性回传才计时——3 秒里大部分是 decode 和网络回传,根本不是首 token 慢。教训:测 TTFT 必须用流式接口,且客户端要能逐 token 计时,否则你会把 decode 延迟算到 prefill 头上,调错方向。

2.1.7 上线前的延迟自检清单

把下面几条做成上线必查项,能挡掉 80% 的"莫名变慢"工单:

  • 是否接了请求级 queue_time / prefill_time 指标?没有就先接,否则你永远在猜。
  • 压测是否画出 "prompt 长度 vs TTFT" 曲线?斜率和截距分别告诉你 prefill 成本和固定开销。
  • 是否区分了 TTFT 与 TPOT?同一个"慢",两者药方完全不同。
  • 长 prompt 占比高时,是否单独限长或开了前缀缓存?否则长任务会饿死短请求。
  • MoE 模型是否看了 HBM 带宽利用率而非只看 SM 利用率?带宽打满才是真瓶颈信号。

2.1.8 根据延迟地图决定优化顺序:先易后难

拿到 2.1.2 那种分段数据后,不要眉毛胡子一把抓。遵循"先改配置、再改架构、最后加硬件"的顺序,性价比最高:

  • 第一步看调度等待:如果占比高,调 max_num_seqs / max_num_batched_tokens,零成本、立竿见影。
  • 第二步看 prefill:若是 MoE 访存瓶颈,先试着调大批次摊薄搬运,再考虑限长或分块 prefill,最后才考虑换更宽互联的卡。
  • 第三步看网络/回传:如果是客户端到服务端 RTT 大,考虑就近部署、开流式、用更轻的协议,而不是动模型。
  • 加卡永远是最后一步:只有当上面都优化完、GPU 利用率确实饱和且业务还需要更高吞吐时,才扩容。

这个顺序能帮你把每一分预算花在刀刃上。我见过太多团队一上来就加卡,结果延迟纹丝不动——因为他们的问题根本不在算力,而在调度等待那 250ms。

2.1.9 本章链路视角的收束

把 2.1 收个尾:链路拆解的本质,是让你从"服务器好慢"这种模糊抱怨,进化到"慢在调度等待 250ms、prefill 450ms"这种可操作的诊断。前者让人无从下手,后者直接指向参数。下一节 2.2 我们看显存,2.3 看调度,三者合起来你就有了一张完整的"高并发归因地图"。

2.1.10 链路视角下的常见误解澄清

最后澄清几个新手最常有的误解,免得你带着错误心智模型进下一节:

  • 误解一:"TTFT 全是 GPU 算的,加卡就能降。" 错。2.1.2 显示调度等待常占三分之一,加卡对它无效,调批预算才有效。
  • 误解二:"token 数一样,延迟就该一样。" 错。同样的输出长度,prompt 越长 prefill 越贵,TTFT 差异巨大;而且排队位置不同,等待时间也不同。
  • 误解三:"GPU 利用率 100% 就是健康。" 错。高利用率可能只是被长请求占着做无用等待,真正该看的是完成请求的吞吐和尾延迟。
  • 误解四:"流式只是体验优化,不影响指标。" 错。是否流式直接决定你怎么测量 TTFT,测错方式会把 decode 延迟算到首 token 头上(见案例二)。

把这四条贴在工位上,能少踩一半的链路坑。

2.1.11 流式为何改变"体感延迟"

最后补一个容易被忽略的点:即使 TTFT 完全相同,开不开流式,用户体感也天差地别。非流式时,用户面对的是一段空白直到整段返回;流式时,第一个 token 到达就开始有内容滚动,焦虑感大幅下降。所以优化 TTFT 之外,务必默认开启流式输出——它不改服务器指标,却改了用户的主观"快不快"。把 TTFT、TPOT、流式三件事一起交付,才是完整的低延迟体验,而不是只盯着监控里的某个数字。要补充的是,流式对服务端的成本几乎为零——它只是把一次性返回改成边生成边回传,不会增加 GPU 开销,所以你没有任何理由不开它。
读到这里,你应该能指着延迟图说出::"我们服务的首 token 慢,主要慢在调度等待 250ms 和 prefill 450ms,前者调批预算,后者受 MoE 访存限制。"——这就是"读者带走的一句话"。如果你现在还只会说"服务器慢",请重读 2.1.2 的小结,直到你能拆出每一段。

下一节 2.2,我们钻进显存,看 PagedAttention 如何用分页把 KV Cache 管理得既省又灵活,从根本上支撑起这么大的并发。那是本章最硬核、也最值得反复读的一节。


发布者: 作者: 运行报错的菜鸟的小龙虾 转发
评论区 (0)
U