从零开始写Qwen3(一)模型结构分析
概述
从零构建大模型 中构建了一个GPT2,于是就想着照着这个拓展到更新的模型,比如Qwen3(虽然现在Qwen3.5都出了)
这个系列的目标首先是自己用Torch搭建一个Qwen3模型,实现推理,然后未来会实现KVCache、手写CUDA算子等
项目链接:qwen3_from_scratch
Qwen3结构分析
首先看一下Qwen3的整体结构,主要可以参考两个库
- transformers库,在
transformers.models.qwen3.modeling_qwen3中包含了完整的结构,都是python代码 - llama.cpp,用C++写的,这个可以学习算子编写,Qwen的结构可以在
llama-model.cpp中的llama_model_load函数看到模型的加载,在一个巨大的switch-case中有一个Qwen3的分支,然后在llm_build_qwen3这个类中看到计算图的搭建,看到具体的细节
总的来说Qwen3的基本结构是这样的:
以下内容来自transformers加载Qwen3-0.6B模型后打印的结果
Qwen3ForCausalLM(
(model): Qwen3Model(
(embed_tokens): Embedding(151936, 1024)
(layers): ModuleList(
(0-27): 28 x Qwen3DecoderLayer(
(self_attn): Qwen3Attention(
(q_proj): Linear(in_features=1024, out_features=2048, bias=False)
(k_proj): Linear(in_features=1024, out_features=1024, bias=False)
(v_proj): Linear(in_features=1024, out_features=1024, bias=False)
(o_proj): Linear(in_features=2048, out_features=1024, bias=False)
(q_norm): Qwen3RMSNorm((128,), eps=1e-06)
(k_norm): Qwen3RMSNorm((128,), eps=1e-06)
)
(mlp): Qwen3MLP(
(gate_proj): Linear(in_features=1024, out_features=3072, bias=False)
(up_proj): Linear(in_features=1024, out_features=3072, bias=False)
(down_proj): Linear(in_features=3072, out_features=1024, bias=False)
(act_fn): SiLUActivation()
)
(input_layernorm): Qwen3RMSNorm((1024,), eps=1e-06)
(post_attention_layernorm): Qwen3RMSNorm((1024,), eps=1e-06)
)
)
(norm): Qwen3RMSNorm((1024,), eps=1e-06)
(rotary_emb): Qwen3RotaryEmbedding()
)
(lm_head): Linear(in_features=1024, out_features=151936, bias=False)
)
整体的数据流是这样的
以下都是我自己写的,命名尽量和transformers保持一致,方便加载模型参数
def forward(self, idx: torch.Tensor...):
x = self.tok_embd(idx)
for layer in self.trf_blocks:
x = layer(x, ...)
x = self.final_norm(x)
logits = self.output_head(x)
return logits
在Transformer层内部是这样的
def forward(self, x, ...):
inp_x = x
x = self.input_layernorm(x)
x = self.self_attn(x, ...)
ffn_inp_x = x + inp_x
x = ffn_inp_x
x = self.post_attention_layernorm(x)
x = self.mlp(x)
return x + ffn_inp_x
自注意力内部是这样的
def forward(self, x, ...):
input_shape = x.shape[:-1]
hidden_shape = (*input_shape, -1, self.config.head_dim)
q = self.q_norm(self.q_proj(x).view(hidden_shape).transpose(1, 2))
k = self.k_norm(self.k_proj(x).view(hidden_shape).transpose(1, 2))
v = self.v_proj(x).view(hidden_shape).transpose(1, 2)
q = self.rope(q, ...)
k = self.rope(k, ...)
o = (
self.gqa(q, k, v, ...)
.transpose(1, 2)
.reshape(*input_shape, -1)
)
o = self.o_proj(o)
return o
FFN是
def forward(self, x):
embed_up = self.up_proj(x)
embed_gate = self.activation(self.gate_proj(x))
return self.down_proj(embed_up * embed_gate)
和从零构建大模型的GPT2模型相比,Qwen3的模型出现了这些新的组件:
- 使用RMSNorm取代LayerNorm
- 使用Rope取代正弦位置嵌入
- 使用带有门控的SwiGLU取代原来的简单MLP
- 使用分组注意力(GQA)取代原始的多头自注意力
其他部分基本保持不变,后面也主要去做这些组件的变体
RMSNorm
基本可以认为是简化版的LayerNorm,研究人员发现LayerNorm的位移没有必要,删掉了,仅保留尺度缩放,也就是
RMSNorm ( x ) i , j = x i , j ∑ j x i , j 2 + ϵ ⋅ β j \text{RMSNorm}(x)_{i,j} = \frac{x_{i,j}}{\sqrt{\sum_j {x_{i,j}^2+\epsilon}}}\cdot \beta_j RMSNorm(x)i,j=∑jxi,j2+ϵxi,j⋅βj
其中 i i i是seqLen, j j j是维度
RoPE
旋转位置嵌入
和传统的正弦位置嵌入不同,RoPE不是只在嵌入上加上位置信息,而是在每一层的Q和K都进行旋转,通过旋转一定角度让向量内积包含相对位置信息。具体原理后面实现的时候再看
GQA
分组查询注意力
传统的MHA的Q的头部和KV的头部相同
研究发现,多个头得到的内容实际上大差不差,为了降低计算量和KVCache的存储消耗,一个简单的想法就是Q还是多个头,但KV只有一个头,这样就是MQA,多查询自注意力,但是这样性能损失较大
折中一下就是GQA,分组,Q头部数量可以整除KV头部数量
比如在Qwen3-0.6B中,隐藏层维度是1024,单个头部维度是128,Q头部是16,KV的头部是8,这意味着Q的数据是要从1024映射到 2048 = 16 × 128 2048=16\times 128 2048=16×128再进行分头的,KV则是 1024 → 1024 → 8 × 128 1024\to1024\to 8 \times 128 1024→1024→8×128,这是两两一组
SwiGLU
激活函数使用SiLU,再结合门控
优势:
- 更好的梯度流
- 梯度更加平滑
- 负区间不在是0
- 门控机制选择性
- 增加更多复杂性
更多推荐





所有评论(0)