← 返回首页
基础理论2026-09-26 发布 · 基础篇 v1.1(持续更新)
通用大模型架构详解:一次前向的形状流转
拿一条真实请求(6 个 token)走完一个通用 decoder-only 大模型的全流程,每一步都标出具体形状
一句话概括:一个通用大模型 = Embedding 查表 + 32 层 decoder block(注意力 + FFN + 残差)+ 最终归一化 + LM head。整个前向传播里,主数据流的形状始终是 [1, 6, 4096]——进多少层都不变;只有入口(分词)和出口(LM head)改形状。
本篇的讲法:像 Kev 决策流程那篇一样,全程用一个具体例子走到底、全程写具体数字:输入是一条 6 个 token 的真实请求,隐藏维 4096、32 层、32 个注意力头(每头 128 维)、8 组 KV 头、FFN 中间层 11008、词表 152000——这些数字取自 Qwen / Llama 系主流开源模型的典型量级。B(batch)、S(序列长)、H(隐藏维)这些字母只在解释维度含义时出现。
[1, 6, 4096]
主数据流形状:1 条请求 × 6 token × 4096 维
×32 层
decoder block 堆叠,形状进出不变
[1, 152000]
最后位置的 logits,152000 为词表大小
一、例子与参照配置
全篇用同一条请求做例子:
原始文本:「订单有问题,需要退款吗」
经 tokenizer 分词后是 6 个 token。模型用的参照配置(Qwen / Llama 系典型量级,不同模型数值有差异,结构完全一致):
| 配置项 | 具体值 | 含义 |
| 词表大小 V | 152000 | 模型认识多少个 token |
| 隐藏维 H | 4096 | 每个 token 被表示成 4096 维向量(模型的"宽度") |
| 层数 N | 32 | decoder block 堆叠 32 层(模型的"深度") |
| 注意力头 | 32 头 × 128 维 | 32×128 = 4096 = H |
| KV 头(GQA) | 8 组 | 8 组 KV 头服务 32 个 Q 头,KV Cache 省 4 倍 |
| FFN 中间维 | 11008 | 先升到 11008 再降回 4096(SwiGLU 门控) |
| 本例输入 | S = 6 | 「订单有问题,需要退款吗」分词后 6 个 token,batch = 1 |
维度字母对照:B = batch(本例 1)、S = 序列长度(本例 6)、H = 隐藏维(本例 4096)、V = 词表(本例 152000)。
二、总流程:九个阶段(带具体数字,阶段四展开到 QKV / Attention)
flowchart TD
classDef io fill:#f6f8fa,stroke:#8b949e,color:#1f2937,stroke-width:1.5px
classDef stage fill:#eff6ff,stroke:#3b82f6,color:#1f2937,stroke-width:1.5px
classDef attn fill:#e8f0fe,stroke:#2f6feb,color:#1a4ea8,stroke-width:1.5px
classDef ffn fill:#e6f5ed,stroke:#1a7f4b,color:#155c38,stroke-width:1.5px
classDef out fill:#e6f5ed,stroke:#1a7f4b,color:#155c38,stroke-width:1.5px
T0["原始文本
「订单有问题,需要退款吗」"]:::io
S1["阶段一 · 分词(tokenizer)
按词表切分 → 6 个 token
[订单] [有问题] [,] [需要] [退款] [吗]"]:::stage
S2["阶段二 · 查词表换成整数 ID
[1533, 100157, 3493, 11, 2762, 104120](示意)
形状 [6] → 加 batch 维 [1, 6]"]:::stage
S3["阶段三 · Embedding 查表
词表矩阵 [152000, 4096] 按 ID 取 6 行
形状 [1, 6, 4096]"]:::stage
S4["阶段四 · 进入 32 层 decoder block
以第 1 层为例(32 层结构完全相同)"]:::stage
S4a["④-a RMSNorm
[1, 6, 4096]"]:::attn
S4b["④-b Q / K / V 投影
Q [1, 6, 4096] @ [4096, 4096] → 拆 32 头 [1, 32, 6, 128]
K, V @ [4096, 1024] → 8 组头 [1, 8, 6, 128](GQA)"]:::attn
S4c["④-c Attention 注意力
scores = Q @ Kᵀ ÷ √128 → [1, 32, 6, 6]
因果掩码(第 3 个 token 看不到第 5 个)→ softmax
probs @ V → [1, 32, 6, 128]"]:::attn
S4d["④-d 合并 32 头 → [1, 6, 4096]
输出投影 + 残差"]:::attn
S4e["④-e FFN(SwiGLU)
[1, 6, 4096] → 升维 [1, 6, 11008] × 2 → 降回 [1, 6, 4096]
+ 残差"]:::ffn
S4f["④-f 第 2 … 32 层:重复同样流程
形状始终 [1, 6, 4096]"]:::stage
S5["阶段五 · 最终 RMSNorm
[1, 6, 4096]"]:::stage
S6["阶段六 · LM head 投影
[1, 6, 4096] @ [4096, 152000]
logits 形状 [1, 6, 152000]"]:::stage
S7["阶段七 · 取最后位置
只要第 6 个位置的打分
[1, 152000]"]:::stage
S8["阶段八 · 采样
温度 / top-p → 下一个 token「是」"]:::out
S9["阶段九 · 自回归
「是」拼回输入(现在 7 个 token)
解码阶段每步只进 1 个新 token [1, 1, 4096]
历史 KV 已缓存,无需重算"]:::stage
T0 --> S1 --> S2 --> S3 --> S4
S4 --> S4a --> S4b --> S4c --> S4d --> S4e --> S4f
S4f --> S5 --> S6 --> S7 --> S8
S8 -.->|"循环直到结束符"| S4
图 1 · 一条 6 token 请求的完整前向(九个阶段):形状从 [6] 出发,[1, 6, 4096] 穿过 32 层,LM head 投到 [1, 6, 152000],采样出第 7 个 token;阶段四展开到 QKV 与 Attention
三个关键观察:
- 入口和出口改形状,中间不改:tokenize 把文字变成 [6] 的整数序列,embedding 查表升到 [1, 6, 4096];此后穿过全部 32 层形状不变,直到 LM head 投到 [1, 6, 152000]。残差流设计的直观体现——主干通道恒定宽度,子层只做"加工",加工完加回主干。
- 训练与解码的形状不同:训练时整段文本一次性进来(本例 [1, 6, 4096]),并行算所有 6 个位置;推理(生成)时用 KV Cache,每步只进 1 个新 token([1, 1, 4096]),历史 6 个位置的结果都在缓存里。
- LM head 是唯一接词表的投影:[4096, 152000] 的矩阵一次乘法就是约 37 亿次乘加(6 位置 × 4096 × 152000),长序列下是隐形开销大户。
三、单层 decoder block 放大:还是 6 个 token 的例子
总流程的阶段三是主体。把其中一层放大,看 6 个 token 是怎么被加工的:
flowchart TD
classDef io fill:#f6f8fa,stroke:#8b949e,color:#1f2937,stroke-width:1.5px
classDef norm fill:#f0ebff,stroke:#6b46c1,color:#4a2f9c,stroke-width:1.5px
classDef attn fill:#e8f0fe,stroke:#2f6feb,color:#1a4ea8,stroke-width:1.5px
classDef ffn fill:#e6f5ed,stroke:#1a7f4b,color:#155c38,stroke-width:1.5px
X["输入 [1, 6, 4096]"]:::io
N1["RMSNorm(Pre-Norm)
[1, 6, 4096]"]:::norm
QKV["Q / K / V 线性投影
Q [1, 6, 4096] @ [4096, 4096] → 拆 32 头 [1, 32, 6, 128]
K, V @ [4096, 1024] → 8 组头 [1, 8, 6, 128](GQA)"]:::attn
SC["注意力分数
scores = Q @ Kᵀ ÷ √128 → [1, 32, 6, 6]
因果掩码(第 3 个 token 看不到第 5 个)→ softmax
probs @ V → [1, 32, 6, 128]"]:::attn
MG["合并 32 头 → [1, 6, 4096]
输出投影 [4096, 4096]
残差:加回输入 X"]:::attn
N2["第二个 RMSNorm
[1, 6, 4096]"]:::norm
FFN["FFN(SwiGLU 门控)
[1, 6, 4096] 升维到 [1, 6, 11008] × 2(gate / up)
SiLU(gate) × up → 降维回 [1, 6, 4096]
残差:加回子层输入"]:::ffn
O["输出 [1, 6, 4096]
交给下一层,共重复 32 次"]:::io
X --> N1 --> QKV --> SC --> MG --> N2 --> FFN --> O
图 2 · 单层 decoder block:紫色归一化、蓝色注意力、绿色 FFN;两个残差把子层输出加回主干,主干形状始终 [1, 6, 4096]
1. 注意力部分:6 个 token 怎么互相看
| 步骤 | 运算 | 形状 |
| Q 投影 | [1, 6, 4096] @ [4096, 4096] | [1, 6, 4096] → 拆头 [1, 32, 6, 128] |
| K / V 投影(GQA) | [1, 6, 4096] @ [4096, 1024] | [1, 8, 6, 128](只有 8 组头) |
| 分数 | Q @ Kᵀ ÷ √128 | [1, 32, 6, 6] |
| 掩码 + softmax | 因果掩码逐行归一化 | [1, 32, 6, 6](6×6 的"谁能看谁"表) |
| 加权求和 | probs @ V | [1, 32, 6, 128] |
| 合并多头 | 32 头 × 128 → 4096 | [1, 6, 4096] |
两个易混点:① [1, 32, 6, 128] 里的 32 是"头的维度"——6 个 token 各自被 32 个头分别表示成 128 维,和序列长度无关;② [1, 32, 6, 6] 的注意力分数是 6×6 的小方阵:第 i 行第 j 列表示"第 i 个 token 对第 j 个 token 的关注程度",因果掩码把 j>i 的格子置 −∞(未来的内容不许看)。GQA 下 K/V 只有 8 组头,Q 的 32 个头每 4 个一组共用,分数矩阵依然是 [1, 32, 6, 6],只是 K/V 存储省了 4 倍。
2. FFN 部分:升维、门控、降维
FFN 不做 token 之间的交互,只对每个位置独立"加深加工":[1, 6, 4096] 先升到 [1, 6, 11008](SwiGLU 有 gate / up 两份),SiLU(gate) × up 逐元素相乘后再降回 [1, 6, 4096]。参数量上 FFN 约占整层的三分之二,是大模型参数的主要存放地。
3. 残差连接:形状不变的秘密
每个子层的输出都会加回它的输入:output = x + sublayer(norm(x))。这就是主干形状恒为 [1, 6, 4096] 的原因——子层随便加工,主干始终保留原始信息通道。没有残差,32 层堆叠梯度根本传不回去。
四、解码阶段与 KV Cache:为什么长上下文贵
第 6 个位置采样出「是」之后开始自回归。每步只进 1 个新 token,但注意力需要"回看"全部历史,所以每层把历史 K/V 缓存下来:
- 每层缓存的形状:
K, V = [1, 8, S, 128](S 是当前总长度,GQA 8 组头);
- 每 token 每层的 KV 元素数:2(K+V)× 8 头 × 128 维 = 2048 个元素;
- fp16 存储(2 字节)→ 4KB / token / 层;32 层合计 128KB / token;
- 本例 6 个 token 的前缀:6 × 128KB ≈ 768KB——很小;但 32K 上下文的请求:32,000 × 128KB ≈ 4GB,这就是长上下文贵的原因。
五、完整形状流转总表(本例 S=6,B=1)
| 阶段 | 张量 | 形状 | 维度含义(字母对照) |
| ① 分词 | token 序列 | [6] | S = 6 个 token |
| ② 查词表 + 加 batch | token IDs | [1, 6] | B = 1 条请求 |
| ③ embedding | 初始 hidden | [1, 6, 4096] | H = 4096 维向量 |
| ④ ×32 层 block | 层输出 | [1, 6, 4096] | 形状不变,语义逐层加深(内部 ④-a ~ ④-f) |
| ④-a RMSNorm | 归一化 | [1, 6, 4096] | Pre-Norm |
| ④-b Q/K/V 投影 | query / key / value | [1, 32, 6, 128] / [1, 8, 6, 128] | 32 个 Q 头、8 组 KV 头 |
| ④-c Attention | scores → probs @ V | [1, 32, 6, 6] → [1, 32, 6, 128] | 6×6 的"谁看谁"方阵 |
| ④-e FFN 中间 | query | [1, 32, 6, 128] | 32 头 × 6 位置 × 128 头维 |
| ④-b K/V(GQA) | key / value | [1, 8, 6, 128] | 8 组 KV 头 |
| ④-d 合并 + 残差 | 层中间输出 | [1, 6, 4096] | 加回主干 |
| ④-e FFN 中间 | gate / up | [1, 6, 11008] | 升维后的门控表示 |
| ⑥ 最终 norm | 最终 hidden | [1, 6, 4096] | 深层语义向量 |
| ⑦ LM head | logits | [1, 6, 152000] | 6 个位置各 152000 个打分 |
| ⑧ 取最后位 | logits | [1, 152000] | 只看第 6 个位置 |
| ⑨ 采样 | next token | 标量 | 采出「是」,拼回输入进下一轮 |
六、常见易混点
- [1, 6, 4096] vs [1, 32, 6, 6]:前者是每个位置的表示(6 个位置 × 4096 维),后者是注意力分数(每头一张 6×6 的"谁看谁"表)。序列长了后者平方增长——32K 序列时是 [1, 32, 32768, 32768],必须 FlashAttention / 稀疏注意力才能装下;
- 拆头的两种写法:[1, 6, 4096] → [1, 32, 6, 128](主流,先按头分组)或 [1, 6, 32, 128] 再转置,读源码时注意 permute / transpose 出现的位置;
- 训练看 [1, 6, 152000],推理只看 [1, 152000]:训练时 6 个位置的 logits 都要算 loss;生成时只关心最后一个位置;
- Pre-Norm vs Post-Norm:现代模型几乎都是 Pre-Norm(先归一化再进子层),残差通道更干净、深层训练更稳;
- 混合架构的差异:不是每层的"注意力"都是全注意力——如 Qwen3.5 系列大部分层是 Gated DeltaNet 线性注意力,只有少数层是全注意力,但"形状进出不变"的规律不变。
七、本篇版本与更新计划
基础篇 v1.1(2026-09-26):本篇是基础参考知识的第一版,先搭通"具体数字形状流转"的主线,后续计划补充:注意力变体(MQA / MLA / NSA)的形状对比、MoE 层的形状与路由、位置编码(RoPE)在哪个维度起作用、多模态输入的形状拼接、不同序列长度下的显存速查表等。有想看的主题欢迎指出。