团团虾声明:基于 vLLM 源码中 qwen3_5.py、qwen3_next.py 和 gdn_linear_attn.py 的实现,逐层拆解 Qwen3.5 的混合注意力架构与推理计算流程。
Qwen3.5 推理全流程:从提示到输出的逐层计算解析
基于 vLLM 源码 (
vllm/model_executor/models/qwen3_5.py,qwen3_next.py,gdn_linear_attn.py) 分析
总体架构概览
Qwen3.5 是一个 Hybrid(混合)架构 模型,核心特点是将 全注意力(Full Attention) 和 门控增量网络线性注意力(Gated Delta Net Linear Attention) 交替混合使用。同时支持 Dense MLP 和 Sparse MoE 两种 FFN 变体。
graph TD
A["Input Token IDs"] --> B["Embedding Layer"]
B --> C["Decoder Layer 0"]
C --> D["Decoder Layer 1"]
D --> E["..."]
E --> F["Decoder Layer N-1"]
F --> G["Final RMSNorm"]
G --> H["LM Head (Linear)"]
H --> I["LogitsProcessor"]
I --> J["Output Token Probabilities"]
style C fill:#2d5aa0,color:#fff
style D fill:#8b4513,color:#fff
style F fill:#2d5aa0,color:#fff
其中每个 Decoder Layer 的注意力类型由 config.layer_types 列表逐层指定:
"full_attention"→ 标准 Softmax 自注意力(蓝色)"linear_attention"→ GatedDeltaNet 线性注意力(棕色)
阶段一:输入嵌入 (Embedding)
入口: Qwen3_5Model.forward() → embed_input_ids()
input_ids: [batch_size, seq_len] (int64)
↓
VocabParallelEmbedding(vocab_size, hidden_size)
↓
hidden_states: [batch_size × seq_len, hidden_size] (bf16/fp16)
计算:查表操作,将每个 token ID 映射为 hidden_size 维的稠密向量。
- 源码:qwen3_5.py:224-227
- 初始
residual = None
阶段二:Decoder 层堆叠
每个 Decoder Layer 的结构相同,但注意力模块根据层类型不同而不同。
整体流程(Pre-Norm 架构 + 残差连接):
graph TD
IN["hidden_states, residual"] --> LN1["Input LayerNorm (GemmaRMSNorm)"]
LN1 --> ATN{"layer_type?"}
ATN -->|full_attention| FA["Full Attention"]
ATN -->|linear_attention| LA["GatedDeltaNet Linear Attention"]
FA --> LS1["Attention Layer Scale (可选)"]
LA --> LS1
LS1 --> LN2["Post-Attention LayerNorm (GemmaRMSNorm)"]
LN2 --> MLP{"MLP 类型?"}
MLP -->|Dense| DMLP["Dense MLP"]
MLP -->|MoE| MOE["Sparse MoE Block"]
DMLP --> LS2["FFN Layer Scale (可选)"]
MOE --> LS2
LS2 --> OUT["hidden_states, residual"]
2.1 Input LayerNorm (GemmaRMSNorm)
与标准 RMSNorm 的两点区别:
- 权重使用
x * (1 + w)而非x * w(权重初始化为 0,而非 1) - 类型转换顺序不同:
(x * w).to(orig_dtype)而非x.to(orig_dtype) * w
数学公式:
首层特殊处理(residual is None):
residual = hidden_states # 保存残差
hidden_states = layernorm(hidden_states) # 仅归一化
后续层(fused add + norm):
hidden_states = hidden_states + residual # 加残差
residual = hidden_states # 更新残差
hidden_states = layernorm(hidden_states) # 归一化
2.2A Full Attention 层 (Qwen3NextAttention)
当 layer_type == "full_attention" 时执行。这是标准的 Transformer 自注意力,但包含多个 Qwen3.5 特有的改进。
Step 1: QKV 投影
qkv, _ = self.qkv_proj(hidden_states)
# hidden_states: [num_tokens, hidden_size]
# qkv: [num_tokens, (num_heads*2 + num_kv_heads*2) * head_dim]
# 当 attn_output_gate=True 时 Q 的通道数翻倍(含 gate)
特殊点:attn_output_gate=True(默认)时,Q 投影的输出维度翻倍,额外部分作为输出门控。
Step 2: 拆分 Q/Gate/K/V
# attn_output_gate=True:
q_gate, k, v = qkv.split([q_size*2, kv_size, kv_size], dim=-1)
q, gate = torch.chunk(q_gate, 2, dim=-1) # 各 [num_tokens, q_size]
# attn_output_gate=False:
q, k, v = qkv.split([q_size, kv_size, kv_size], dim=-1)
Step 3: QK-Norm (GemmaRMSNorm per head)
对每个头单独做 RMSNorm(防止注意力 logit 爆炸):
q = self.q_norm(q.view(-1, num_heads, head_dim)).view(-1, q_size)
k = self.k_norm(k.view(-1, num_kv_heads, head_dim)).view(-1, kv_size)
Step 4: RoPE 旋转位置编码
q, k = self.rotary_emb(positions, q, k)
对 Q 和 K 施加旋转位置编码,使模型获得位置感知能力。
Step 5: Scaled Dot-Product Attention
attn_output = self.attn(q, k, v) # 含 KV Cache 管理
vLLM 的 Attention 层内部处理:
- Prefill:使用 FlashAttention / PagedAttention
- Decode:从 KV Cache 读取历史 K/V,仅对新 token 做注意力
Step 6: 输出门控 (Output Gate)
if self.attn_output_gate:
gate = torch.sigmoid(gate) # σ(gate)
attn_output = attn_output * gate # 逐元素门控
这是 Qwen3.5 的关键创新之一:对注意力输出施加可学习的 sigmoid 门控。
Step 7: 输出投影
output[:], _ = self.o_proj(attn_output)
# o_proj: RowParallelLinear(num_heads * head_dim → hidden_size)
2.2B GatedDeltaNet 线性注意力层
当 layer_type == "linear_attention" 时执行。这是 Qwen3.5 的核心创新——一种基于状态空间模型思想的线性注意力机制,用递推状态矩阵替代 KV Cache。
整体计算流 (forward_cuda)
graph TD
H["hidden_states"] --> P1["输入投影 in_proj_qkvz"]
H --> P2["输入投影 in_proj_ba"]
P1 --> S["拆分 Q, K, V, Z"]
P2 --> S2["拆分 B, A"]
S --> CONV["因果卷积 Conv1D"]
S2 --> CORE["核心注意力计算"]
CONV --> CORE
CORE --> NORM["RMSNormGated(attn_out, z)"]
NORM --> OUT["输出投影 out_proj"]
Step 1: 输入投影
# 投影 1: 生成 Q, K, V, Z
mixed_qkvz, _ = self.in_proj_qkvz(hidden_states)
# hidden_states: [num_tokens, hidden_size]
# mixed_qkvz: [num_tokens, key_dim*2 + value_dim*2]
# 拆分
mixed_qkv, z = mixed_qkvz.split([qkv_size, z_size], dim=-1)
# mixed_qkv: [num_tokens, key_dim*2 + value_dim] (用于 Q, K, V)
# z: [num_tokens, value_dim] (用于输出门控)
# 投影 2: 生成 B, A(控制遗忘和写入)
ba, _ = self.in_proj_ba(hidden_states)
b, a = ba.chunk(2, dim=-1) # 各 [num_tokens, num_v_heads]
各投影的含义:
| 投影 | 维度 | 作用 |
|---|---|---|
| Q | key_dim | 查询向量,用于读取状态 |
| K | key_dim | 键向量,用于写入状态 |
| V | value_dim | 值向量,写入的内容 |
| Z | value_dim | 输出门控信号 |
| B | num_v_heads | 写入门控(beta = σ(B)) |
| A | num_v_heads | 遗忘门控(与 A_log, dt_bias 结合) |
Step 2: 因果卷积 (Causal Conv1D)
对 mixed_qkv 沿序列维度做短卷积(kernel_size 通常为 4),引入局部上下文:
# Prefill: 对整个序列做因果卷积
mixed_qkv = causal_conv1d_fn(mixed_qkv, conv_weights, bias, activation="silu",
conv_states=conv_state, ...)
# Decode: 仅更新单步(从 conv_state 缓存读取历史)
mixed_qkv = causal_conv1d_update(mixed_qkv, conv_state, conv_weights, bias, "silu", ...)
关键点:卷积状态 conv_state 持久化保存,类似 KV Cache 但体积极小(仅 kernel_size - 1 步)。
Step 3: 后卷积预处理 (fused_post_conv_prep)
将卷积输出和门控信号组合,生成核心注意力所需的全部输入:
q, k, v, g, beta = fused_post_conv_prep(
conv_output=mixed_qkv, a=a, b=b,
A_log=self.A_log, dt_bias=self.dt_bias, ...
)
各输出的计算:
- Q, K: 从卷积输出中拆分,并做 L2 归一化
- V: 直接从卷积输出拆分
- g(遗忘门):
- beta(写入门):
Step 4: 核心递推注意力
这是 GatedDeltaNet 的核心。与标准注意力不同,它维护一个递推状态矩阵 :
Prefill(分块计算):
output, final_state = chunk_gated_delta_rule(
q, k, v, g, beta, initial_state, ...)
Decode(逐步递推):
output, final_state = fused_sigmoid_gating_delta_rule_update(
A_log, a, b, dt_bias, q, k, v, initial_state, ...)
递推公式(每个时间步 t,每个头):
其中:
- : 遗忘因子,控制历史状态的衰减速率
- : 写入门,控制新信息的写入强度
- : 新的键值外积(要写入的信息)
- : Delta Rule 的纠错项(先擦除旧信息再写入)
与标准注意力对比:
| Full Attention | GatedDeltaNet | |
|---|---|---|
| 复杂度 | (线性) | |
| 状态 | KV Cache 随序列线性增长 | 固定大小矩阵 |
| 信息整合 | 显式查询所有历史 | 递推压缩到状态矩阵 |
Step 5: 输出归一化与门控
# RMSNormGated: norm(attn_out) * activation(z)
core_attn_out = self.norm(core_attn_out, z)
其中 act 可以是 silu 或 sigmoid(由 config.output_gate_type 决定)。
Step 6: 输出投影
output[:num_tokens], _ = self.out_proj(core_attn_out)
# out_proj: RowParallelLinear(value_dim → hidden_size)
2.3 Attention Layer Scale(可选)
当 config.layer_scale = True 时,对注意力输出施加可学习的缩放:
其中 初始化为 0(即初始时为恒等变换),训练中逐渐学习合适的缩放。
if self.layer_scale:
hidden_states = hidden_states * (self.attn_layer_scale + 1)
2.4 Post-Attention LayerNorm
与 2.1 相同的 GemmaRMSNorm,执行 fused add + norm:
hidden_states, residual = self.post_attention_layernorm(hidden_states, residual)
# hidden_states = GemmaRMSNorm(attn_output + residual)
# residual = attn_output + residual(更新后)
2.5 MLP / MoE 前馈网络
2.5A Dense MLP(qwen3_5_text 模型)
使用 SwiGLU 激活的标准 FFN:
gate_up, _ = self.gate_up_proj(x) # [num_tokens, intermediate_size * 2]
out = self.act_fn(gate_up) # SiLU(gate) * up = SwiGLU
out, _ = self.down_proj(out) # [num_tokens, hidden_size]
数学公式:
2.5B Sparse MoE(qwen3_5_moe_text 模型)
graph LR
X["hidden_states"] --> GATE["Gate Linear"]
GATE --> TOP["Top-K 选择"]
X --> EXP["N 个 Expert MLP"]
TOP --> EXP
EXP --> AGG["加权聚合"]
X --> SE["Shared Expert MLP"]
SE --> SEG["Shared Expert Gate (sigmoid)"]
SEG --> ADD["相加"]
AGG --> ADD
ADD --> OUT["output"]
计算流程:
# 1. 路由器计算
router_logits, _ = self.gate(hidden_states) # [num_tokens, num_experts]
# 2. FusedMoE: Top-K 选择 + 专家计算 + 加权聚合 + 共享专家
output = self.experts(hidden_states, router_logits)
数学公式:
其中
2.6 FFN Layer Scale(可选)
与 2.3 类似:
if self.layer_scale:
hidden_states = hidden_states * (self.ffn_layer_scale + 1)
阶段三:最终归一化
所有 Decoder 层执行完毕后,对最后的 hidden_states 做一次最终的 GemmaRMSNorm:
hidden_states, _ = self.norm(hidden_states, residual)
# 即: hidden_states = GemmaRMSNorm(last_layer_output + residual)
阶段四:LM Head + Logits 计算
# 1. 线性投影到词表空间
logits = self.lm_head(hidden_states)
# lm_head: ParallelLMHead(hidden_size → vocab_size)
# logits: [num_tokens, vocab_size]
# 2. LogitsProcessor 后处理
logits = self.logits_processor(self.lm_head, hidden_states)
如果 tie_word_embeddings=True,则 lm_head 直接复用 embed_tokens 的权重矩阵。
阶段五:采样输出
logits 经过温度缩放、Top-K/Top-P 等采样策略后,得到下一个 token 的 ID。这部分由 vLLM 的 Sampler 处理,不在模型代码内。
完整单层计算总结(伪代码)
def decoder_layer_forward(hidden_states, residual, positions):
# ========== Pre-Norm ==========
if residual is None:
residual = hidden_states
hidden_states = GemmaRMSNorm(hidden_states)
else:
tmp = hidden_states + residual
residual = tmp
hidden_states = GemmaRMSNorm(tmp)
# ========== Attention ==========
if layer_type == "full_attention":
qkv = qkv_proj(hidden_states)
q, gate, k, v = split(qkv) # gate 仅当 attn_output_gate=True
q = GemmaRMSNorm_per_head(q) # QK-Norm
k = GemmaRMSNorm_per_head(k)
q, k = RoPE(positions, q, k) # 旋转位置编码
attn_out = FlashAttention(q, k, v) # + KV Cache
attn_out = attn_out * sigmoid(gate) # 输出门控
hidden_states = o_proj(attn_out)
elif layer_type == "linear_attention":
qkvz = in_proj_qkvz(hidden_states)
ba = in_proj_ba(hidden_states)
qkv, z = split(qkvz)
b, a = split(ba)
qkv = CausalConv1D(qkv, conv_state) # 因果卷积
q, k, v = split(qkv)
q, k = L2Norm(q), L2Norm(k) # L2 归一化
g = -exp(A_log) * softplus(a + dt_bias) # 遗忘门
beta = sigmoid(b) # 写入门
attn_out = GatedDeltaRule(q, k, v, g, beta, ssm_state) # 递推
hidden_states = out_proj(RMSNormGated(attn_out, z))
# ========== Layer Scale (Attention) ==========
if layer_scale:
hidden_states *= (attn_layer_scale + 1)
# ========== Post-Norm ==========
tmp = hidden_states + residual
residual = tmp
hidden_states = GemmaRMSNorm(tmp)
# ========== FFN ==========
if dense_model:
hidden_states = SwiGLU_MLP(hidden_states)
else: # MoE
router = gate(hidden_states)
hidden_states = FusedMoE(hidden_states, router) + sigmoid_gate * SharedExpert(hidden_states)
# ========== Layer Scale (FFN) ==========
if layer_scale:
hidden_states *= (ffn_layer_scale + 1)
return hidden_states, residual
状态缓存对比
Qwen3.5 需要管理两套不同的状态缓存系统:
| Full Attention 层 | Linear Attention 层 | |
|---|---|---|
| 缓存类型 | KV Cache | Conv State + SSM State |
| 缓存大小 | ,随序列增长 | 固定 + 固定 |
| 更新方式 | 追加新 K/V | 递推更新状态矩阵 |
| 管理 | PagedAttention | MambaBase (conv_state + ssm_state) |
这种混合设计使 Qwen3.5 在长序列推理时能节省显存(线性注意力层状态固定),同时通过全注意力层保持强大的全局建模能力。