团团虾声明:基于 vLLM v0.20.1 源码中 qwen3.py 和 qwen2.py 的实现,逐层梳理 Qwen3 从 token 输入到 logits 输出的完整推理计算流程。
Qwen3 推理全流程解析
基于 vLLM v0.20.1 中
qwen3.py+qwen2.py的实现
vllm/model_executor/models/qwen3.pyvllm/model_executor/models/qwen2.py
总览:端到端数据流
graph TD
A["input_ids<br/>(token 序列)"] --> B["Embedding<br/>VocabParallelEmbedding"]
B --> C["hidden_states<br/>(seq_len, hidden_size)"]
C --> D["Decoder Layer × N"]
D --> E["Final RMSNorm"]
E --> F["LM Head<br/>(线性映射到 vocab_size)"]
F --> G["Logits → 采样 → 输出 token"]
subgraph "每个 Decoder Layer"
D1["Input RMSNorm"] --> D2["Self-Attention<br/>(QKV + QK-Norm + RoPE + Attention + O_proj)"]
D2 --> D3["Residual Add"]
D3 --> D4["Post-Attention RMSNorm"]
D4 --> D5["MLP (SwiGLU)"]
D5 --> D6["Residual Add"]
end
第 1 步:Embedding(词嵌入)
入口: Qwen3ForCausalLM.forward → Qwen2Model.forward
# qwen2.py L429-L433
hidden_states = self.embed_input_ids(input_ids) # VocabParallelEmbedding
residual = None
计算: input_ids: (seq_len,) → 查表 → hidden_states: (seq_len, hidden_size)
每个 token ID 被映射成一个 hidden_size 维的向量。例如 Qwen3-8B 的 hidden_size=4096。
第 2 步:N 个 Decoder Layer 循环
# qwen2.py L441-L444
for idx, layer in enumerate(islice(self.layers, self.start_layer, self.end_layer)):
hidden_states, residual = layer(positions, hidden_states, residual)
每个 layer 是 Qwen3DecoderLayer,包含两个子模块:Self-Attention 和 MLP,各自前面有一个 RMSNorm,各自后面有一个 残差连接。
2.1 Input RMSNorm + 残差管理
# qwen3.py L223-L227
if residual is None: # 第一层
residual = hidden_states
hidden_states = self.input_layernorm(hidden_states)
else: # 后续层: fused add + norm
hidden_states, residual = self.input_layernorm(hidden_states, residual)
计算: RMSNorm 对每个 token 向量做归一化:
[!TIP] vLLM 的
RMSNorm支持 fused add + norm:hidden_states, residual = norm(hidden_states, residual)会先做residual += hidden_states,再对结果做 norm。这减少了一次显存读写,是性能优化。
2.2 Self-Attention(自注意力)
这是 Qwen3 与 Qwen2 的关键区别所在。整个注意力计算分为 5 步:
步骤 ①:QKV 线性投影
# qwen3.py L150-L151
qkv, _ = self.qkv_proj(hidden_states) # (seq_len, q_size + 2*kv_size)
q, k, v = qkv.split([self.q_size, self.kv_size, self.kv_size], dim=-1)
计算: 一次矩阵乘法 hidden_states @ W_qkv,然后切分成 Q、K、V 三个张量。
[!IMPORTANT] Qwen3 的 QKV 没有 bias(
qkv_bias=False,见 L195),而 Qwen2 的 QKV 有 bias(bias=True)。这是两代架构的一个重要差异。
GQA (Grouped-Query Attention): Qwen3 使用 GQA,即 num_kv_heads < num_attention_heads。例如 Qwen3-8B 有 32 个 Q 头,但只有 8 个 KV 头,每 4 个 Q 头共享一组 KV。
步骤 ②:QK-Norm(🆕 Qwen3 新增)
# qwen3.py L152-L158
q_by_head = q.view(*q.shape[:-1], q.shape[-1] // self.head_dim, self.head_dim)
q_by_head = self.q_norm(q_by_head) # 对每个头独立做 RMSNorm
q = q_by_head.view(q.shape)
k_by_head = k.view(*k.shape[:-1], k.shape[-1] // self.head_dim, self.head_dim)
k_by_head = self.k_norm(k_by_head) # 对每个头独立做 RMSNorm
k = k_by_head.view(k.shape)
计算: 将 Q/K reshape 成 (seq_len, num_heads, head_dim),对每个头的 head_dim 维度做 RMSNorm,再 reshape 回去。
[!IMPORTANT] QK-Norm 是 Qwen3 相对于 Qwen2 的核心架构改进。 Qwen2 没有这一步(Qwen2 的
qk_norm默认为False,是后来为 BAGEL 等模型才加的可选项)。QK-Norm 的作用是稳定注意力分数,防止 Q·K 点积值过大导致训练不稳定。
步骤 ③:RoPE 旋转位置编码
# qwen3.py L159
q, k = self.rotary_emb(positions, q, k)
计算: 对 Q 和 K 施加旋转位置编码。将每个头的 head_dim 维度两两配对,按照 position 乘以旋转矩阵:
这使得 Q·K 的点积只依赖于两个 token 的相对位置。
步骤 ④:Attention 计算
# qwen3.py L160
attn_output = self.attn(q, k, v)
计算(标准 scaled dot-product attention):
其中 mask 是因果掩码(causal mask),确保每个 token 只能看到它之前的 token。
[!NOTE] vLLM 中
self.attn的实际实现取决于后端(FlashAttention、PagedAttention 等),此处封装在Attention类中。推理时,KV 会被缓存(KV Cache),新 token 只需计算自己的 Q 并与已缓存的 KV 做注意力。
步骤 ⑤:输出投影
# qwen3.py L161
output, _ = self.o_proj(attn_output) # RowParallelLinear
计算: attn_output @ W_o,将注意力输出映射回 hidden_size 维度。
2.3 残差连接 + Post-Attention RMSNorm
# qwen3.py L234
hidden_states, residual = self.post_attention_layernorm(hidden_states, residual)
计算: residual = residual + attn_output,然后 hidden_states = RMSNorm(residual)
2.4 MLP(SwiGLU)
# qwen2.py L113-L117
def forward(self, x):
gate_up, _ = self.gate_up_proj(x) # (seq_len, 2 * intermediate_size)
x = self.act_fn(gate_up) # SiluAndMul → (seq_len, intermediate_size)
x, _ = self.down_proj(x) # (seq_len, hidden_size)
return x
计算(SwiGLU 激活):
分解为三步:
- gate_up_proj: 一次矩阵乘法同时计算 gate 和 up 两路:
x @ [W_gate; W_up],输出2 * intermediate_size - SiluAndMul: 前半部分过 SiLU 激活函数,与后半部分逐元素相乘
- down_proj: 降维回
hidden_size
[!NOTE] SwiGLU 中的 intermediate_size 通常是
hidden_size的 ~2.67 倍(例如 Qwen3-8B: hidden=4096, intermediate=11008)。
2.5 残差连接
# qwen3.py L235-L236
hidden_states = self.mlp(hidden_states)
return hidden_states, residual
# 注意:这里的残差连接在下一层的 input_layernorm 中以 fused 方式完成
第 3 步:Final RMSNorm
# qwen2.py L454
hidden_states, _ = self.norm(hidden_states, residual)
计算: 最后一次残差加和 + RMSNorm,得到最终的 hidden_states。
第 4 步:LM Head → Logits → 采样
入口: Qwen3ForCausalLM.compute_logits
# qwen3.py L331-L332
logits = self.logits_processor(self.lm_head, hidden_states)
计算:
- LM Head:
hidden_states @ W_lm_head→(seq_len, vocab_size)的 logits - 采样: 在 logits 上做 top-k / top-p / temperature 采样,得到下一个 token
[!NOTE] 如果
tie_word_embeddings=True(L295-L296),LM Head 直接复用 Embedding 层的权重矩阵,不额外占用显存。
Qwen3 vs Qwen2:关键架构差异
| 特性 | Qwen2 | Qwen3 |
|---|---|---|
| QKV bias | ✅ 有 bias | ❌ 无 bias |
| QK-Norm | ❌ 默认无 | ✅ 必选(每个头独立 RMSNorm) |
| MLP | SwiGLU | SwiGLU(完全相同,直接复用) |
| Layer Norm | RMSNorm | RMSNorm(相同) |
| 位置编码 | RoPE(默认 θ=1M) | RoPE(默认 θ=1M) |
这个文件能理解到什么程度?
✅ 能理解的
| 层面 | 说明 |
|---|---|
| 模型整体架构 | Embedding → N × DecoderLayer → Norm → LM Head 的完整流程 |
| 每层的计算步骤 | RMSNorm → QKV → QK-Norm → RoPE → Attention → O_proj → RMSNorm → MLP 的精确顺序 |
| 数据流和张量形状 | 每一步输入输出的维度变化 |
| Qwen3 的独特设计 | QK-Norm、无 QKV bias 等区别于其他模型的选择 |
| 并行化策略 | Tensor Parallel (TP) 和 Pipeline Parallel (PP) 的切分逻辑 |
⚠️ 不能完全理解的(需要看其他文件)
| 层面 | 需要看的文件 |
|---|---|
| Attention 具体内核 | vllm/attention/backends/ — FlashAttention / PagedAttention 的实际 CUDA 实现 |
| KV Cache 管理 | vllm/v1/core/ — PagedAttention 的内存管理和块分配 |
| 采样策略 | vllm/model_executor/layers/sampler.py — top-k, top-p, temperature 等 |
| 调度和批处理 | vllm/v1/core/scheduler.py — continuous batching 和请求调度 |
| RoPE 实现细节 | vllm/model_executor/layers/rotary_embedding.py — 旋转位置编码的具体数学 |
| 量化 | vllm/model_executor/layers/quantization/ — GPTQ, AWQ, FP8 等量化逻辑 |