为什么值得把 LLaMA 的架构单独拎出来看现在做 Agent、RAG、微调绕不开开源权重模型。而开源权重模型里LLaMA 系列的架构几乎成了事实上的公共底座Qwen、Baichuan、InternLM、DeepSeek 早期的很多设计都能看到 LLaMA 结构的影子。所以与其一个个去看各家模型的论文不如先把 LLaMA 这条主线吃透再看别的模型时会快很多。这篇文章不讲训练数据怎么清洗、不讲 RLHF 怎么做只聚焦一件事从 LLaMA 1 到 LLaMA 3Transformer 结构本身改了什么为什么这么改。我会按版本顺序走每个版本挑出真正影响推理和微调的改动来讲并给出对应的最小代码实现。需要提前说明的是Meta 对 LLaMA 3 的完整技术报告披露程度比 LLaMA 1/2 高很多但仍有部分细节比如具体的数据配比没有公开。文中凡是涉及这类信息我会明确写出来不做推测性描述。LLaMA 1 定下的骨架几个当时不算主流的选择LLaMA 1 在 2023 年初发布参数量从 7B 到 65B。它最有意思的地方不是又一个大模型而是它在几个关键位置做了和 GPT-3 不一样的选择而这些选择后来被大量模型抄走。位置编码用 RoPE而不是可学习的位置嵌入GPT 系列早期用的是可学习的位置嵌入learned positional embedding把位置当成一个可训练的向量加到 token embedding 上。这种方式的问题在于训练时见过的最大长度就是模型能处理的最大长度超了就没法外推。LLaMA 用的是 RoPERotary Position Embedding。它的做法是不去加位置信息而是对 Query 和 Key 向量做旋转旋转角度由位置决定。这样两个 token 之间的注意力分数天然只依赖它们的相对距离。RoPE 的核心实现大概是这样importtorchdefprecompute_freqs_cis(dim:int,end:int,theta:float10000.0):# dim 是 head_dim必须是偶数freqs1.0/(theta**(torch.arange(0,dim,2)[:(dim//2)].float()/dim))ttorch.arange(end,devicefreqs.device)freqstorch.outer(t,freqs).float()# (end, dim//2)freqs_cistorch.polar(torch.ones_like(freqs),freqs)# 转成复数returnfreqs_cisdefapply_rotary_emb(xq,xk,freqs_cis):# xq, xk: (B, T, n_heads, head_dim)xq_torch.view_as_complex(xq.float().reshape(*xq.shape[:-1],-1,2))xk_torch.view_as_complex(xk.float().reshape(*xk.shape[:-1],-1,2))freqs_cisfreqs_cis[:xq.shape[1]].view(1,xq.shape[1],1,-1)xq_outtorch.view_as_real(xq_*freqs_cis).flatten(3)xk_outtorch.view_as_real(xk_*freqs_cis).flatten(3)returnxq_out.type_as(xq),xk_out.type_as(xk)这里用复数乘法来做旋转是把二维平面上的旋转写成复数乘法的形式代码比手写 cos/sin 矩阵干净。注意theta在 LLaMA 1 里是 10000这个值在后面 LLaMA 3 长上下文版本里被调过后面会讲。RoPE 带来的实际好处是模型在短序列上训练推理时对更长序列有一定外推能力注意是一定不是无限。这也是后来所有做长上下文的模型都基于 RoPE 做改造的原因。Pre-Norm RMSNorm原始 Transformer 用的是 Post-Norm也就是LayerNorm(x Sublayer(x))。这种结构在深层网络里训练不稳定需要小心调 warmup。LLaMA 用的是 Pre-Norm把归一化放到子层前面classRMSNorm(torch.nn.Module):def__init__(self,dim:int,eps:float1e-6):super().__init__()self.epseps self.weighttorch.nn.Parameter(torch.ones(dim))def_norm(self,x):returnx*torch.rsqrt(x.pow(2).mean(-1,keepdimTrue)self.eps)defforward(self,x):returnself._norm(x.float()).type_as(x)*self.weight同时它把 LayerNorm 换成了 RMSNorm。区别是 RMSNorm 不减均值只做均方根归一化。少了一次均值计算也少了一个 bias 参数。这个改动在效果上基本无损但计算量小一些。社区里有人做过对比实验结论是 RMSNorm 和 LayerNorm 在同等规模下差异很小主要收益在速度。SwiGLU 替代 ReLU FFNFFN 层 LLaMA 用的是 SwiGLU结构是三个线性层加一个门控classFeedForward(torch.nn.Module):def__init__(self,dim:int,hidden_dim:int,multiple_of:int256):super().__init__()hidden_dimint(2*hidden_dim/3)# 对齐到 multiple_of 的整数倍方便硬件加速hidden_dimmultiple_of*((hidden_dimmultiple_of-1)//multiple_of)self.w1torch.nn.Linear(dim,hidden_dim,biasFalse)self.w2torch.nn.Linear(hidden_dim,dim,biasFalse)self.w3torch.nn.Linear(dim,hidden_dim,biasFalse)defforward(self,x):returnself.w2(torch.nn.functional.silu(self.w1(x))*self.w3(x))注意这里hidden_dim乘的是 2/3因为 SwiGLU 有三个矩阵而不是两个为了保持总参数量和标准 FFN 接近要把中间维度压下来。这个 2/3 是经验值不是推导出来的。另外multiple_of256是为了让维度对齐到硬件友好的数值这一点在 LLaMA 1 论文里没有明说是从官方实现里看到的。【关键结论】LLaMA 1 的这几个改动——RoPE、Pre-Norm、RMSNorm、SwiGLU——后来基本成了开源 LLM 的默认配置。你现在看任何一个 2023 年之后的开源模型大概率能同时看到这四个。LLaMA 2架构基本没动改动在别处LLaMA 2 在 2023 年 7 月发布。如果只看架构它和 LLaMA 1 几乎一模一样。真正的变化在三个地方上下文长度从 2048 提到 4096。这个改动对 RoPE 的theta值没有调整直接扩大训练长度。训练数据量从 1T token 提到 2T token。这个和架构无关但直接决定了模型的知识密度。加入了 RLHF 和 GQA 的 70B 版本。注意LLaMA 2 只有 70B 用了 GQA7B 和 13B 还是标准的多头注意力。GQAGrouped Query Attention是什么值得单独说一下因为它是 LLaMA 3 全系采用的关键改动。标准多头注意力里Query、Key、Value 的 head 数量相同。推理时 KV Cache 的大小和 head 数成正比。当序列很长、batch 很大时KV Cache 会吃掉大量显存。GQA 的做法是让多个 Query head 共享一组 Key/Value head。比如 32 个 Query head 分成 8 组每组 4 个 Query 共享 1 个 KV head。这样 KV Cache 直接降到原来的 1/4。classAttention(nn.Module):def__init__(self,n_heads,n_kv_heads,dim):super().__init__()self.n_headsn_heads self.n_kv_headsn_kv_heads self.head_dimdim//n_heads self.wqnn.Linear(dim,n_heads*self.head_dim,biasFalse)self.wknn.Linear(dim,n_kv_heads*self.head_dim,biasFalse)self.wvnn.Linear(dim,n_kv_heads*self.head_dim,biasFalse)self.wonn.Linear(n_heads*self.head_dim,dim,biasFalse)defforward(self,x,freqs_cis,mask):B,T,_x.shape xqself.wq(x).view(B,T,self.n_heads,self.head_dim)xkself.wk(x).view(B,T,self.n_kv_heads,self.head_dim)xvself.wv(x).view(B,T,self.n_kv_heads,self.head_dim)xq,xkapply_rotary_emb(xq,xk,freqs_cis)# 关键把 kv head 复制到和 q head 一样多xkxk.repeat_interleave(self.n_heads//self.n_kv_heads,dim2)xvxv.repeat_interleave(self.n_heads//self.n_kv_heads,dim2)# ... 后续标准注意力计算repeat_interleave这一步在训练时是显式复制推理时很多实现会直接在 kernel 里处理不真的复制张量。这里写出来是为了让逻辑清楚。GQA 相比 MQAMulti-Query Attention所有 Query 共享一组 KV折中得更好MQA 压缩太狠效果掉得明显GQA 在压缩率和效果之间取了个平衡点。LLaMA 2 70B 用 GQA 的实测效果Meta 在论文里说和标准 MHA 接近但这个结论我没有自己复现过。LLaMA 3词表扩张才是最大的改动LLaMA 3 在 2024 年 4 月发布先出 8B 和 70B后来补了 405B。架构上最值得说的改动有两个而且这两个改动的影响完全不在一个量级。全系采用 GQALLaMA 2 只有 70B 用 GQALLaMA 3 从 8B 到 405B 全部用。8B 的配置是 32 个 Query head、8 个 KV head压缩比 4:1。这个改动对显存的影响是实打实的部署 8B 模型时长上下文场景下 KV Cache 不再是瓶颈。词表从 32K 扩到 128K这个改动比 GQA 影响更大但经常被忽略。LLaMA 1/2 用的是 SentencePiece BPE词表 32000。LLaMA 3 换成了基于 tiktoken 的 BPE词表 128256。为什么扩词表这么重要考虑中文场景。LLaMA 2 的 32K 词表对中文支持很差一个常见汉字经常被切成 2-3 个 token。同样一段中文文本用 LLaMA 2 的分词器编码出来token 数可能是英文的 2 倍以上。这直接导致两个后果一是有效上下文被压缩二是推理成本翻倍。词表扩到 128K 后中文的 token 效率明显改善。我实际用 LLaMA 3 的 tokenizer 和 LLaMA 2 的对比过同一段中文文本前者 token 数大约是后者的一半左右。这个数字会随文本内容变化但量级上的差异是确定的。【踩坑提醒】如果你要基于 LLaMA 3 做微调词表变化意味着 embedding 层和 lm_head 的参数量变了。从 32000×4096 变成 128256×40968B 模型光这两层就多了接近 4 亿参数。加载旧版 LLaMA 2 的微调权重到 LLaMA 3 上是不可能的必须重新训练。上下文长度和 RoPE thetaLLaMA 3 初始版本上下文是 8K。后来 Meta 通过调整 RoPE 的theta值把上下文扩到了 128K。这里涉及 RoPE 的一个性质theta越大不同位置的旋转频率差异越小位置编码能覆盖的范围就越长。LLaMA 1/2 用的是 10000长上下文版本会调到 500000 甚至更大。# 长上下文版本典型配置freqs_cisprecompute_freqs_cis(dimhead_dim,endmax_seq_len*2,theta500000.0# 而不是默认的 10000)需要说明的是单纯改theta并不能直接把模型外推到任意长度通常还需要配合继续训练continued pretraining来让模型适应新的位置分布。Meta 在 LLaMA 3 的长上下文版本上做了这一步但具体训练细节公开得不完整。三个版本放在一起对比特性LLaMA 1LLaMA 2LLaMA 3位置编码RoPE (theta10000)RoPE (theta10000)RoPE长上下文版本 theta 调大归一化Pre-RMSNorm同 LLaMA 1同 LLaMA 1FFNSwiGLU同 LLaMA 1同 LLaMA 1注意力标准 MHA70B 用 GQA其余 MHA全系 GQA词表大小3200032000128256上下文长度204840968K后扩到 128K训练 token 量1T2T15T8B这张表里有一个信息需要标注LLaMA 3 8B 的 15T token 是 Meta 官方博客给出的数字70B 和 405B 的具体数字我记不准确所以没有列。从表里能看出一个规律LLaMA 1 定架构LLaMA 2 加数据LLaMA 3 改词表和注意力。架构层面的创新主要集中在第一代后面两代更多是工程和规模上的调整。这些改动对实际开发意味着什么如果你在选模型做微调或者部署这几个点值得注意。词表大小直接影响 embedding 参数量。LLaMA 3 的 8B 模型embedding 层占了相当一部分参数。如果你做的是小语种或者垂直领域微调词表大意味着有更多参数需要训练LoRA 的 rank 可能需要相应调大。GQA 让 KV Cache 变小但不改变计算量。GQA 减少的是显存占用注意力计算本身的 FLOPs 没变。所以如果你的瓶颈是显存GQA 帮助很大如果瓶颈是算力GQA 帮助有限。RoPE theta 不是随便调的。改动 theta 相当于改变位置编码的频率分布必须配合训练。直接改推理时的 theta 值会让模型输出变得混乱这一点我在小模型上试过效果确实会崩但具体崩成什么样和模型规模、序列长度都有关。LLaMA 1/2 的 32K 词表在中文场景下成本劣势明显。如果你的应用以中文为主又必须用 LLaMA 系模型LLaMA 3 相比 LLaMA 2 在 token 效率上的提升可能比参数量提升带来的收益还大。一个可以跑的最小验证如果你想自己验证 RoPE 的相对位置性质可以跑这段代码。它不依赖任何模型权重纯粹验证位置编码的数学性质。importtorchdefrope_relative_property_check():dim64theta10000.0freqs1.0/(theta**(torch.arange(0,dim,2).float()/dim))defrotate(x,pos):# x: (dim,)anglespos*freqs cos,sintorch.cos(angles),torch.sin(angles)x1,x2x[0::2],x[1::2]outtorch.stack([x1*cos-x2*sin,x1*sinx2*cos],dim-1)returnout.flatten()torch.manual_seed(0)qtorch.randn(dim)ktorch.randn(dim)# 位置 (0, 5) 和位置 (10, 15) 的相对距离都是 5score_atorch.dot(rotate(q,0),rotate(k,5))score_btorch.dot(rotate(q,10),rotate(k,15))print(f相对距离 5 的两组注意力分数:{score_a.item():.6f},{score_b.item():.6f})# 两者应该非常接近差异来自浮点误差if__name____main__:rope_relative_property_check()跑出来两个数应该在小数点后四五位才出现差异。这就是 RoPE 相对位置性质的直接体现注意力分数只依赖位置差不依赖绝对位置。写在最后把 LLaMA 三代放在一起看最反直觉的一点是真正影响使用体验的改动往往不是那些听起来最架构的改动。GQA 听起来很硬核但它主要解决显存问题而词表从 32K 扩到 128K 这种听起来很工程的改动反而直接决定了中文场景下的成本和效果。如果你打算基于 LLaMA 系模型做二次开发我的建议是先确认三件事你的场景以什么语言为主决定词表选择、你的瓶颈是显存还是算力决定是否必须用 GQA 版本、你的序列长度需求是多少决定是否需要长上下文版本。这三个问题想清楚选型基本就定了。