文章

推理系统 01:一条请求如何抵达硬件,又变成回答

以 Qwen3-8B 和固定 vLLM V1 源码为坐标,沿数据流与控制流追踪请求、分词、调度、Prefill、Decode 和流式输出。

推理系统 01:一条请求如何抵达硬件,又变成回答

系列目录 · 下一章:文本到张量

用户输入“解释一下矩阵乘法”,界面开始显示回答。我们容易把中间过程缩写成“模型理解问题,然后生成答案”。但要解释首字等待、显存不足或者多用户排队,就必须把这句话拆成一组可以观察的计算与控制过程。

本章的任务是建立坐标。我们不在第一章穷尽每个算子,而是追踪一个请求:它在什么时候是字符串,什么时候是整数数组,什么时候成为浮点张量;谁决定何时执行;哪些数据被保留到下一个生成步骤。

1. 先沿着地图走一遍

全屏打开实验

点击“下一步”时,观察右侧三件事:数据的形状、控制组件、以及当前阶段尚未发生的事。然后把输入长度从 128 改为 2048,思考为什么隐藏状态与缓存变大,但模型层数和权重表形状不变。

这个页面执行形状与容量计算,不执行神经网络。流程是单请求的教学剖面;真实服务会让许多请求交错前进。

2. 数据流和控制流是两条线

数据流描述变换:

1
2
3
网络字节 → 消息对象 → 模板化文本 → Token IDs
       → 隐藏向量 → 层内张量 → logits → 新 Token ID
       → 文本片段 → 网络响应

控制流描述决定:

1
2
请求校验 → 等待或准入 → 选择本轮 Token → 准备执行输入
       → 启动设备计算 → 处理输出 → 继续调度或结束

第一条线回答“算什么”,第二条线回答“什么时候算、与谁一起算、资源从哪里来”。大模型主要定义数值计算;vLLM 组织服务与执行;设备运行内核。它们之间还隔着张量框架、后端、编译和运行时。

因此,Token ID 并不会被翻译成一条“表达含义”的机器指令。它是数据。设备上的指令执行查表、矩阵乘法、规约和搬运,而数值与索引决定这些指令处理什么内容。

3. 请求抵达前,权重通常已经在设备里

先建立一个常驻服务的假设:模型已经完成加载,运行时已经建立,必要的预热也已执行。此时用户输入并不会携带模型权重。

权重是跨请求复用的长期资源;请求的 Token IDs、当前位置和 KV Cache 则与请求生命周期有关。把两者混为一谈,会错误地以为每次提问都需要把全部模型从 CPU 上传到 GPU。

冷启动、权重卸载、多模型切换会改变这个假设,但不能拿它们代表已经预热的常驻服务。性能测量时因此必须说明是否包含冷启动。

主线模型固定为 Qwen3-8B,配置给出隐藏维度 4096、36 层、32 个 Query 头、8 个 KV 头及 128 的单头维度。这些值来自固定修订的 config.json,而不是根据“8B”标签猜测。

4. 文本先经过模板与分词

聊天接口接收的往往是带 role 和 content 的消息,而不是一段已经准备好的模型输入。例如 user、assistant、system 是应用层的结构化字段。

聊天模板将这些字段排列成模型训练时约定的序列,加入角色和轮次标记。分词器再把序列编码成整数。于是,用户看见的十个字可能对应不止十个 Token,模板还会额外贡献 Token。

对于单条长度为 T 的序列,概念上的输入为:

\[\mathbf{i}=(i_0,i_1,\ldots,i_{T-1}),\qquad i_t\in\{0,\ldots,V-1\}.\]

这里的 V 表示模型输入表允许的行数;实际 tokenizer 词表和模型为对齐等原因保留的行数需要分别检查。整数容器在具体后端中可能使用不同位宽,不应把教学图中的整数数组直接认定为某一种内存 ABI。

第 2 章会用真实样本检查模板、字节和 Token 的边界。本章先记住:序列长度应以实际送入模型的编码为准。

5. vLLM 决定本轮执行哪些 Token

在 vLLM 的 V1 路径中,服务前端、引擎核心、调度器和模型执行器承担不同责任。为防止滚动更新让阅读路径失效,本系列使用 v0.10.2 源码作坐标:

这些是源码阅读入口,不意味着在本机运行过整套服务,也不意味着 NPU 后端使用同一个 GPU 文件。

假设 A 的输入很长,B 已经开始逐 Token 生成。一次调度可以选择 A 的一段输入和 B 的一个新位置。真实的批处理因此未必是整齐的 [B,T,d] 长方体;执行器可以把本轮 Token 打包,辅以位置、序列边界和缓存映射等元数据。

这解释了为什么 HTTP 层同时收到的请求,不一定共同组成后续每一个执行批次。“请求批次”和“计算步的 Token 集合”必须区分。

6. 查表后,整数进入数值计算

嵌入表为:

\[E\in\mathbb{R}^{V\times d},\qquad x_t=E[i_t,:].\]

Qwen3-8B 的表形状是 [151936,4096]。选中一个 ID,就读取一行 4096 个数;长度 T 的输入概念上得到 [T,4096] 隐藏状态。

从线性代数看,查表可以写成 one-hot 向量与 E 相乘,但实际不必构造长度 V 的稀疏 one-hot 数组。直接索引同一行即可。数学等价形式不等于运行时一定采用的操作步骤。

