手搓Qwen模型架构:从Transformer核心组件到最小实现
发布时间:2026/9/29 4:49:24来源:尧图网络
刷了整整两个月大模型的公众号文章、看了无数张架构图结果关掉手机还是说不清楚Qwen和Llama到底差在哪。这种挫败感估计每个想认真入坑大模型的人都经历过。我自己的转折点是某天准备动手做LoRA微调时连config.json里num_key_value_heads这个字段都没看懂那一瞬间我意识到不把模型架构这一层彻底啃透后面什么微调、部署、RAG全都是在摇摇晃晃的地基上盖楼。所以我开了从零手搓大模型之路这个系列把学习Qwen模型架构的全过程、读源码的方式、踩过的理解误区一条条记录下来。这篇是这个系列的第一篇目标只有一个让完全没碰过Transformer源码的人也能彻底读明白Qwen的骨架。1. 为什么我把学习大模型架构的第一站定在Qwen1.1 大模型那么多选谁当解剖对象其实是个策略问题想学大模型架构可选的模型太多了Llama系列、Mistral、Baichuan、DeepSeek、GLM、Qwen……每一个都有自己的拥趸。但如果从零基础入门动手实践这个角度出发Qwen可以说是学习性价比最高的选择。我先说一下当初选型时的思考路径。第一Qwen是完全开源开放的权重、代码、技术报告、配置文档一样不缺HuggingFace上直接能下到各种尺寸的模型完全没有只能看论文不能碰代码的尴尬。第二Qwen的架构很主流它没有搞什么特别冷门的自定义模块而是把当代大模型的主流设计——RMSNorm、RoPE旋转位置编码、SwiGLU激活函数、GQA分组查询注意力——全部吸收了进来。这意味着你学会Qwen的架构就等于掌握了当前绝大多数开源大模型的基础骨架。第三Qwen的中文资料极其丰富无论是官方文档还是社区博客质量都很高。比起硬啃英文论文和英文issue中文资料的消化速度至少快一倍。1.2 从Qwen各版本能看到一条清晰的模型演进脉络还有一点很关键Qwen这个模型家族本身就是一个很好的教材。Qwen1、Qwen1.5、Qwen2、Qwen2.5一路演进下来你能看到架构师们在哪些地方做了取舍。比如Qwen1时代它采用的是类似GPT系列的经典decoder-only结构到Qwen2开始全面采用GQA来优化推理时的显存占用和速度Qwen2.5则更多在数据质量、训练策略和上下文长度上做文章架构层面的改动反而不大。这种演进轨迹几乎是这两年大模型架构发展史的缩影——从追求效果的堆参数到兼顾推理效率的工程优化再到数据和训练的精细化打磨。所以在学Qwen架构时我实际上是站在一个横截面上同时看到了时间线。这也是我会把Qwen作为学习对象的重要原因。下面我会从头到尾把Qwen2模型架构的关键模块逐个拆开来讲。2. Qwen2整体骨架从config.json读出的信息量2.1 建议先看config.json而不是直接扑进源码很多朋友学模型架构第一件事就去啃model.py源码这其实是走弯路。几百行甚至上千行的代码夹杂着各种缓存逻辑、分布式处理、特殊分支初学者看三页就懵了。我的经验是先读模型的config.json先把这模型到底长什么样搞清楚再去看代码就顺多了。我以Qwen2-7B为例把config.json里最关键的一些字段整理成了下表config字段Qwen2-7B的取值含义hidden_size3584隐藏层维度即每个token向量化后的长度num_hidden_layers28堆叠的decoder层数量num_attention_heads28注意力头的数量num_key_value_heads4K和V的头数也是GQA的核心参数intermediate_size18944FFN中间层的维度通常是hidden_size的4倍以上vocab_size151936词表大小max_position_embeddings32768预训练时设置的最大位置长度rms_norm_eps1e-6RMSNorm中的epsilonrope_theta1000000.0RoPE的旋转基础频率use_sliding_windowfalse是否使用滑动窗口注意力看到这张表你应该已经有感觉了一个Transformer decoder模型本质上就是一堆配置参数的组合。hidden_size决定了整个网络有多宽num_hidden_layers决定了网络有多深num_attention_heads和num_key_value_heads则决定了注意力机制怎么切。2.2 decoder-only结构一个不断翻译自己的循环Qwen2的宏观结构可以用一句话概括Embedding层 28个完全相同的Decoder Layer堆叠 最后的RMSNorm LM Head输出层。整个结构里没有Encoder这是decoder-only架构最核心的特征。为什么GPT、Qwen这些模型都从encoder-decoder转向了decoder-only这背后的原因有几个。第一decoder-only架构本身就是一个通用的条件概率建模器天然适合做生成式任务输入一段文本模型就持续预测下一个token逻辑非常干净。第二模型的能力可以靠规模效应硬堆出来decoder-only结构在同等参数量下表现更好。第三在推理阶段decoder-only有天然的缓存优化空间所有历史token的K和V都可以缓存下来不用重复计算这就是后面会提到的KV Cache的基础。我当初学到这里最大的感触是不要老想着模型内部有什么神秘魔法它本质上就是个非常高维的、层层加工的翻译机——每一层都在对上一层的特征表示做精炼和抽象把纯粹的token序列逐步翻译成蕴含语义和语法的隐藏表示最后接一个Softmax输出层得到下一个token的概率分布。2.3 和经典Transformer相比Qwen2改了啥如果拿Qwen2和2017年谷歌那篇经典的Transformer论文中的decoder模块做对比你会发现把LayerNorm换成了RMSNorm去掉了均值中心化简化计算的同时训练更稳定把绝对位置编码换成了RoPE旋转位置编码让模型能更好地外推到更长序列把Attention从MHA换成了GQA大幅减少KV Cache的显存占用把FFN中的ReLU换成了SwiGLU提升了非线性表达能力移除了所有bias项进一步简化参数这些都是常识之外的细节但恰恰是这个时代模型架构的关键。后面的章节我逐个展开。3. 四个核心组件拆解RMSNorm、RoPE、GQA、SwiGLU3.1 RMSNorm省掉均值计算的穷人版LayerNormQwen2用的归一化方案是RMSNormRoot Mean Square Normalization。要理解RMSNorm得先理解归一化在大模型里到底是干嘛的。神经网络在层层传递的过程中数据的分布会发生偏移数值可能会变得过大或者过小导致训练不稳定、收敛缓慢。归一化的作用就是让每一层的输出都能保持在一个相对稳定的尺度上。经典Transformer用的是LayerNorm它会把每个token的隐藏向量减去均值、除以标准差得到标准正态分布。RMSNorm做了个大胆的简化直接去掉减去均值的步骤只保留除以RMS(均方根)这一步。公式长这样RMS(x) sqrt(mean(x^2) eps) RMSNorm(x) x / RMS(x) * gamma其中gamma是一个可学习的缩放参数eps是防止分母为0的极小值。Qwen2的config里rms_norm_eps就是它。一开始我觉得这很反直觉减去均值不是归一化的关键环节吗后来跑实验才明白RMSNorm省掉它之后性能和LayerNorm基本持平但因为少了一次均值计算和一次向量减法计算量降低了一些。在几十亿参数的模型里这种计算量的节省是实打实的。它用一个极小的精度代价换取了可观的训练和推理速度提升。3.2 RoPE旋转位置编码让模型知道token的先后顺序Transformer本身是无序的它接收的是一堆向量如果没有任何位置信息模型会把我爱你和你爱我当成完全一样的东西。所以必须显式地把位置信息注入进去。早期BERT用的是绝对位置编码给每个位置分配一个固定向量后来有人尝试相对位置编码让模型学习两个token之间的距离而Qwen系列用的RoPERotary Position Embedding旋转位置编码是一个更聪明的方案。RoPE的核心思想非常巧妙把位置信息通过旋转矩阵的方式直接注入到Q和K向量里。就好比给每个token的向量加了一个旋转角度位置越靠后旋转角度越大。当模型计算Q和K的点积时两个向量之间的夹角差就自然携带了它们的相对位置信息。具体实现时RoPE把向量的相邻两个维度组成一个二维平面在这个平面上做旋转。对于hidden_size3584的Qwen2-7BQ向量有3584个维度会被分成1792个二维平面每个平面使用不同的旋转频率。低频维度负责捕捉长距离的依赖关系高频维度负责捕捉短距离的局部关系这就是RoPE能支持长文本的重要原因。用代码看的话RoPE的核心逻辑很简洁def precompute_rope_frequencies(dim, max_seq_len, theta10000.0): # 为每个二维子空间计算基础频率 inv_freq 1.0 / (theta ** (torch.arange(0, dim, 2).float() / dim)) # 生成位置序列 [0, 1, 2, ..., max_seq_len-1] positions torch.arange(max_seq_len).float() # 计算每个位置、每个子空间的旋转角度 freqs torch.outer(positions, inv_freq) cos torch.cos(freqs) sin torch.sin(freqs) return cos, sin def rotate_half(x): # 将输入沿最后一个维度分成两半并交换 x1, x2 x.chunk(2, dim-1) return torch.cat((-x2, x1), dim-1) def apply_rotary_pos_emb(q, k, cos, sin): # 将旋转位置编码应用到Q和K上 return q * cos rotate_half(q) * sin, k * cos rotate_half(k) * sin注意Qwen官方实现里光这一小块逻辑就有很多工程细节比如用view操作而不是chunk来配对相邻维度本质上都是为了让旋转的方向符合预期。我建议自己动手写一遍只看不写是记不住的。3.3 GQA分组查询注意力内存和效果之间的黄金平衡点这一块是Qwen2架构里我认为最值得展开讲的部分因为它是大模型从能跑走向好用的关键工程创新。经典的MHAMulti-Head Attention多头注意力里每个注意力头都有自己独立的Q、K、V矩阵。比如28个头就有28份K和V。这带来的问题很直接推理的时候每个token的K和V都要缓存下来供后续token使用这份缓存叫KV Cache。头数越多KV Cache占用显存越大。在长上下文场景下KV Cache甚至会成为显存瓶颈。后来出现了MQAMulti-Query Attention让所有Q头共享一组K和V显存是下来了但效果损失比较大。GQAGrouped Query Attention分组查询注意力走的是中间路线把Q头分成若干个组每个组内共享一组K和V。拿Qwen2-7B举例num_attention_heads28num_key_value_heads4也就是说28个Q头分成4组每组7个头共享一份K、V。这既保留了多头的表达能力又把KV Cache的显存占用降低到MHA的1/74/28效果损失却很小。很多讲GQA的教程就停在这了我要补一个更直观的计算。假设序列长度为2048hidden_size3584每个K/V头的维度是1283584/28。如果采用MHAKV Cache单层每个token的显存占用是2×3584×2字节bfloat1614KB28层下来非常可观。采用GQA之后K/V参数总量直接除以7单层KV Cache每个token降到2×4×128×2字节2KB。这个节省在长序列推理时直接决定了你显卡能不能跑起来。3.4 SwiGLU激活函数比ReLU强在哪Qwen2的FFN前馈网络部分用的激活函数不是早期Transformer的ReLU而是SwiGLUSwish-Gated Linear Unit门控线性单元。经典的FFN结构是先通过一个线性层把hidden_size扩展到intermediate_size过ReLU再通过另一个线性层压缩回来。SwiGLU做了个改造把流向扩展后的向量分成两路一路过Swish激活函数另一路作为门控直接乘上去。公式大概是SwiGLU(x) Swish(xW1) * (xW2) * W3其中Swish(x) x * sigmoid(x)。用生活化的语言理解门控机制就像双联开关一个信号控制另一个信号能否通过。对比ReLU直接把负数清零SwiGLU里的门控是一路模拟信号能更平滑地控制信息的流通量梯度能更好地回传。当然不是没有代价——SwiGLU引入了额外的一组参数W2模型参数量会变大一点。这也是为什么intermediate_size的设定不能太大否则参数量会爆炸。4. 一条数据的完整旅行前向传播的维度流转全记录4.1 从Token到Embedding文本变成了数字坐标架构看的是静态结构但真正理解一个模型必须看数据在它身体里是怎么流动的。这一章我用一个具体的例子说清楚维度流转非常有助益。现在想象我们给Qwen2-7B输入一句话你好中国的AI发展很快假设分词后得到20个token。模型内部发生的流程是这样的第一步这20个token的ID会去查Embedding表。这个Embedding表的大小是vocab_size151936 × hidden_size3584整个表大约有5.4亿个参数。查完表后我们得到一个张量形状是 [batch_size1, seq_len20, hidden_size3584]。到这里文本已经变成了计算机能处理的坐标向量。4.2 在28个Decoder Layer中层层精炼这个 [1, 20, 3584] 的张量接下来要依次穿过28个Decoder Layer。每个Decoder Layer的内部流程几乎一样先过RMSNorm对每个token的3584维向量做归一化进入注意力模块生成Q、K、V向量其中Q有28个头的投影K和V只有4个头GQA投影后Q的形状是 [1, 20, 28×128][1, 20, 3584]K和V的形状是 [1, 20, 4×128][1, 20, 512]。随后K和V会复制扩展成和Q一样的头数28并施加RoPE位置编码计算注意力Q和K做点积得到注意力分数矩阵 [1, 28, 20, 20]表示每个token对其它所有token的关注程度经过softmax归一化后与V相乘得到加权后的表示注意力输出经过一个线性投影恢复到hidden_size再与输入做残差连接Residual Connection得到 [1, 20, 3584]进入MLP模块把hidden_size扩展成intermediate_size18944过SwiGLU激活函数再压缩回3584同样做残差连接这28层做完之后每个token的向量都经过了充分的上下文融合。这个融合过程非常重要——它让模型知道了每个词在当前语境下的准确含义比如苹果在这句话里是水果还是手机品牌。4.3 最后的输出层每个候选词拿到一个分数最后一层Decoder Layer输出的 [1, 20, 3584] 张量会再过一次RMSNorm然后进入LM Head输出层。LM Head本质上就是一个线性层把3584维映射回151936维的词表空间。输出的形状是 [1, 20, 151936]。这个输出就是logits通俗讲就是模型对词表里每个词打出的分数。要让分数变成概率需要经过softmax归一化。做生成时Qwen2会做两件事一是只关注最后一个token对应的151936维logits因为每一个新token的预测只看已经生成的全部上下文二是把概率分布按温度系数缩放后做采样或贪心解码选出一个词作为下一个token然后把它拼到输入末尾继续下一轮。这就是整个大模型生成文字的本质。4.4 中间还要注意Causal Mask因果掩码我在上面刻意忽略了注意力矩阵的一个关键细节Qwen2是decoder-only模型在计算注意力分数时每个token只能看到它之前的token不能看到它后面的token否则就作弊了。所以注意力分数矩阵 [1, 28, 20, 20] 在softmax之前会把上三角部分全部置为负无穷。这就是Causal Mask也叫因果掩码。它对保序学习至关重要——如果模型能看到未来的词那训练和生成之间就有巨大偏差。有一说一我刚开始看这里时没太在意直到自己调试时发现输出结果混乱才意识到mask这一步不能省。5. 手搓一个最小可运行版Qwen5.1 从零写核心模块的取舍思路你必须承认真正的Qwen2-7B是3584宽度、28层深度、5.4亿词表Embedding的巨兽本地没几张大显存卡根本跑不动推理。但如果只是为了理解架构我们完全可以用最小配置手搓一个可以运行的缩小版——hidden_size设成128、层数设成4、注意力头8个、KV头2个词表大小设成1000。它依然具备Qwen2的全部核心组件逻辑但参数量只有几百万CPU都能跑。我管这个叫教学版Qwen。下面这些代码你保存成一个Python文件就能跑通前向传播。我没有把全部代码贴完只把最关键的部分展示出来但足够说明实现逻辑。RMSNorm实现class RMSNorm(nn.Module): def __init__(self, dim, eps1e-6): super().__init__() self.weight nn.Parameter(torch.ones(dim)) self.eps eps def forward(self, x): rms torch.sqrt(x.pow(2).mean(-1, keepdimTrue) self.eps) return x * rms * self.weight注意力部分含GQAclass GroupedQueryAttention(nn.Module): def __init__(self, hidden_size, num_heads, num_kv_heads): super().__init__() self.num_heads num_heads self.num_kv_heads num_kv_heads self.head_dim hidden_size // num_heads self.q_proj nn.Linear(hidden_size, num_heads * self.head_dim, biasFalse) self.k_proj nn.Linear(hidden_size, num_kv_heads * self.head_dim, biasFalse) self.v_proj nn.Linear(hidden_size, num_kv_heads * self.head_dim, biasFalse) self.o_proj nn.Linear(hidden_size, hidden_size, biasFalse) def forward(self, x, cos, sin): bsz, seq_len, _ x.shape q self.q_proj(x).view(bsz, seq_len, self.num_heads, self.head_dim) k self.k_proj(x).view(bsz, seq_len, self.num_kv_heads, self.head_dim) v self.v_proj(x).view(bsz, seq_len, self.num_kv_heads, self.head_dim) # 施加RoPE q, k apply_rotary_pos_emb(q, k, cos, sin) # GQA核心把KV头数复制扩展到和Q头数一致 k k.repeat_interleave(self.num_heads // self.num_kv_heads, dim2) v v.repeat_interleave(self.num_heads // self.num_kv_heads, dim2) # 转置为 [batch, heads, seq, head_dim] q q.transpose(1, 2) k k.transpose(1, 2) v v.transpose(1, 2) # 缩放点积注意力 因果掩码 scores torch.matmul(q, k.transpose(-2, -1)) / math.sqrt(self.head_dim) mask torch.triu(torch.ones(seq_len, seq_len, devicex.device), diagonal1).bool() scores scores.masked_fill(mask, float(-inf)) attn torch.softmax(scores, dim-1) out torch.matmul(attn, v) out out.transpose(1, 2).contiguous().view(bsz, seq_len, -1) return self.o_proj(out)MLP部分SwiGLUclass SwiGLUFFN(nn.Module): def __init__(self, hidden_size, intermediate_size): super().__init__() self.gate_proj nn.Linear(hidden_size, intermediate_size, biasFalse) self.up_proj nn.Linear(hidden_size, intermediate_size, biasFalse) self.down_proj nn.Linear(intermediate_size, hidden_size, biasFalse) def forward(self, x): # Swish门控 gate self.gate_proj(x) up self.up_proj(x) swish_gate gate * torch.sigmoid(gate) return self.down_proj(swish_gate * up)5.2 组装Decoder Layer并验证前向传播有了上面这些组件组装一个Decoder Layer就非常简单了class DecoderLayer(nn.Module): def __init__(self, hidden_size, num_heads, num_kv_heads, intermediate_size): super().__init__() self.input_layernorm RMSNorm(hidden_size) self.self_attn GroupedQueryAttention(hidden_size, num_heads, num_kv_heads) self.post_attention_layernorm RMSNorm(hidden_size) self.mlp SwiGLUFFN(hidden_size, intermediate_size) def forward(self, x, cos, sin): # 注意力子层残差连接 x x self.self_attn(self.input_layernorm(x), cos, sin) # FFN子层残差连接 x x self.mlp(self.post_attention_layernorm(x)) return x整个最小Qwen模型class MiniQwen(nn.Module): def __init__(self, vocab_size, hidden_size, num_layers, num_heads, num_kv_heads, intermediate_size, max_seq_len): super().__init__() self.embed_tokens nn.Embedding(vocab_size, hidden_size) self.layers nn.ModuleList([ DecoderLayer(hidden_size, num_heads, num_kv_heads, intermediate_size) for _ in range(num_layers) ]) self.norm RMSNorm(hidden_size) self.lm_head nn.Linear(hidden_size, vocab_size, biasFalse) # 预计算RoPE参数这里按max_seq_len进行实际推理时可动态扩展 cos, sin precompute_rope_frequencies(hidden_size, max_seq_len) self.register_buffer(cos, cos) self.register_buffer(sin, sin) def forward(self, input_ids): x self.embed_tokens(input_ids) for layer in self.layers: x layer(x, self.cos[:input_ids.size(1)], self.sin[:input_ids.size(1)]) x self.norm(x) logits self.lm_head(x) return logits写完之后用随机初始化的方式跑一遍model MiniQwen( vocab_size1000, hidden_size128, num_layers4, num_heads8, num_kv_heads2, intermediate_size512, max_seq_len64 ) input_ids torch.randint(0, 1000, (1, 16)) logits model(input_ids) print(logits.shape) # 期望输出: torch.Size([1, 16, 1000])只要能输出 [1, 16, 1000]说明前向传播的维度流转完全正确你这个手搓模型就从原理上跑通了。5.3 这个最小版和真实Qwen2还有多大差距我把教学版和真实Qwen2的差异列一下如果你后续拿真实模型做实验会有用真实Qwen2用的是完整的bfloat16混合精度训练教学版用float32真实Qwen2的注意力里有KV Cache逻辑教学版没有每轮都是全量计算真实Qwen2的RoPE实现做了很多工程优化比如针对不同头部维度的freqs计算教学版是标准实现真实Qwen2在长序列推理时会有位置编码外推和NTK缩放,教学版没有真实Qwen2的Embedding和LM Head做了参数共享tie_weights有些配置会开启这个选项但骨架是完全一致的。你能把教学版完全吃透再去看HuggingFace的transformers源码会觉得非常亲切因为每一步都能对应上。6. 架构学完后的下一步从看懂走向会用6.1 建议按这个顺序继续往下走学完架构很多人会陷入一个迷茫期觉得我明明看懂了但还是不知道能干嘛。这时候我建议按下面的路径往下走先跑通一次真实Qwen2的推理哪怕是最小的0.5B模型配上Ollama或transformers体验一下模型真正跑起来的感觉同时观察显存占用然后做一次LoRA微调不用拿多大规模的数据集训练一个能输出的模型即可。微调时的很多概念比如哪些层被冻结、哪些层被训练都和架构知识直接相关再尝试用vLLM部署观察prefill和decode两个阶段的行为差异这能加深你对KV Cache的理解最后可以往RAG、Agent方向拓展6.2 后续系列会在哪些方向上展开作为系列的开篇这篇文章其实只在做地基。后续我会重点从两个方向继续深入一是KV Cache和推理优化这属于从架构到工程实现的关键桥梁二是在Qwen2架构基础上怎么做LoRA微调可以回答学了架构之后怎么让模型学会新知识这个问题。6.3 给零基础读者的一些学习心得最后再分享几个我在学习过程中踩出来的心得第一不要试图一次看懂所有代码。你先用本节最前面的表格把config.json理解透然后把模型拆成Embedding、Decoder Layer、LM Head三个部分分而治之。每次只吃透一个小模块效果比通读源码强十倍。第二一定要把代码跑起来。哪怕就是在Colab上用CPU跑一遍MiniQwen的前向传播都会让你对架构的理解提升一个量级。代码不运行永远只是感觉懂了。第三善用print大法。在模型前向传播的各个阶段打印张量shape你会非常直观地看到维度怎么流转。某些时候你还能用einops来调试维度它比你手工reshape要稳得多。第四先用小模型做实验。想复现GQA的KV Cache节省效果用0.5B模型在消费级显卡上就能测出来没必要一上来就上7B甚至是32B。小模型迭代快、踩坑成本低更适合反复实验。在真实项目里跑Qwen2.5的时候我对架构是根基这个判断感受越来越深。光会API调用出了问题只能干瞪眼懂一点架构起码知道该往哪个方向排查。这篇先聊到这儿下一步我打算继续更新KV Cache和推理优化相关的内容咱们下篇见。
网站建设高端定制企业官网