Skip to content

约 6 分钟阅读

07. 一个请求的全链路 ​

前六篇一直盯着 GPU 上的计算。这一篇把镜头拉远,跟着资料助手的一次请求走完全程:从 HTTP 请求到达,经过对话模板、分词、调度、Prefill、Decode 循环、采样、反分词,直到最后一个字推送回浏览器。读完你应该能画出这条链路,指出每个环节在 CPU 还是 GPU 上运行、可能卡在哪里,并理解温度、top-p 这些采样参数在哪一步起作用。第三部分的每一项技术,都会在这条链路上找到自己的位置。

先走一遍 ​

一个请求从 API 入口到流式返回的全链路,标出 CPU 与 GPU 上的环节

按顺序,一个请求会经过这些环节:

环节做什么在哪里运行可能的瓶颈
API 入口接收 HTTP 请求,校验参数和权限CPU连接数、请求解析
对话模板把多轮消息拼成模型认识的格式CPU模板写错会直接影响质量
分词文本切成 token 编号CPU超长输入、高并发时占用 CPU
调度决定这一步哪些请求参与计算CPU请求很多时调度本身耗时
Prefill并行处理全部输入,建立 KV CacheGPU,计算受限输入很长时拉高 TTFT
Decode 循环每步生成一个 token,读全部 KVGPU,访存受限显存带宽,决定 TPOT
采样按参数从概率分布中选出 tokenGPU,较轻复杂约束会增加开销
停止判断检查结束符、停止词、长度上限CPU很少成为瓶颈
反分词与推送token 拼回文字,流式发给客户端CPU与主循环争抢 CPU

下面用一个只有几个 token 的教学例子,一步步看请求的状态和 KV Cache 怎样变化:

逐步观察一个请求教学 token,不是真实分词
  1. 1排队与分词
  2. 2Prefill
  3. 3Decode 1
  4. 4Decode 2
  5. 5停止与释放
输入年假怎么申请?
输出待生成待生成待生成

输入已转换为 4 个教学 token;尚未进行模型计算。

活跃 KV:0 个位置。刚采样出的 token,要在下一次前向中才建立自己的 KV。

1 / 5

对话模板:模型看到的不是你发的原文 ​

调用推理服务时,你通常发送的是一组消息:

json
[
  {"role": "system", "content": "你是公司资料助手,只根据提供的制度回答。"},
  {"role": "user", "content": "年假怎么申请?"}
]

模型并不理解 JSON。服务端会用这个模型专属的对话模板(chat template),把消息拼成一段带特殊标记的文本,标出哪段是系统提示、哪段是用户、从哪里开始轮到助手回答。不同模型的模板不同,模板通常和模型文件一起发布。

模板用错了,模型可能答非所问,或者停不下来。这类问题看起来像模型质量差,实际上出在推理服务的配置上。排查回答质量问题时,先确认实际送进模型的文本是什么。

分词和反分词:CPU 上的工作 ​

拼好的文本由分词器切成 token。分词跑在 CPU 上,一般很快,但输入有几万个 token、同时又有大量请求时,它也会占用可观的 CPU 时间。

生成出来的 token 要反过来拼回文字,叫反分词(detokenize)。流式输出时这一步要增量进行,而且有个细节:一个汉字在 UTF-8 编码里占 3 个字节,可能被切在两个 token 里。反分词时必须等字节凑齐了再输出,否则用户会看到乱码。

GPU 优化到位以后,这些 CPU 环节反而可能成为新的瓶颈:GPU 算完一步,要等 CPU 准备好下一步的输入才能继续。所以 vLLM 的 V1 版本把 API 服务、分词和反分词放在独立的进程里,与负责调度和 GPU 执行的引擎核心并行运行,避免它们拖慢主循环。

调度与执行:GPU 主循环 ​

