概述

从零构建大模型 中构建了一个GPT2,于是就想着照着这个拓展到更新的模型,比如Qwen3(虽然现在Qwen3.5都出了)

这个系列的目标首先是自己用Torch搭建一个Qwen3模型,实现推理,然后未来会实现KVCache、手写CUDA算子等

项目链接:qwen3_from_scratch

从零开始写Qwen3目录

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 102410248×128,这是两两一组

SwiGLU

激活函数使用SiLU,再结合门控
优势:

  1. 更好的梯度流
    1. 梯度更加平滑
    2. 负区间不在是0
  2. 门控机制选择性
    1. 增加更多复杂性
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