隐藏向量随后经过 Transformer 层。位置处理也需要遵循具体模型:这里使用 RoPE 作用于 Q/K,不能把所有模型统一画成“输入嵌入加一张绝对位置嵌入表”。

7. Prefill 不是同时独立回答每个位置

Prefill 处理输入序列,因果注意力让位置 t 只能读取不晚于 t 的信息。虽然输入位置已经全部给定,可以并行组织其计算,但它们通过注意力相互依赖,并非相互独立。

对某一层,Q/K/V 的概念形状分别为:

\[Q:[T,32,128],\qquad K,V:[T,8,128].\]

这是 GQA:多个 Query 头共享一组 KV 头。每层都产生自己的 K/V,缓存保留它们供后续生成使用。

最后一个输入位置的最终隐藏状态用于预测下一个 Token。普通自回归路径中,第一个输出 Token 通常由 Prefill 的结果产生。先画完 Prefill、再额外执行一次单 Token Decode 才得到第一个输出,会把时间线错移一步。

Prefill 可以分块,因此“一次输入 Prefill”不一定等于“一次调度迭代”。本章地图表示语义阶段,不表示固定次数的内核调用。

8. Decode 复用什么,又仍需计算什么

采样得到新 ID 后,下轮将这个 ID 作为新输入。模型为新位置计算各层隐藏状态及新 K/V,同时读取历史 K/V 参与注意力。

缓存避免的是反复计算历史位置的 K/V 等相关工作。它没有消除模型权重读取、新位置的投影、MLP 或对历史缓存的访问。上下文越长,Decode 的注意力访问负担仍可能增加。

在未量化、无分片、无共享的简化条件下,T 个缓存位置的容量为:

\[M_{KV}=2LH_{kv}d_hTs.\]

因子 2 表示 K 与 V;s 为每个元素的字节数。代入 BF16 的 s=2:

\[M_{KV}/T=2\times36\times8\times128\times2 =147456\ \text{bytes}=144\ \text{KiB}.\]

4096 个缓存位置对应 576 MiB;两个相同长度、互不共享的请求就是 1152 MiB。这还没算权重、激活、分页取整和运行时开销,也不是多卡部署中每卡的最终占用。

9. 到底什么东西在 GPU 上执行

模型层描述运算关系;内核决定怎样把运算拆成设备可执行的工作。以矩阵乘法为例,内核需要把矩阵切块,安排数据进入片上存储或寄存器,再通过计算指令累加结果。

GPU 的矩阵计算单元、线程组织、内存层次和互连影响这件事;NPU 也需要相应的计算、存储与搬运机制,但其编程与执行模型不能直接套用 warp 或 CUDA 的术语。

实现可能融合多个操作,也可能为不同形状选择不同内核。数学上写出 softmax(QKᵀ)V,不代表完整 QKᵀ 一定被写入 HBM。反过来,“支持这个模型”也不等于所有算子都已经走上最高效路径。

因此,从模型 FLOPs 推出时间还缺少数据搬运、并行效率、内核开销与同步等因素。Prefill 常呈现更高的矩阵计算利用率,而低批量 Decode 常受到权重读取带宽约束;这些是条件相关的倾向,不是适用于任意上下文和并发量的定律。

10. 输出 Token 不等于一个网络片段

新 ID 经分词器解码后,服务端把文本片段发送给客户端。一个 Token 可能覆盖多个字符,也可能只对应某个字符的一部分字节;流式实现可能缓冲不完整文本,网络传输还会重新分块。

所以,网页收到多少个事件,不能直接当作生成了多少 Token。统计应使用服务端的 Token 计数,并明确首字时间是在客户端还是服务端测量。排队、分词、Prefill 和网络都可能影响用户感知的首字等待。

达到停止条件、输出上限或用户取消时,请求结束。缓存资源需要回收;若存在前缀缓存等复用机制,物理块生命周期还可能长于这一个请求。

11. 检查理解

问题一:输入长度翻倍,模型权重是否翻倍?

展开推导

不会。权重与模型结构有关,跨请求复用;输入隐藏状态和该请求缓存的位置数会变化。注意力计算量的变化还取决于阶段和实现,不能简单把所有开销都乘二。

问题二:已经缓存历史 K/V,Decode 是否完全不需要读取历史?

展开推导

仍需读取。新 Query 要与历史 Key 形成注意力,并聚合历史 Value。缓存节省重新生成历史状态的工作,不等于历史不再参与计算。

问题三:同一个数学模型在两种设备上结果语义相同,是否意味着程序相同?

展开推导

不意味着。算子分解、数据布局、低精度表示、内核、运行时和驱动都可能不同。即使数学目标相同,浮点运算顺序也可能带来数值差异。

下一章沿输入端继续向下:我们会看到“你好”怎样变成字节、真实 Token ID,以及聊天模板怎样改变模型真正收到的序列。

资料与复现范围

模型配置引用见上文;源码版本 v0.10.2 是固定阅读基线。交互地图没有加载权重、执行 vLLM 或进行性能实测。公式采用单序列、无缓存共享的 BF16 示例。更完整的版本和数据摘要见系列目录

本文由作者按照 CC BY 4.0 进行授权