尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

从零手搓大模型:深入拆解Qwen架构与源码实现

发布时间:2026/9/29 1:55:07

资讯中心
01
ARTICLE

从零手搓大模型:深入拆解Qwen架构与源码实现

从零手搓大模型:深入拆解Qwen架构与源码实现
1. 从零手搓大模型我为什么选择Qwen作为第一个研究对象大概是从去年年底开始身边越来越多的朋友问我要大模型学习资料说看完理论还是不知道模型内部到底长什么样。说实话这种情况太正常了。市面上的大模型学习路线、上海交大的《动手学大模型》教程、各种AI大模型入门指南内容质量都不差但大部分都停在一个层次告诉你Transformer是什么、Attention是什么然后就没有然后了。我自己的感觉是学习大模型这件事卡住大多数人的不是理论而是“不知道怎么把理论对应到代码上”。所以我想写这个“从零手搓大模型之路”系列带着大家真正走进一个开源大模型的内部把它拆开揉碎看清楚。系列第一篇选定的对象是Qwen通义千问。为什么选Qwen而不选其他模型理由有三个。第一Qwen是当前中文开源大模型里生态最完整的之一。从0.5B到72B、MoE架构的Qwen3系列覆盖了从个人娱乐到生产部署的全部规模段。你在网上搜“大模型本地部署”“ollama部署大模型”“vllm部署大模型”这类教程Qwen是出场率极高的名字。选择它意味着后续你学到的内容可以无缝迁移到实际部署中。第二Qwen的代码实现非常规矩。它没有像某些模型那样为了刷榜搞各种奇怪的trick而是把当前大模型主流的成熟技术老老实实地用上了GQA、RMSNorm、SwiGLU、RoPE、KV Cache一个不少。读懂Qwen基本上就摸清了当前主流大模型架构的通用范式之后再看LLaMA、Mistral、DeepSeek都会觉得非常亲切。第三也是最重要的一点Qwen系列有各种规格的模型权重而且从0.5B到7B级别的模型对显存需求并不夸张。你可以真的在自己的消费级显卡甚至MacBook上跑起来边看代码边调试这是“手搓”的前提。这篇文章我会按我自己实际研究Qwen的过程来组织先把Qwen的整体架构在脑子里建立一个地图再逐层拆解关键组件的实现最后给出我亲测有效的代码阅读路径和环境配置方案。正文里所有关于源码的解析都基于Qwen2/Qwen2.5系列的官方实现因为Qwen3的MoE版本在架构上做了一些变化属于进阶内容我会在系列后面的文章里单独讲。2. 学习Qwen之前先要在脑子里建立一张架构地图2.1 Qwen的整体结构一个标准Decoder-Only架构很多第一次接触大模型的同学拿到Qwen的代码后第一反应是懵代码目录里一堆文件不知道先看哪个。我自己第一次看的时候也踩了这个坑在配置文件和工具函数里绕了好久最后才意识到应该先从模型的核心类入手。如果你下载的是官方源码找到qwen2/modeling_qwen2.py这个文件里面有一个叫Qwen2ForCausalLM的类这就是整个模型的“外壳”。这个类做的事情可以概括为三件管理一个Qwen2Model实例这是真正的主体网络管理一个lm_head线性层把模型输出的hidden states映射到词表大小的logits在训练和推理时把“预测下一个token”这个任务从数学上封装好包括交叉熵损失的计算逻辑而Qwen2Model内部就是标准的Decoder-Only堆叠结构。它由一个embed_tokens词嵌入层、一组数量相同的DecoderLayer也就是我们常说的Transformer Block堆叠而成最后再接一个norm层RMSNorm。这里有一个很重要的细节早期Transformer的Encoder-Decoder架构现在已经不是大模型的主流了。Qwen这类因果语言模型用的是纯Decoder结构每个token在计算attention时只能看到它之前的token不能看到后面的。这种设计决定了它天然适合做文本生成任务而不是文本理解任务理解任务通常需要双向上下文。为什么大家最后都统一到了Decoder-Only简单说就是效果更好训练更稳定而且在推理时可以通过KV Cache大幅提升速度工程上也更简单。作为一个系列的第一篇我不展开讲这段历史但大家在脑子要记住这个判断。2.2 组件清单Qwen里都有哪些关键零件把Qwen模型比作一台汽车的话DecoderLayer就是发动机。发动机里有哪些主要零件我先把清单列出来Qwen2Attention注意力模块整个模型的核心计算单元Qwen2MLP多层感知机模块负责对每个token的特征做非线性变换input_layernorm注意力前的归一化层post_attention_layernormMLP前的归一化层一个DecoderLayer的工作流程是输入hidden_states先做input_layernorm归一化然后送入注意力层注意力输出与原始输入做残差连接再做post_attention_layernorm归一化送入MLPMLP输出同样做残差连接。两份残差加起来就是这个DecoderLayer的最终输出。这就是大名鼎鼎的Pre-Norm结构。为什么要用Pre-Norm而不是Post-Norm在原始Transformer论文里归一化是放在子层之后的但后来大家发现Pre-Norm在深层网络中训练更稳定梯度不容易爆炸所以现在几乎所有大模型都切到了Pre-Norm。这些细节看起来小但实际上都是决定模型能否训练起来的关键。最终整体结构可以用这样一行伪代码来概括输入tokens - Embedding - [DecoderLayer x N] - RMSNorm - Linear - Logits - Softmax - 下一个token把这个结构刻在脑子里你再看任何模型的代码都是在往这个框架里填补具体实现。2.3 Qwen模型规模的配置哲学从0.5B到72B差异在哪里Qwen系列的另一大学习价值在于它用同一套代码支撑了多个规模的模型。从0.5B、1.8B、7B、14B到72B它们共享同一个Qwen2Config配置类差异完全由几个关键数字控制hidden_size隐藏层维度决定每个token被表示成多少维的向量num_hidden_layersDecoderLayer的层数num_attention_heads注意力头的数量num_key_value_headsKV头的数量GQA的核心参数intermediate_sizeMLP中间层维度以Qwen2.5-7B为例它的配置大致是hidden_size3584num_hidden_layers28num_attention_heads28num_key_value_heads4intermediate_size18944。注意看num_key_value_heads4这就是Qwen使用GQA分组查询注意力的体现。而0.5B模型配置差异极大num_key_value_heads和num_attention_heads都小很多。拿到这些数字可以算一算模型的大概参数量。比如单个attention层的参数量大约是4 * hidden_size * hidden_sizeQ、K、V、O四个矩阵加上MLP的参数量大约3 * hidden_size * intermediate_sizegate、up、down三个矩阵再加embedding是vocab_size * hidden_sizeQwen的词表大小是151936比一般模型大不少大致就能和模型实际参数量对得上。这个计算过程建议大家都自己动手算一遍能极大地帮助理解模型规模从何而来。3. 用一整个章节来讲清楚Qwen的注意力机制3.1 从标准自注意力开始理解Attention到底在算什么注意力机制是所有现代大模型的核心。网上讲Attention的文章已经非常多但我发现很多朋友的问题不是“不理解公式”而是“不知道代码里具体在做什么”。我们先用标准的缩放点积注意力来理解。假设输入的hidden_states是一个形状为[batch_size, seq_len, hidden_size]的张量注意力层要做的事情是通过三个权重矩阵Wq、Wk、Wv把hidden_states分别投影成Query、Key、Value得到三个张量计算Query和Key的点积除以缩放因子sqrt(head_dim)得到注意力分数对注意力分数做Softmax归一化得到每个位置对所有历史位置的注意力权重用注意力权重对Value做加权求和得到输出最后通过Wo矩阵投影回原来的维度用代码表达就是import torch import torch.nn.functional as F def scaled_dot_product_attention(query, key, value, maskNone): d_k query.size(-1) scores torch.matmul(query, key.transpose(-2, -1)) / torch.sqrt(torch.tensor(d_k, dtypequery.dtype)) if mask is not None: scores scores.masked_fill(mask 0, -1e9) attn_weights F.softmax(scores, dim-1) output torch.matmul(attn_weights, value) return output但这里有一个细节Qwen的实际实现并不是直接对完整的hidden_states做矩阵乘法而是先view成多头的形式。具体来说会把hidden_states从[batch_size, seq_len, hidden_size]reshape成[batch_size, seq_len, num_heads, head_dim]然后转置成[batch_size, num_heads, seq_len, head_dim]。为什么要拆成多头因为不同的头可以关注不同维度的信息有的头关注语法关系有的头关注语义相关性多头让模型有机会在不同子空间学习不同的注意模式。3.2 GQAQwen在注意力模块上做的最重要改进如果完全使用标准的多头注意力MHA每个头都会拥有独立的K和V在推理时随着生成的token越来越多KV Cache的占用量会线性增长。对于7B这种规模的模型MHA的KV Cache会轻松吃掉几个GB的显存这在生产环境是难以接受的。Qwen采用的分组查询注意力Grouped Query AttentionGQA是对MHA的折中方案。GQA的做法是让Query保持多个头比如28个但把Key和Value的头数量减少比如只有4个然后每组Query头共享一个KV头。在Qwen的实现中28个Query头被分成4组每组7个Query头对应1个KV头。这直接让KV Cache的占用降到了MHA的七分之一。登录一个真实的模型部署场景使用vllm部署Qwen2.5-7B时如果max_model_len设置为8192MHA的KV Cache需要大约4.6GB而GQA只需要约0.66GB。这个差距是实际且显著的。在你用Ollama做本地部署时GQA也是能让消费级显卡跑起来的重要前提。至于为什么不是MQAMulti-Query Attention所有Query头共用一个KV头MQA的KV Cache更小但实验表明它对模型质量有可感知的损伤。GQA是质量和效率之间的平衡点。这些判断不是我的结论而是当前开源社区的共识了解它你就明白架构设计中处处是取舍。3.3 因果掩码与位置编码的工程实现Qwen作为因果语言模型在注意力计算时必须保证每个位置只能attend到它自己及之前的位置。这个掩码在训练时是通过一个causal_mask矩阵实现的形状为[1, 1, seq_len, seq_len]上三角部分被mask掉。Flash Attention的实现里则直接把is_causalTrue传给底层kernel不需要显式构造掩码矩阵这也是FlashAttention节省显存的原因之一——它不需要把完整的注意力分数矩阵存在显存里。另一个关键组件是RoPE旋转位置编码。位置编码解决的核心问题是Attention计算是基于点积的点积本身对位置是无感的如果不加位置信息“我爱你”和“你爱我”在模型看来很难天然区分。RoPE的思路是不同位置指的是token在序列中的位置对Q和K向量施加不同的旋转角度。Qwen的配置文件里有rope_theta1000000.0和rope_scaling两个参数。rope_theta控制旋转频率的基数更大的rope_theta能支持更长的上下文外推。在Qwen的官方代码中还有一个微妙的处理inv_freq在100维之前没有缩放、在100维之后乘以1/2。这是Qwen为了稳定长上下文训练做的特殊设计在注释里被称为“truncate dimension”。如果你在阅读其他模型的代码时不记得这个细节反而会对不上数值。RoPE的代码实现是比较容易劝退初学者的地方。但我的建议是这个阶段你要能理解RoPE的原理和它解决的问题不必逐行手推它的复数数学形式。等真正需要做长上下文微调时再回来抠细节绝对来得及。4. 归一化与激活函数Qwen里看似简单但不容忽视的零件4.1 RMSNorm为什么Qwen不用LayerNorm传统的Transformer用的是LayerNorm它对输入向量做归一化需要计算均值和方差。Qwen用的是RMSNorm它只做了缩放不减去均值。RMSNorm的计算公式非常简单把输入向量的每个元素平方、求平均、开根号得到RMS然后每个元素除以这个RMS再乘上一个可学习的权重weight。相比LayerNormRMSNorm省去了计算均值的步骤计算开销更小而且实验表明它在大模型训练中效果不输LayerNorm甚至更稳定。Qwen的RMSNorm实现在qwen2/modeling_qwen2.py里调用的是HuggingFace的Qwen2RMSNorm类。代码里有几个细节值得注意eps参数设成了1e-6这是一个很小的常数防止除以零variance_epsilon在配置里叫rms_norm_eps归一化之后还要乘上一个可学习的weight向量具体来说就是self.weight形状与hidden_size一致RMSNorm的一个工程优势是在推理时权重可以提前融合计算减少kernel调用的延迟。在量化场景下RMSNorm的数值稳定性也优于LayerNorm。总之现在的开源大模型基本已经达成了“用RMSNorm”的共识。4.2 SwiGLU激活函数MLP模块为什么是三个矩阵Qwen的MLP模块不是简单的“线性层-激活函数-线性层”两层结构而是采用了SwiGLU激活函数拆成了三个权重矩阵gate_proj、up_proj和down_proj。直观理解up_proj负责把hidden_states映射到更高维的空间intermediate_size通常是hidden_size的2到5倍让模型有更大的特征表达能力gate_proj负责计算门控信号决定每个维度的信息有多少应该通过gate_proj的输出经过SiLU激活函数后与up_proj的输出逐元素相乘最后经过down_proj降维回hidden_size。写成公式或代码是def forward(self, x): return self.down_proj(F.silu(self.gate_proj(x)) * self.up_proj(x))Qwen为什么选择SiLU而不是更经典的ReLU或者GELUSiLU也叫Swish在负区间不是完全截断为零而是保留了一个平滑的负值这让梯度可以更顺畅地流过负值区域训练更稳定。SwiGLU则是这个基础上的门控变体引入门控机制后模型可以更灵活地控制信息的通过程度。如今SwiGLU几乎成了开源大模型的标配你看LLaMA和Mistral用的都是它。4.3 残差连接的作用让数百层网络也能稳定训练大模型动辄几十层甚至上百层如果没有残差连接信号在逐层传递时会不断衰减或爆炸。残差连接的做法非常朴素把输入直接加到子层的输出上让网络学习的是“残差”而不是完整的映射函数。Qwen在每个注意力层和MLP层前后都加了残差连接。这带来一个有趣的推论即使中间层的输出为0信号依然可以通过残差旁路传递到下一层梯度也能通过这条短路回传。这也是为什么堆到几十层还能训练起来的一个重要原因。对于初学者理解残差连接还有一个好处当你调试模型输出时如果你把某一层权重全部置零模型依然能输出一些类词向量的结果这就是残差连接在起作用。动手验证这种小实验比看十篇文章都记得牢。5. 手把手实操把Qwen源码跑起来并逐层调试5.1 环境准备与依赖安装在正式读代码之前我建议你先搭一个能跑通的环境。具体硬件方面0.5B模型只需要大约4GB内存或显存7B模型在CPU上也能跑但速度较慢推荐至少有8GB显存的GPU。我自己的开发机是一张RTX 3090跑7B模型毫无压力。环境建议用Python 3.10以上版本然后安装依赖。pip install transformers4.45.0 torch2.1.0 accelerate sentencepiece需要注意Qwen2模型的tokenizer与现代分词器不完全一样需要用sentencepiece来处理。这里有一个很多新手会踩的坑只安装transformers和torch运行时却报ModuleNotFoundError: sentencepiece然后卡了半天才发现漏装。如果你想要完整复现我的调试过程最好clone一份官方源码到本地git clone https://github.com/QwenLM/Qwen2.git不过更实用的做法是直接在Python中引入已安装包的源码位置transformers库会自带modeling_qwen2.py等文件路径通常在你的Python环境site-packages/transformers/models/qwen2/目录下。提示阅读模型源码我更推荐直接读site-packages里那一份。它经过HuggingFace的适配与AutoModelForCausalLM接口完全对齐你用起来更方便而且它还额外做了cache、device_map等工程上的兼容。5.2 从加载模型到查看参数第一段必须运行的代码环境搭好后先运行下面这段最朴素的代码确认能成功加载模型from transformers import AutoModelForCausalLM, AutoTokenizer import torch model_name Qwen/Qwen2.5-0.5B-Instruct tokenizer AutoTokenizer.from_pretrained(model_name) model AutoModelForCausalLM.from_pretrained( model_name, torch_dtypetorch.float32, device_mapcpu ) print(model)这段代码会输出整个模型的层级结构。你会在输出中看到类似这样的结构Qwen2ForCausalLM( (model): Qwen2Model( (embed_tokens): Embedding(151936, 896) (layers): ModuleList( (0-23): 24 x Qwen2DecoderLayer( (self_attn): Qwen2Attention(...) (mlp): Qwen2MLP(...) (input_layernorm): Qwen2RMSNorm(...) (post_attention_layernorm): Qwen2RMSNorm(...) ) ) (norm): Qwen2RMSNorm(...) ) (lm_head): Linear(in_features896, out_features151936, biasFalse) )注意0.5B模型的embed_tokens输出维度是896是的不同规模的模型hidden_size不同。实际动手查看这个结构对建立“模型就是一堆张量运算”的直觉帮助极大。5.3 用断点调试的方式观察Attention内部计算光看模型结构还远远不够关键是要能看到张量在每一层的形状变化。这是最接近“手搓”的体验。建议你在Pycharm或者VS Code里打开site-packages/transformers/models/qwen2/modeling_qwen2.py在Qwen2Attention.forward的函数入口处打上断点然后运行一段生成代码。你会看到输入张量的形状是[batch_size, seq_len, hidden_size]然后一路观察hidden_states通过q_proj、k_proj、v_proj线性层形状变为[batch_size, seq_len, num_heads * head_dim]经过view和transpose之后变成[batch_size, num_heads, seq_len, head_dim]注意k_proj输出的头数是num_key_value_heads而不是num_heads第3点是理解GQA的关键也是最直观的观察点。你在调试时可以看到Qwen2-7B的query在attention计算时被repeat_kv扩展到了与K、V相同的头数。我自己的经验是不要直接把整个model.generate跑完。先用model(input_ids)单次前向在指定的层里打点查看张量形状配合sizes、shapes这类调试窗口比任何教程都高效。5.4 验证位置编码RoPE的核心逻辑位置编码这部分我建议你调试一下apply_rotary_pos_emb函数。它的代码逻辑是根据seq_len生成一个位置索引序列[0, 1, 2, ..., seq_len-1]结合inv_freq计算出cos和sin值调用rotate_half函数把Q和K的向量一分为二旋转后拼接关于这段代码我踩过的一个坑是手动修改rope_theta后直接用短序列训练好的模型生成长文本效果不佳。这是因为rope_theta变化需要配合相应的位置编码缩放策略比如Yarn或NTK-aware scaling不是简单调大就能外推的。在长上下文微调和推理时要格外注意这也是很多人在模型部署、微调中遇到“效果变差”的隐藏原因。具体的数学原理我在后面写长上下文文章时再展开。5.5 实际动手用CPU跑通一次完整的文本生成环境、代码、调试都搞定之后跑通完整生成流程是检验理解的好方法。代码如下prompt 大模型架构的核心组件包括 inputs tokenizer(prompt, return_tensorspt) outputs model.generate( inputs.input_ids, max_new_tokens50, do_sampleFalse, ) print(tokenizer.decode(outputs[0], skip_special_tokensTrue))用0.5B模型在CPU上这一步通常几秒就能完成。实际操作时建议你设置断点观察generate内部是如何一步步调用模型的先是单次前向得到logits然后取最后一个位置用softmax或贪心策略选出最有概率的token拼接回输入再继续生成。这个循环本质上就是所有大模型在推理时最主要的工作模式。当你亲手看到这个循环跑起来之后下一步想了解KV Cache、vllm部署、LoRA微调时就有了坚实的基础。因为你已经知道了推理速度慢是因为每生成一个token都要重新计算前面所有token的attention。6. 我踩过的坑与给你的避坑建议6.1 版本兼容性问题transformers版本太老或太新都会出事Qwen2系列模型对transformers版本是有要求的。太老的版本比如4.30以下根本不知道Qwen2这个模型类加载时会报错说找不到匹配的模型架构太新的版本有时候也会因为库内部重构出现一些兼容性警告虽然通常不影响使用但会让人心里没底。我的建议是把transformers锁定在4.45系列这是目前最稳的版本区间。无论你是用AutoModelForCausalLM.from_pretrained加载还是准备后续做微调和部署这个版本踩坑最少。另一个常见问题是torch_dtype设定为float16在CPU上会报错。因为CPU上的某些算子不支持半精度计算最简单的解决办法就是CPU上用float32GPU上再用float16或bfloat16。这个细节在5.2的代码里我已经体现出来了。6.2 初学阶段最容易走偏的三个方向根据我带过不少新人的经验初学大模型架构时最容易走偏的方向有三个。第一个是陷入数学细节不能自拔。比如非要把RoPE的复数推导手推一遍或者把FlashAttention的前向反向原理完全搞透。这些内容值得学但绝对不该在你还没跑通模型时花大量时间。先建立整体认知再逐个突破难点效率会高上一倍。第二个是过度依赖别人的教程而不看源码。网上各种大模型学习资料非常多但很多二手资料在传播过程中会失真甚至出现错误。源码就是最权威的文档当你对某个实现有疑问时永远应该先回到源码去验证。第三个是只跑代码不总结。读完代码之后一定要动手画一遍结构图或者写一篇笔记。写笔记的过程就是检验你理解程度的过程。如果你发现自己写不清楚某个组件说明这个组件你还没吃透回去再看。6.3 关于“手搓大模型”的正确预期最后说一下我对“手搓”这个词的理解。很多人以为“从零手搓大模型”是让你从线性代数开始手写一个完整的大模型训练脚本我觉得这不是合理的学习路径。以现在大模型的复杂程度哪怕是7B级别的模型从零手写训练代码且训练出有意义的模型对个人开发者来说几乎不可能。我更推荐的路径是把开源模型当“乐高积木”理解每一块积木的结构和功能尝试修改一部分积木观察整体的影响然后逐步走向微调、部署和定制。这个系列后续的文章也会沿着这条路径展开先讲清楚架构再讲数据准备然后是LoRA微调实战这也可以直接对接你搜到的各种“lora微调实战教程qwen”内容、量化部署、vllm服务化最后是RAG应用开发。其实做这件事对我的个人收获还有一个意外之喜搞懂模型架构之后再看那些大模型相关的新闻、论文和技术讨论眼里看到的不再是空洞的概念而是具体的计算图。你会觉得那些八股文一样的模型介绍突然都变成了一种你可以验证、甚至能质疑的东西。这大概就是“手搓”带来的最大的底气。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。