1. 从调API到啃源码:为什么非要看懂大模型底层架构
先聊个现象。这两年大模型火得不行,身边很多人都在用,调个OpenAI的接口、跑个开源模型推理、拿LangChain拼个Agent,看起来都挺顺手。但真到了要自己做点东西的时候——比如想优化推理速度、想搞清楚为什么显存老是爆、想微调一个自己的模型——就傻眼了,只能到处搜教程,搜到的还都是"用某某框架一键搞定"的黑盒方案。
我自己也经历过这个阶段。最早接触大模型衍生产品时,我也是个纯调包侠,pip install一下,写两行代码,跑通demo就觉得自己会了。直到有一次,公司要做一个大模型相关的功能优化,需要控制单次请求的延迟和显存占用,我发现我对模型内部到底发生了什么一无所知——它为什么吃这么多显存?为什么同一个模型在不同框架下性能差那么多?我连从哪儿看起都不知道。就是从那次开始,我意识到:如果不懂底层架构,不懂源码,你就永远只能当一个"使用工具的人",而不是"制造工具的人"。
这篇文章想做的一件事,就是从大模型的底层架构出发,做一次源码层面的深度拆解。我不会只讲概念,而是把注意力机制、位置编码、前馈网络这些核心组件落实到代码层面,再带你走一遍训练、微调、推理部署的全链路源码逻辑,最后聊一些延伸的方向和应用场景。无论你是正准备入行大模型方向的开发者,还是已经会调API但想深入底层的工程师,这篇文章都能给你一条可执行的路径。
文章涉及的代码和思路,主要基于目前主流的开源大模型架构,像Transformer家族、可能的MoE结构、以及一些主流训练推理框架的具体实现。你会发现很多所谓的"高深概念",落到代码里其实就那几行,理解了底层之后,上面那些花活全是衍生品。
2. 模型结构源码拆解:从Embedding到输出层,逐行看Transformer
大模型基本都是Transformer架构,不管是最早的GPT系列,还是后来各种开源模型,核心骨架没变过。所以源码拆解第一步,就是把Transformer的每一个组件在代码里对应起来。
2.1 输入层:Token、Embedding、位置编码
先看输入侧。模型不能直接吃原始文本,得先把文本切成token,再把token映射成向量。这个过程在源码里分三步:
第一步是分词。中文文本切token跟英文不一样,英文按空格切就行,中文不行,得用专门的tokenizer,比如BPE、SentencePiece这些算法。词表大小通常在几万到十几万之间,比如常见的8万词表。
第二步是Embedding。这一步就是查表,词表里每个token对应一个向量。代码实现成一个矩阵,形状是[vocab_size, hidden_size],比如7B模型hidden_size通常是4096。输入token的id去查这张表,拿到对应的向量表示。
第三步是位置编码。因为Transformer的自注意力机制本身不关心词的顺序,把句子里的词打乱,注意力计算出来的结果是一样的,所以必须把位置信息加进去。两种主流做法:
- 绝对位置编码:BERT用的正弦位置编码,或者GPT系列的Learned Positional Embedding,直接学一个位置向量表。
- 相对位置编码:现在很多大模型用的是RoPE(旋转位置编码),它不直接把位置加在向量上,而是通过旋转矩阵把位置信息编码进去。这个对长文本更友好,外推能力好,所以LLaMA、很多国内开源模型都用了RoPE。
看源码的时候,我建议先找到模型的forward函数入口,从输入经过Embedding、位置编码,再到各个模块的顺序走一遍,后面就不会迷路。
2.2 注意力机制的源码级理解:Q、K、V到底在算什么
自注意力是整个Transformer的核心,也是最值得花时间吃透的部分。公式大家都知道:
Attention(Q, K, V) = softmax(QK^T / sqrt(d_k)) V但公式背会了,代码里是怎么写的呢?我贴一段核心逻辑的伪代码,和真实实现很接近:
def self_attention(x, W_q, W_k, W_v, W_o): # x: [batch_size, seq_len, hidden_size] q = x @ W_q # [batch_size, seq_len, num_heads * head_dim] k = x @ W_k v = x @ W_v # 拆成多头 q = q.view(batch_size, seq_len, num_heads, head_dim).transpose(1, 2) k = k.view(batch_size, seq_len, num_heads, head_dim).transpose(1, 2) v = v.view(batch_size, seq_len, num_heads, head_dim).transpose(1, 2) # 计算注意力分数 scores = q @ k.transpose(-2, -1) / math.sqrt(head_dim) # scores: [batch_size, num_heads, seq_len, seq_len] # mask处理,因果模型要盖住未来位置 if causal_mask: scores = scores.masked_fill(mask == 0, float('-inf')) attn_weights = torch.softmax(scores, dim=-1) output = attn_weights @ v # [batch_size, num_heads, seq_len, head_dim] # 多头合并 output = output.transpose(1, 2).contiguous().view(batch_size, seq_len, hidden_size) output = output @ W_o return output整个过程讲直白一点:Q是你"想找什么"的查询向量,K是每个位置"我能提供什么"的标识向量,V是每个位置"真正给出什么"的内容向量。Q和K做点积,算的是"当前位置跟其他所有位置的匹配程度",除以根号d_k是为了防止分数过大把softmax推到饱和区,梯度就没了。匹配度高的位置,注意力权重就大,V的信息就会被更多地带出来,这就是"模型在文本里找关联"的本质。
多头注意力为什么要多头?一个头只能关注一种关系模式,比如语法关系、指代关系、语义相似度,这些是不同维度的关联。8个头、32个头,就是让模型并行学多种关系模式,最后拼在一起。源码里就是q、k、v分别拆成多个块,各算各的,最后concat。
Attention部分特别需要小心的是因果mask。生成模型(GPT系列)是自回归的,预测第i个token的时候不能看到后面的token,所以注意力分数矩阵的上三角要被mask掉,置为负无穷,softmax之后那些位置就归零了。有些初学者在改模型结构的时候会把这个mask搞丢,结果训练时模型"偷看"了未来信息,指标虚高,一上线就露馅。这种问题如果能看懂源码就能避免。
2.3 前馈网络与归一化:Transformer里容易被忽略但同样关键的部分
注意力算完,每个token得到一个新的向量表示,但这个向量是"综合了上下文"的信息。接下来要过一个前馈网络(FFN),通常是两层线性变换加激活函数:
FFN(x) = GELU(xW1 + b1) W2 + b2中间维度一般是hidden_size的4倍,比如hidden为4096,中间就是11008左右(LLaMA的配置)。这一步的意义可以理解成:注意力负责"收集信息",FFN负责"消化信息"。每个token独立过同一个FFN,把所有注意力收集来的信息做一次非线性变换,增加模型的表达能力。
这里有个细节,最近很多模型用SwiGLU或者类似变体替代了传统GELU,效果更好。它不是简单的激活函数,而是对输入做了门控,类似LSTM里"门"的思路。所以你在看不同模型的源码时,FFN层结构会有差异,但整体位置和作用是固定的。
归一化(LayerNorm/RMSNorm)一般夹在注意力之前或之后。现在主流是Pre-LN结构,也就是先归一化再做注意力/FFN,这样训练更稳定。有个细节是RMSNorm——它不带均值中心化,只做缩放,计算量更小,效果却不差,现在很多模型都用它。不理解差别的读者,只要记住RMSNorm是LayerNorm的轻量变体就行。
3. 训练源码逻辑:数据、损失函数、分布式并行是如何协作的
模型结构只是骨架,要让它"会说话",得靠训练。训练源码涉及的东西比模型结构多得多,我挑几个最常遇到、也最关键的部分。
3.1 数据管线:Tokenize、批次组织、注意力掩码
训练大模型的第一件事是准备数据,源码里这部分很容易被忽略,因为不动模型结构,但数据质量直接决定模型效果。数据管线一般做这几件事:
- 清洗和过滤:去掉重复数据、低质量内容、敏感信息,质量比数量重要。很多开源模型的论文里都提到数据清洗策略,比如按困惑度过滤、按文档长度过滤。
- Tokenize:把文本变成id序列。注意要处理超长文本的截断策略——截头还是截尾?这个影响很大。
- 拼接打包:短文本拼起来凑满一个序列,避免浪费算力。
- 生成标签:自回归模型的标签是"预测下一个token",所以对第i个位置,标签就是第i+1个token。源码里一般把输入和标签做一次左移/右移就得到了。
数据这块还有一个指标"样本效率",同样的数据量,数据管线设计得好,训练出来的模型效果能差一大截。我见过很多团队,模型结构用别人的,就换换数据,效果就有明显提升,原因主要在这里。
3.2 损失函数:交叉熵与标签偏移
大模型训练几乎都用交叉熵损失。源码层面就是:
logits = model(input_ids) # [batch_size, seq_len, vocab_size] loss = cross_entropy(logits[:, :-1, :].reshape(-1, vocab_size), labels[:, 1:].reshape(-1))注意logits取的是[:-1](每个位置预测下一个位置的概率),labels取的是[1:](真正的下一个token),这个"标签偏移"是源码里最容易看错的地方。还有个细节,因为绝大多数token都是常见的词,训练时通常会对词表做加权或者用温度系数调节,让模型不要只预测高频词。
3.3 分布式训练源码:DDP、ZeRO、FSDP
7B以上的模型,一张A100(80G)也放不下完整训练状态(参数+梯度+优化器状态),所以分布式训练是必选项。源码层面有几种模式:
- DDP(分布式数据并行):每张卡放完整的模型副本,各算各的梯度,然后All-Reduce同步梯度。简单但显存压力大,适合小模型。
- ZeRO(零冗余优化器):把优化器状态、梯度、参数分片到各个卡上,用时再聚合。DeepSpeed的核心思路。
- FSDP(完全分片数据并行):PyTorch官方的实现,类似ZeRO-3,把参数、梯度、优化器状态都分片,PyTorch 2.x之后用起来比较顺手。
看这些分布式源码的时候,最关键的是理解"通信"开销,如果每步都要All-Reduce,那通信会成为瓶颈。所以代码里通常会做梯度累积来减少通信频次,或者用混合精度训练(FP16/BF16)减少通信量。我给你的建议是先跑通单卡训练,再去看分布式——分布式里90%的坑,本质上都是没理解单卡逻辑就贸然上多卡造成的。
4. 微调源码实战:LoRA、指令微调与大模型微调的操作细节
训练一个大模型动辄几百万成本,普通人根本玩不起。但我们一般也不需要从头训练,而是用微调。微调是在别人训好的基座模型上,用更小的数据、更少的算力,让模型适应你的任务域。这是最贴近实战的部分,也是热词里"大模型微调实战"关注的点。
4.1 SFT:让模型学会"听指令"
基座模型的输出是"接着往下写"风格,不会好好回答问题。指令微调(SFT)就是收集一批"指令-回答"对,用这些数据训练模型学会指令遵循。源码实现跟预训练几乎一样,区别主要在于:
- 数据格式不同:是user/assistant的对话结构。
- 标签处理更精细:一般只计算assistant部分的loss,user那部分要mask掉。如果不mask,模型会去学习生成你的指令,浪费算力还可能学坏。
- 学习率要比预训练小很多,通常是1e-5到2e-5这个量级。
SFT数据质量比数量重要太多。5000条高质量的人工标注数据,效果可能比50万条网爬的杂数据好得多,这一点在源码训练日志里对比loss曲线就能看出来,杂数据的loss下降慢且容易过拟合。
4.2 LoRA源码原理:冻结主干,只学低秩增量
LoRA现在是最常用的微调方法,它的核心思路简单得不像话:预训练模型权重W保持不变,只额外训练一个小矩阵A和B,用BA加到W上。因为微调任务的数据量有限,权重更新量其实存在一个低秩空间,不需要动整个矩阵。
class LinearWithLoRA(nn.Module): def __init__(self, original_linear, rank=8, alpha=16): super().__init__() self.original = original_linear # 冻结 self.lora_A = nn.Parameter(torch.randn(original.in_features, rank)) self.lora_B = nn.Parameter(torch.zeros(rank, original.out_features)) self.scale = alpha / rank def forward(self, x): # 注意:训练和推理时的行为不同 return self.original(x) + (x @ self.lora_A @ self.lora_B) * self.scale为什么要用A随机初始化、B全零初始化?因为初始增量BA=0,模型输出跟原版完全一致,训练更稳,不会一开始就跑偏。scale系数alpha/rank为什么那么设?经验做法,平衡学习率缩放。
LoRA的好处是显存占用小(微调7B模型,单张24G卡就可以跑),而且多个微调任务可以共用一个基座模型,随时切换低秩权重,不用重复加载模型。做LoRA微调的时候要灵活调整rank,不是越大越好。rank大了,能学的内容多,但过拟合风险也大,还占显存。一般rank=8、16、32起步,看验证集效果再调。
4.3 微调里实际容易遇到的坑
我不建议第一次做微调就直接上大任务,从小基座(1B-3B级别)和小数据(几千条)开始是最稳的。几个我在实操中反复踩过的坑:
- 学习率没调对:用预训练的默认学习率直接微调,模型直接训飞。微调一定要用小学习率。
- 数据格式不对:对话模板不一致,模型学会了对齐格式但没真正学会服从指令。
- 梯度累积和批次大小没配合好:卡少的情况下,梯度累积步数要和总batch_size算清楚,别看曲线漂亮,实际效果不行。
- 只盯着loss不看生成效果:loss降到一定程度后就不再有意义了,最终检验标准是生成样本的质量。一定要手动看几十条输出,对比前后差异,而不是只看指标曲线。
5. 推理引擎与部署优化源码:为什么推理比训练更需要功夫
聊完训练,就到了部署端。很多团队训练搞定了,部署直接被线上延迟和显存打趴。推理阶段跟训练是完全不同的逻辑,因为推理是逐token生成的,每一个新token都要依赖之前所有token的计算,这个过程叫自回归生成。代码逻辑就像这样:
for i in range(max_new_tokens): logits = model(sequence) next_token = sample(logits[:, -1, :]) # 只取最后一个位置的预测 sequence = torch.cat([sequence, next_token], dim=-1)这个循环是理解所有推理优化的钥匙。它带来的两个问题,也是推理优化要解决的两个核心问题:
- 延迟问题:每一步生成都要过一遍整个模型,不能并行。生成1024个token,等于跑了1024次前向。
- 重复计算问题:每步都在重新计算之前已经算过的token的KV值,白白浪费算力。
5.1 KV Cache:避免重复计算的工程艺术
既然注意力计算需要每个位置的K和V,而且前序位置的K、V在后续步骤中不变,那就可以把它们缓存下来,每次只计算新token的K、V,然后拼上去。这就是KV Cache。
KV Cache能大幅提升推理速度,但代价是显存增长。计算公式是:
KV Cache显存 = 2 (K和V) x num_layers x num_heads x head_dim x seq_len x batch_size x dtype_size按7B模型、batch_size=1、seq_len=2048、FP16算下来,大概是2GB左右。如果batch和seq_len再大,显存会涨得很可观。所以后面所有推理优化都是围绕怎么压缩KV Cache来得来的。
5.2 连续批处理与PagedAttention源码逻辑
传统的批处理方式是等一个请求完全生成完,才让下一个请求进来,这样GPU利用率很低。连续批处理(Continuous Batching)的思路是:一个请求的某个token生成完了,就把这个位置让给新请求。像饭店拼桌一样,极大提高了吞吐。
vLLM之所以快,除了连续批处理,核心是PagedAttention。它的思路很有意思:把KV Cache切成固定大小的块,用类似操作系统虚拟内存分页的方式管理。块可以不连续存放,需要时才换入换出。之前的KV Cache分配是连续的,既要预分配最大长度,又容易产生碎片,PagedAttention就把这两点都解决了。You don't need to modify the model to use it. 把注意力算法换成PagedAttention的块管理版本就行,这也是为什么它能无缝兼容很多开源模型的原因。
5.3 量化与本地部署:模型瘦身术
部署层面另一个大头是量化,就是把FP16的权重压缩成INT8、INT4。量化后模型体积变小、推理速度变快,代价是精度略降。源码层面有几种实现路子:
- PTQ(训练后量化):不用重新训练,直接对权重做量化。简单,但大模型可能出现某些层特别敏感,精度掉得多。
- QAT(量化感知训练):训练时就模拟量化误差,精度保得好,但成本高。
- GPTQ/AWQ这些算法:通过分析权重和激活分布,找到最优量化参数,在PTQ里做得比较精细。
本地部署这个事,现在门槛越来越低了。像ollama这样的工具,一条命令就能把开源模型跑起来,但如果你想做真正的服务化部署,建议自己用FastAPI把vLLM包一层,控制并发、流式返回、超时策略这些。纯用现成工具省事,却失去了对底层逻辑的把控能力,一旦出现性能问题就无从下手。
6. 大模型源码阅读的环境准备与学习路线:手把手搭建调试环境
最后说说怎么落地。很多读者看到这里可能已经在想:代码我大概看懂了,但从哪儿开始实操?怎么搭环境?看哪些项目的源码?我按自己的学习路径整理一下。
6.1 硬件与软件环境
硬件方面,我建议至少准备一张RTX 3090或4090(24GB显存),这样7B模型的推理、LoRA微调都能跑。如果你没有独立显卡,网上通常有免费的CPU环境可以跑小模型(1B以下级别),或者用云服务的免费额度、GPU按小时租用。学习阶段不用追求配置,能跑通就行。
软件环境方面,明确几个层次:
- Python基础:PyTorch是主力框架,需要掌握基本的Tensor操作和nn.Module。
- 模型代码库:HuggingFace Transformers,多数开源模型都有它的实现。
- 推理框架:vLLM serving系统,重点看它如何处理KV Cache。
- 训练框架:微调可以从Transformers的Trainer开始,再进阶到用DeepSpeed或多卡。
6.2 阅读源码的方法论
源码动辄几万行,不可能从头到尾都看,关键是带问题去看。三个方法特别管用:
第一个方法是从入口文件开始。比如Transformers里,先找到modeling_llama.py,看LlamaModel类的forward函数,跟一遍数据流,从输入token到输出logits,搞清楚每一层的形状变化。这一步过了,模型结构的大框架就在你脑子里了。
第二个方法是写最小复现脚本。不用看全部,只取核心组件,比如单独把attention模块抠出来,造一个小输入,打印每一步的shape和中间结果。能用很小的代码量验证你对原理的理解,比通读整份源码高效得多。
第三个方法是对比式阅读。同一个模型结构有好几个实现(官方、HuggingFace、vLLM),对比它们的差异,能加深对框架设计的理解。
6.3 学习路线建议
结合我自己从零到一的过程,建议按这个顺序走:
- 先跑通一个开源模型,比如用ollama或transformers跑7B模型的推理,体验完整流程。
- 阅读Transformer核心组件的源码,重点是注意力机制、FFN、位置编码。这个阶段不需要大显存,只看代码理解逻辑。
- 跑一次微调实验,用LoRA在某个中文数据集上,对开源模型做SFT,喂几百条数据就行,关键是通理解全部流程。
- 部署一个模型到本地服务,用vLLM或类似框架,测一下吞吐和延迟,再尝试调整参数。
- 遇到不懂的,去看对应模块的源码。带着问题找源码,是最快的学习方式。
很多人的误区是先去翻大模型的原始论文,被公式劝退。我的建议恰恰相反,先看代码,有代码底子在,回头看论文反而豁然开朗。论文是抽象的代码,代码是具体的论文,两条腿走路比单看哪一边都快。
7. 从架构源码到上层应用:RAG、Agent、多模态与提示词工程
当你把底层架构吃透了,你会发现之前那些"上层应用"其实都是在围绕同一套底层逻辑在做文章。
7.1 RAG和提示词工程:都是在对齐上下文
现在的RAG应用,本质上就是把检索到的文本拼到用户的查询里面,一起喂给大模型。听起来简单,但效果好坏依赖两个能力:一个是检索质量——你搜回来的内容是不是真的有用,另一个是模型对上下文的理解能力——它能不能从冗长的上下文里精准找到关键信息。
源码层面,RAG的组装过程就是处理好两个sequence的拼接、Attention mask的构造、特殊token的区分。很多RAG效果不好,不是模型不行,而是上下文管理做得糙——没有及时清理无关内容,没有处理好指令和检索内容的边界。大模型的"上下文工程"其实就是围绕这个在做文章。当你理解模型内部如何处理长文本,就知道上下文应该怎么设计才最高效。
7.2 AI Agent的源码视角:从单一模型到系统架构
AI Agent这两年特别火,但如果你去看一个Agent的源码,会发现它本质上是:一个循环。大模型在这个循环里扮演"决策大脑"的角色,判断当前应该调用哪个工具,然后根据工具返回结果再判断下一步做什么。代码骨架通常是:
while not task_finished: response = llm_chat(messages) # 模型给出决策 if response contains tool_call: result = execute_tool(response.tool_name, response.arguments) messages.append(result) else: finished = TrueAgent出问题,90%不是模型太笨,而是循环逻辑有漏洞——工具调用格式解析失败、没有设置最大轮数导致死循环、错误没有处理机制导致整个流程崩溃。这些问题的排查,都需要你理解模型输出的结构,理解工具调用的协议设计,这比纯调API更依赖底层模型的细节把握。
7.3 多模态模型架构扩展
多模态模型(比如能看图对话的模型)可以理解成:在文本模型的基础上,增加了一个视觉编码器,把图片变成一组向量,再通过一个投影层映射到文本模型的embedding空间里。源码上就是加了一个vision tower和projector。
你如果已经理解了文本transformer的源码,看多模态模型会轻松很多,因为文本部分是原封不动地复用,多出来的部分是"如何让视觉信号和文本信号对齐"——这本质上还是embedding空间的对接问题。所以学多模态,前提还是先把文本模型的底层吃透。
至于提示词工程,很多人把它当成"写咒语",其实底层逻辑是:模型在预训练阶段见过海量文本模式,提示词的作用是激活正确的模式。理解了这一点,你就会明白为什么chain-of-thought有效——因为它在激活模型"分步推理"的模式。提示词工程不是玄学,是对模型底层行为模式的一种利用。
最后说几句实在的
回过头看,我花在读源码上的时间,是这些年ROI最高的学习投入。大模型技术迭代快得吓人,今天学的新框架可能半年后就过时了,但只要Transformer这个底座不变,你读源码积累的理解就不会过时。新出的模型、新出的工具,你扫一眼就能猜到它大概是怎么实现的,学什么东西都快。
想给刚开始的读者一个建议:不要被"底层架构""源码解析"这种词吓到,不用一次性通读几千行代码,从一个小块开始,搭好环境,跑通一次,把attention的shape变化亲手打印出来看看。你只需要有一个能跑起来的显卡或云环境、一个7B左右的开源模型、一份transformers的代码,就足够开始第一天的实操。
源码阅读是一场持久战,它不会立竿见影地给你结果,但积累到某个临界点之后,你再看技术文章、再跟人聊技术方案,整个视野会完全不一样。