01. 全景:从一句话到下一个 token
第 00 篇走过了 2017 年原始 Transformer 的编码器-解码器结构,今天的大语言模型只保留了其中的解码器。员工小林在资料助手里问了一句“年假怎么申请?”,几百毫秒后,屏幕上开始一个字一个字地冒出回答。中间这段时间,模型内部到底做了什么?这一篇先把整台机器从头到尾走一遍:这句话被切成几块、每一块变成什么、经过哪几十层、最后怎样变成回答的第一个 token。主算例是 Llama-3.1-8B,后面所有章节都在这张全景图的某一块上展开。
读完应能说清三件事:一个 decoder-only 模型由哪几块组成、每块的输入输出是什么形状;80 亿参数里七成为什么在 FFN;以及“一次只出一个 token”的自回归生成,对训练和推理分别意味着什么。
模型只做一件事:给下一个 token 打分
不管是回答制度问题、写代码还是翻译,大语言模型对外只提供一个功能:给定一串 token,给词表里每个 token 打一个分,分数越高,越可能是下一个。token 是模型处理文字的单位,可能是一个字、一个词或半个词(第 02 篇细讲);Llama-3.1-8B 的词表有 128,256 个 token,所以每次要给出 128,256 个分数。这组分数叫 logits,经过 softmax 换算成概率,再按某种规则挑出一个 token。
挑出的 token 接到输入末尾,再问一次“下一个是什么”,回答就一个 token 一个 token 地长出来。这种做法叫自回归(autoregressive)。聊天、续写、调用工具,都是这一个功能反复调用的结果。
现在主流的大语言模型几乎都是 decoder-only 结构:只有一叠“解码器层”,每个位置只能看到自己和左边的内容,看不到右边。这个名字来自 2017 年原始 Transformer 的编码器—解码器结构,第 09 篇会讲为什么最后只留下了解码器这一半。
一句话走过的七步
下面这张图把“年假怎么申请?”从文本到下一个 token 的全过程画在一起,方括号里是每一步输出的张量形状。第一维是序列长度(这里是 5 个 token),第二维是每个 token 的向量长度。
按顺序看每一步:
- 分词。分词器把“年假怎么申请?”切成
年 / 假 / 怎么 / 申请 / ?5 个 token,换成词表里的编号[8107, 107099, 104237, 112838, 11571](Llama 3.1 分词器实测),形状[5]。 - 词嵌入。拿编号去一张
[128256, 4096]的表里取行,每个 token 得到一个 4096 维的向量,形状变成[5, 4096]。这一步只是查表,不做乘法。 - 32 个解码器层。每层的输入输出都是
[5, 4096],形状不变,变的是向量里的内容:越往后,每个位置的向量里混进了越多上下文信息。 - 最终 RMSNorm。把每个向量的数值尺度统一一下,交给输出层。
- 输出层(代码里叫
lm_head)。一个4096 → 128256的线性层,把每个位置的向量变成 128,256 个分数,形状[5, 128256]。 - 取最后一行。第 5 行,也就是“?”这个位置的分数,代表“读完整句话之后,下一个 token 是什么”。前 4 行分别是“读到年”“读到假”……之后的预测,生成时用不上。
- 选出 token。softmax 后按解码规则挑一个,例如“根据”(示意,真实输出取决于模型和解码设置),接到末尾进入下一轮。
真实的对话请求不会只送这 7 个字:应用会先套上对话模板,加上系统提示和角色标记,Llama 3.1 默认模板套完是 40 个 token。结构上没有区别,只是序列长度从 5 变成 40,第 08 篇讲模板。
一层里面:注意力与 FFN 的分工
32 个解码器层结构完全相同,各有各的参数。图下半部分放大了其中一层,里面只有两个子层:
- 自注意力(self-attention):让每个位置去看前面的位置,把需要的信息搬过来。“申请”这个位置靠它知道前面说的是“年假”,而不是“报销”。这是层里唯一让不同 token 之间交换信息的地方。
- 前馈网络 FFN(feed-forward network):对每个位置的向量单独做加工,各算各的,互不相干。它先把 4096 维放大到 14336 维,经过门控激活再压回 4096 维。大部分参数在这里,第 05 篇讲它学到了什么。
两个子层之前各有一个 RMSNorm,把输入的数值尺度归一化;子层的输出不替换原来的向量,而是加回去,这叫残差连接。贯穿整个模型的那条 [5, 4096] 的向量通道,叫残差流(residual stream):词嵌入把初始向量放进去,每个子层读一份、算出一点增量、再加回来。第 06 篇专门讲为什么要这样设计。
写成简化的 PyTorch 风格代码,一层就是这几行,对应 Transformers 里的 LlamaDecoderLayer:
python
def decoder_layer(h): # h: [序列长度, 4096]
a = attention(rms_norm_1(h)) # 看前面的 token,输出 [序列长度, 4096]
h = h + a # 加回残差流
f = ffn(rms_norm_2(h)) # 每个位置各自加工,[序列长度, 4096]
h = h + f # 再加回残差流
return h
def model(token_ids): # token_ids: [序列长度]
h = embed_tokens[token_ids] # 查表 → [序列长度, 4096]
for layer in layers: # 32 层
h = decoder_layer(h)
return lm_head(final_norm(h)) # → [序列长度, 128256]这段代码里没有出现“位置”。Llama 把位置信息放在注意力内部,用 RoPE 旋转 Q 和 K(第 07 篇),所以图上没有单独的位置编码模块。
参数花在哪:8B 的分解表
“8B”是 80 亿个参数。按 config.json 里的几个数字,可以把每个矩阵的大小逐个乘出来:
| 部分 | 矩阵形状 | 每层参数 | 32 层合计 | 占全模型 |
|---|---|---|---|---|
| 注意力 q_proj | 4096 × 4096 | 16,777,216 | ||
| 注意力 k_proj | 4096 × 1024 | 4,194,304 | ||
| 注意力 v_proj | 4096 × 1024 | 4,194,304 | ||
| 注意力 o_proj | 4096 × 4096 | 16,777,216 | ||
| 注意力小计 | 41,943,040 | 1,342,177,280 | 16.7% | |
| FFN gate / up / down | 各 4096 × 14336 | 176,160,768 | 5,637,144,576 | 70.2% |
| 两个 RMSNorm | 各 4096 | 8,192 | 262,144 | 约 0 |
| 词嵌入 | 128256 × 4096 | 525,336,576 | 6.5% | |
| 输出层 | 4096 × 128256 | 525,336,576 | 6.5% | |
| 最终 RMSNorm | 4096 | 4,096 | 约 0 | |
| 合计 | 218,112,000 | 8,030,261,248 | 100% |
k 和 v 只有 4096 × 1024,是因为 Llama-3.1-8B 用了 GQA:32 个查询头共用 8 组 K、V,每组 128 维,8 × 128 = 1024(第 04 篇)。按比例画出来是这样:
FFN 占七成,原因就在形状上。一层 FFN 有三个 4096 × 14336 的矩阵,中间维度是隐藏维度的 3.5 倍;注意力四个矩阵里最大的也只有 4096 × 4096,k、v 还因为 GQA 缩到四分之一。一层里 FFN 和注意力的参数比约 4.2 : 1。
词嵌入和输出层合计 13.1%,取决于词表大小和隐藏维度,和层数无关。模型越小这部分占比越高:Qwen2.5-0.5B 的词表矩阵占了全部参数的 27.6%,第 02 篇会算这笔账。模型卡里常见的“非嵌入参数”(non-embedding parameters)就是扣掉这两块之后的数,更能反映模型主体的规模。
计算花在哪:每个 token 约 15 GFLOPs
参数量直接决定了每个 token 的计算量。一个参数在线性层里参与一次乘法和一次加法,即 2 次浮点运算(FLOPs)。每个 token 都要和每个矩阵相乘一遍,所以:
text
每 token 前向计算 ≈ 2 × 参与矩阵乘的参数
= 2 × (8,030,261,248 − 525,336,576) 词嵌入是查表,不算乘法
≈ 2 × 75.0 亿 ≈ 15.0 GFLOPs RMSNorm 的 26 万个参数忽略不计粗算时常直接写 2 × 80 亿 ≈ 16 GFLOPs,差别就在词嵌入那一块。其中输出层一项是 2 × 4096 × 128256 ≈ 1.05 GFLOPs,单独一个线性层就是一整层 FFN(0.35 GFLOPs)的三倍。
这 15 GFLOPs 是“和权重相乘”的部分,每个 token 都一样,跟上下文多长无关。注意力里还有一部分随上下文增长:第 n 个 token 要和前面 n 个位置算相关分数、再加权求和,每层约 4 × n × 4096 FLOPs。小林这句话只有 5 个 token,这部分可以忽略;带着 8192 个 token 的员工手册提问时,32 层加起来约 4.3 GFLOPs,已经是固定部分的四分之一多。第 03 篇算这笔账。
存储上,权重按 BF16 每个参数 2 字节,约 16.06 GB(14.96 GiB);生成时每个 token 还要在 32 层各存一份 K、V,每 token 128 KiB。显存怎样分配见推理系列的显存账本。
自回归:一次只出一个 token
回到第 7 步:选出“根据”以后,模型要把它接到末尾,用 6 个 token 再算一遍,才能得到下一个 token。
每一步都依赖上一步的结果,所以生成只能串行。好在不必真的把整段重算:前面位置的 K、V 不会因为后面来了新 token 而改变(每个位置只看左边),可以缓存下来,第 2 步开始只为新 token 算一遍 32 层。这就是推理系列讲的 Prefill 与 Decode 和 KV Cache。Transformers 的 LlamaForCausalLM 也有一个 logits_to_keep 参数,生成时只为最后一个位置算输出层,省掉 [5, 128256] 里用不上的前 4 行。
训练时情况相反,前 4 行全都有用。训练文本是完整已知的,“年 → 假”“年假 → 怎么”“年假怎么 → 申请”……每个位置都在预测它的下一个 token,一次前向就能同时得到 5 道题的答案和误差。之所以能这样并行,正是因为每个位置看不到右边:第 1 个位置的预测不会偷看到第 2 个 token。这个“只看左边”的约束叫因果掩码,第 03 篇手算它。训练怎样利用这些误差,见训练系列的一步训练。
这张全景图什么时候不够用
“32 个一模一样的层,每层一个注意力加一个 FFN”是 Llama 这一类稠密模型的样子,拿它去套所有新模型会出错:
- MoE 模型把 FFN 换成几十到几百个“专家”,每个 token 只走其中几个。DeepSeek-V3 总参数 6710 亿,每个 token 只激活 370 亿,“每 token 计算 ≈ 2 × 参数”要换成激活参数(第 10 篇)。
- 混合架构里,不是每层都用标准注意力:有的层只看最近几千个 token(滑动窗口),有的层换成状态大小固定的线性注意力(第 11、12 篇)。这时“注意力计算随上下文线性增长”只对部分层成立,KV Cache 也不再是每层一样大。
- MLA(多头潜在注意力)把 K、V 压缩成低维向量再缓存,k_proj、v_proj 的形状和上面的表完全不同(第 04、15 篇)。
判断一个模型是否还是这张图,先看它的 config.json:出现 num_experts、num_local_experts、n_routed_experts 一类字段,说明 FFN 是 MoE;layer_types 按层列出的注意力类型不全相同(例如混着 sliding_attention 和 full_attention),说明各层不一样;出现 kv_lora_rank,说明用了 MLA。第 18 篇会系统讲怎样读配置和源码。
一个 decoder-only 模型就是“分词 → 查表 → 32 层(注意力 + FFN,残差加 RMSNorm)→ 输出层 → 选出一个 token”,循环往复。参数七成在 FFN,计算量在短上下文时约等于 2 倍参数,长上下文时注意力那部分会追上来。下一篇从这条流水线的入口开始:分词与词嵌入,看“年假怎么申请?”为什么正好切成 5 块,那张 [128256, 4096] 的表又从哪里来。