请求分好词以后,进入推理引擎的等待队列。引擎的核心是一个循环,每一圈叫一个 step:

  1. 调度器看一眼当前所有请求:哪些在等待,哪些正在生成,显存还剩多少,然后选出这一步要参与计算的请求,以及每个请求这一步处理多少 token。
  2. 执行器把这些请求的输入整理成张量,连同每个请求的 KV Cache 位置信息,一起送进模型做一次前向计算。新请求在这一步做 Prefill,老请求做一步 Decode。
  3. 模型为每个请求的最后一个位置输出一组分数,交给采样器选出下一个 token。
  4. 新 token 追加到各自的请求里,检查是否该停止;没停的进入下一圈。

这个循环每秒要跑几十到上百圈。第三部分讲的分页、连续批处理、前缀缓存、分块预填充,都是在改进这个循环的某一步。

采样:从概率到 token ​

模型每一步输出的是词表上每个 token 的分数(logits)。怎样从中选出一个,由请求里的采样参数决定。

贪心(greedy):直接选分数最高的那个。结果最稳定,但容易重复,缺少变化。请求里把 temperature 设为 0,通常就是贪心。

温度(temperature):先把分数除以温度,再换算成概率,然后按概率随机抽取。

text
概率ᵢ ∝ exp(分数ᵢ ÷ 温度)

温度小于 1,高分和低分的差距被拉大,抽取更保守;温度大于 1,分布被拉平,低概率的 token 也有机会被选中,输出更多样,也更容易出错。资料助手这类要求准确的场景,温度通常设得很低。

top-k:只在分数最高的 k 个 token 里抽取。top-p(核采样):按概率从高到低累加,只在累计概率达到 p 的那一小组 token 里抽取。它们的作用都是砍掉长尾里那些不靠谱的候选。

还有一些参数在采样之前修改分数:重复惩罚降低已经出现过的 token 的分数;结构化输出(比如必须输出合法 JSON)会把不符合语法的 token 的分数直接设为负无穷。

关于确定性要注意一点:即使温度为 0,同一个问题在服务端多次请求,结果也可能不完全相同。因为请求和别的请求凑在不同的批里时,GPU 上的计算顺序会变化,浮点数相加的顺序不同,结果会有极小的差异,偶尔会让某一步选出不同的 token。需要严格可复现时,要用引擎专门提供的确定性模式,并接受它带来的性能代价。

停止、流式返回与取消 ​

每生成一个 token,引擎都要判断是否停止:遇到模型的结束符,命中请求里指定的停止词,或者达到 max_tokens 上限。返回结果里通常带一个结束原因(如 stop 或 length),用来区分“正常说完”和“被长度截断”。被截断的回答,应用层要当作不完整的结果处理。

流式输出通常用 SSE(Server-Sent Events)协议,每生成一小段就推送一次。前面说过,一次推送可能包含多个 token,测 TPOT 时不能按推送次数计算。

用户中途关掉页面时,取消信号要一路传进引擎:从等待队列中移除请求,或者在下一步把它从批中拿掉,并释放它的 KV Cache。如果取消没有传到引擎,GPU 会继续为一个已经没人看的请求生成内容,白白占着显存和算力。高峰期大量用户刷新页面时,这个问题会放大。

每个环节由后面哪一篇展开 ​

链路上的问题对应技术章节
KV Cache 怎样分配而不浪费PagedAttention第 8 篇
调度器怎样挑选请求、显存不够怎么办连续批处理、抢占第 9 篇
同样的系统提示和资料能否少算前缀缓存第 10 篇
长输入的 Prefill 怎样不卡住别人分块预填充第 11 篇
一次前向内部怎样更快注意力算子、CUDA Graph第 12 篇
每一步要搬的数据能否更少量化第 14 篇
一步能否推进多个 token投机解码第 15 篇

一个请求要经过 API 入口、对话模板、分词、调度、Prefill、Decode 循环、采样、停止判断和反分词,其中只有中间几步在 GPU 上运行。对话模板决定模型看到什么,采样参数决定怎样从概率里选出 token,取消要一直传到引擎才能释放资源。

第二部分到这里结束。我们已经知道瓶颈在哪里:Decode 访存受限,KV Cache 随人数和长度增长,CPU 环节也可能拖慢 GPU。第三部分开始逐个拆解推理引擎的核心技术,第一个是让 vLLM 成名的 PagedAttention。

别急,先让缓存热一下。