🧪 大模型结构实验室:动手演示

四个浏览器内跑起来的实验台:亲手切 token、亲手算注意力、亲眼看前向传播、亲手算 KV Cache——大模型结构不再是黑盒名词
动手实验注意力可视化前向传播动画KV Cache 计算器源码精读
🎯 本页学习目标
1. 通过切词实验理解 tokenizer:为什么「strawberry」里有几个 r 这类问题会难倒大模型
2. 亲手调缩放系数,观察 softmax 打分如何从「平均分」变成「一票聚焦」,理解注意力温度
3. 能按六个阶段口述一个 token 走完 Transformer 前向传播的全过程
4. 会用公式估算任意配置的 KV Cache 显存,并说出 MHA/GQA/MLA 三方案的量级差异
建议用时:约 35 分钟(动手) | 前置:建议先读 07-06 演进全景07-01 大模型基础
说明:本页全部实验在你的浏览器本地计算(纯 JS,无网络请求),教学用简化配置,真实模型同原理、规模不同

1 实验室地图

实验台你能动手做什么对应的知识
① Tokenizer 切词台输入任意文本,看它被切成 token 块BPE 子词、为什么数字母会错
② 注意力显微镜调缩放系数,看 6×6 注意力权重热力图实时变化Q·K 打分、softmax、因果掩码、指代消解
③ 前向传播动画播放/单步/调速,看 token 走完全部六站Embedding→注意力→FFN→残差→输出
④ KV Cache 计算器拖滑块配模型,实时算三种注意力方案的显存KV Cache 公式、GQA/MLA 的意义

建议顺序:先玩 ①② 找手感,再看 ③ 把全流程串起来,最后用 ④ 把「工程问题」变成可计算的数字。每个实验台下面都配了对应源码精读(第 6 节),先玩再看代码,或边玩边对照。

2 实验台① Tokenizer 切词台

大模型眼里的文字不是「字」也不是「词」,而是 token(子词单元)。切词器(BPE 等算法)按统计规律把高频组合编进词表:常见词整块保留,生僻词拆成小块。先用下面的实验台亲手切一切(本台内置一个约 100 条的教学版迷你词表,真实大模型词表有 10 万+ 条,原理相同):

输入文本,看切词结果:
💡 观察重点:常见词(「大模型」「注意力」)是一整块,而模型「读」的就是这些块的编号——它从来没有直接见过字母!试着输入「strawberry」再切一次:10 个字母被切成 3 块,模型看到的是 str + aw + berry 这样的「积木」,要数里面有几个 r,得让积木边界处的字符「显形」,这正是大模型数不清字母的根源(也解释了它为什么「背得出」单词却「拼不对」单词)。

3 实验台② 注意力显微镜:亲手算一遍注意力

现在把「注意力」放到显微镜下。玩具句:「小猫 坐在 垫子上 因为 它 很困」——其中的「它」指谁?人一眼看出是小猫,注意力层能不能学会?每个词先被表示成 4 维特征向量(动作性/物体性/位置性/指代性,教学设定),再经过两个可学习矩阵 Wq、Wk 投影出 Q(想找什么)和 K(能提供什么),然后真刀真枪算一遍 打分 → 缩放 → softmax:

\mathrm{score}(i,j)=\frac{Q_i\cdot K_j}{\sqrt{d_k}}\times s,\qquad \mathrm{attn}_{ij}=\mathrm{softmax}_j\big(\mathrm{score}(i,j)\big)

其中 s 是缩放系数(下面滑块控制,真实模型里固定为 1,由训练学出合适的分布)。行=「谁在看(Query)」,列=「看谁(Key)」,颜色越深权重越大:

💡 三个必做操作:① 找「它」那一行——默认参数下 小猫 ≈ 42%、垫子上 ≈ 30%,注意力真的把「它」指向了实体词(指代消解涌现);② 把缩放系数拖到 1 再拖到 12——s 小时每行权重接近平均(谁都不聚焦),s 大时一行只剩一两个深格(一票聚焦):这就是 softmax 的「温度」效应,推理模型长思维链的稳定性也和它相关;③ 勾上「因果掩码」——右上角全部归零,每个词只看自己和前文,这正是 Decoder 生成时「不许偷看未来」的实现方式。

4 实验台③ 前向传播动画:一个 token 的六站之旅

注意力只是其中一站。现在把整个 Decoder 层的流水线搭起来,跟着一个 token 走完六站——用控制条播放、单步、调速(空格键=播放/暂停):

| 阶段 1/6
① 输入分词 机器 文本切成 token 序列 ② Embedding 查表变向量 +位置信息(RoPE) ③ 自注意力 词与词开会 交换信息(Q·K·V) ④ FFN 前馈 先放大 4 倍 再压回(SwiGLU) ⑤ 残差 ×N 层 L-1 层 L 重复几十层 越深懂越多 ⑥ 输出采样 下一个词 概率分布 token 小球
图① Transformer 前向传播六站(交互动画):token 小球沿轨道流动,高亮站随播放推进,当前阶段解说见下方蓝框
💡 看完动画应该能答:为什么说「注意力负责交流、FFN 负责消化」?——③ 站里词与词横向交换信息,④ 站里每个位置独立做非线性变换(知识主要存在 FFN 参数里);残差捷径(⑤ 站)让梯度能「抄近道」流回浅层,几十层才训得动。这套流程乘以 N 层(如 60 层)再接输出头,就是一个完整的大模型。

5 实验台④ KV Cache 计算器:把工程问题变成数字

07-06 页说过:推理时 KV Cache 是显存与带宽的第一大户。公式只有一行:

\mathrm{KV\ Cache}=2\,\times\,L\,\times\,s\,\times\,h_{kv}\,\times\,d_h\,\times\,b\,\times\,\mathrm{batch}

(2=K 和 V 各一份,L=层数,s=序列长度,hkv=KV 头数,dh=头维度,b=每元素字节数)。拖动滑块,看同一个配置下 MHA(不省)/ GQA(分组共享)/ MLA(低秩压缩) 三方案的显存差距——这就是 07-06 第 5.2 节那条演进线的代价账:

精度: 快捷配置:
MHA(每个 Q 头独享 KV)
10.7 GB
GQA(8 组共享,当前滑块)
2.7 GB
MLA(低秩压缩,潜向量 576 维)
1.5 GB
💡 试这三组配置:① 默认(70B 级,32K 上下文):MHA 要 40+ GB——一张 80GB 卡的光 KV Cache 就装不下多少并发;GQA 直接砍到 1/4;MLA 再砍半。② 把上下文拉到 1M(max 滑块右端):任何方案都暴涨,这就是为什么 1M 上下文必须配合稀疏注意力(07-06 问题③④)。③ 切到 INT4 精度:全部数字减半再减半——量化是最便宜的「显存扩容」。decode 每生成一个 token 都要把这些 GB 从显存读一遍(显存带宽约 3TB/s 时,10GB KV 意味着每 token 至少 3ms 纯搬运),这是本实验最想让你带走的体感。

6 结构代码精读:五段代码看懂大模型

下面五段代码与四个实验台一一对应,全部是「骨架级」真实逻辑(简化到每段十几行,保留主干)。逐段给出「逐行讲人话」。

6.1 注意力的最小实现(实验台②的完整版)

import numpy as np

def attention(Q, K, V, causal=False):            # Q,K,V 形状 (n词, d_k维)
    d_k = Q.shape[-1]
    scores = Q @ K.T / np.sqrt(d_k)              # ① 打分:每个词的Q和每个词的K做点积
    scores = scores - scores.max(-1, keepdims=True)  # ② 减最大值防 exp 溢出(结果不变)
    w = np.exp(scores)
    w = w / w.sum(-1, keepdims=True)             # ③ 每行 softmax 归一化成权重
    if causal:                                   # ④ 因果掩码(Decoder 需要)
        mask = np.tril(np.ones(w.shape[-2:]))    #    下三角=1:只许看自己+前文
        w = np.where(mask > 0, w, 0)
        w = w / w.sum(-1, keepdims=True)
    return w @ V                                  # ⑤ 每个词的新表示=按权重汇聚全句的V

讲人话:①就是实验台②热力图里每个格子的来历;③对应你拖动的缩放滑块(s 大 → exp 拉开差距 → 尖锐);④的「下三角」就是你勾选的因果掩码;⑤得到的新表示送进下一层。整个注意力不过五步,大模型最核心的计算就这么多——难的不是公式,是把它的显存与带宽管好。

6.2 RoPE 旋转位置编码:位置是一种「角度」

def rope(q, pos, theta=10000.0):                 # q:(d,) 一个词的查询向量,pos=它的位置
    half = q.shape[-1] // 2
    angles = pos / (theta ** (2*np.arange(half)/q.shape[-1]))
    cos, sin = np.cos(angles), np.sin(angles)    # 位置 pos → 每个二维子空间一个旋转角
    q2 = q.reshape(-1, 2)                         # 向量拆成 d/2 个平面
    return np.concatenate([q2[:,0]*cos - q2[:,1]*sin,
                           q2[:,0]*sin + q2[:,1]*cos])   # 每个平面旋转对应角度

讲人话:把向量的相邻两维看成平面上的一个点,位置 pos 决定旋转多少度。两个词做点积时,旋转角自动相减——点积结果只依赖「相对位置差」,这就是 RoPE 天然表达相对位置、且能用插值/YaRN 外推到训练时没见过的长度的原因(07-06 第 5.1 节)。

6.3 GQA:一行代码的显存减半术

# 32 个 Q 头,只有 8 份 K/V:第 0~3 号 Q 头共用第 0 份 KV,以此类推
kv_expanded = kv.repeat(4, axis=1)               # (s, 8, d) -> (s, 32, d) 逻辑展开
scores = q @ kv_expanded.transpose(0, 2, 1)      # 照常做点积,数学形式不变

讲人话:GQA 的全部思想就是「4 个 Q 共享 1 份 KV」——存储时只存 8 份,计算时逻辑展开(现代框架用广播视图,不真复制)。KV Cache 和显存读取量直接除以 4,精度几乎无损。这就是它成为 Llama-2-70B 之后开源标配的原因:一行代码,三分之一显存。

6.4 MoE 路由:top-k 选专家

logits = x @ W_router.T                          # 路由器给每个专家打分 (n_tok, n_expert)
topk_idx = np.argpartition(logits, -8)[-8:]      # 只要分数最高的 8 个专家
w = softmax(logits[topk_idx])                    # 这 8 个人的分数归一化成权重
y = sum(wi * experts[i](x) for wi, i in zip(w, topk_idx))  # 加权求和,其余专家不参与

讲人话:DeepSeek-V3 有 256 个专家,每个 token 只激活 8 个——「argpartition 取前 8」这行就是全部路由逻辑。训练时的难点不在这一行,而在负载均衡:路由器偏心会让少数专家累死、多数专家闲置(07-01 第 8 节的辅助损失/动态偏置就是治这个的)。

6.5 KV Cache 生成循环:为什么能「一个字一个字往外蹦」

past_kv = None                                   # 缓存:历史所有 token 的 K/V
for step in range(max_new_tokens):
    logits, past_kv = model(last_token, past_kv=past_kv)  # 只喂最新的 1 个 token!
    last_token = sample(logits)                   # 从概率分布采样下一个词
    output.append(last_token)                     # 历史 K/V 已在缓存,无需重算

讲人话:没有 KV Cache 时,生成第 1000 个词要把前 999 个词全部重算一遍——O(n²) 的浪费。缓存后每步只算 1 个新 token 的 Q/K/V,历史的直接取。代价就是实验台④那笔账:省下的是计算,欠下的是显存——cache 越滚越大,decode 阶段每步都要全量读它,这就是「访存密集」的来源(07-06 图⑦)。

7 常见误区

⚠️ 误区一:「注意力权重可以直接解释模型在想什么」。本页的玩具模型确实如此,但真实多层模型里,某一层的注意力权重只是信息流动的一小部分(还有 FFN、残差、多层叠加),「attention≠explanation」是可解释性领域的共识。实验台②教的是机制,不是读心术。
⚠️ 误区二:「KV Cache 是可选的优化」。没有它,生成第 N 个词要重算前 N-1 个词的全部 K/V,长文本生成慢到不可用。它是所有推理引擎的标配地基,工程问题(显存/带宽/并发)全部围绕它展开。
⚠️ 误区三:「token 越少越好」。压缩率高确实省上下文,但过度压缩(词表里全是长块)会让模型连「新造的词」「数字」「代码变量名」都切不开。tokenizer 是压缩率与覆盖面的权衡,不是一味求省。

8 自测

1. 大模型经常数错「strawberry 里有几个 r」,根本原因是?
💡 解析: C 正确——实验台①演示了切词:模型输入是 token 编号序列,字母层面的问题需要「穿过块边界」才能回答,本质是表示粒度问题。A 无关;B 现代模型词表覆盖如此常见词;D 无中生有,位置编码作用于 token 序列,与字母拼写无关。
2. 实验台②里,把缩放系数 s 从 1 调到 12,注意力分布会?
💡 解析: B 正确——s 放大了打分差距,softmax 对「差距」是指数敏感的:分数差一点点,权重差一大截。s→0 时退化为平均;s→∞ 时退化为「只看最高分的那一个词」。真实模型里这个平衡由训练学得,1/√d_k 是让方差保持在合适区间的标准初始化选择。
3. Decoder 生成文本时「因果掩码」的作用是?
💡 解析: A 正确——如果训练时能偷看未来,「预测下一个词」就变成「抄答案」,模型学不到真正的预测能力。C 有迷惑性:掩码确实让矩阵少算一半(上三角),但这是任务正确性的结果而非目的;B、D 与因果掩码无关。
4. 一个 80 层模型,GQA 8 组 KV 头、头维 128、FP16 精度、32K 上下文、batch=1,KV Cache 约多大?
💡 解析: B 正确——代入公式:2×80×32768×8×128×2B = 10,737,418,240 B ≈ 10 GB。这就是实验台④的默认值(Llama-3-70B 同配置):一张 80GB 的卡,光 KV Cache 就占了 1/8,剩下留给权重(70B FP16 要 140 GB,还得量化)和激活。数字要能心算量级,这是大模型部署的基本功。
5. 关于 MHA→GQA→MLA 三方案的显存排序(同配置),正确的是?
💡 解析: C 正确——实验台④三条柱直观展示:MLA(低秩压缩潜向量)最省,GQA(分组共享)次之,MHA(全头独立)最大。同时注意精度排序大致相反:MLA 设计目标是「压到 1/10 级还几乎不掉点」,GQA 几乎无损,MHA 是精度基线。工程选择永远是「精度损失可接受范围内,显存带宽越省越好」。
6. 「残差连接」在六站动画的第⑤站,它的主要作用是?
💡 解析: D 正确——输出 = 层(x)+ x,梯度反向传播时可以沿这条捷径无损直达浅层,避免深层网络的梯度消失/退化(ResNet 一脉相承的思想,Transformer 把它用到极致)。A 无关(残差不加参数);B 错(它是搭配不是替代);C 错(残差中间结果还要为反向传播保存,反而略增显存)。

9 小结

四个实验台串起来,就是大模型的完整微观图景:tokenizer 决定模型「看见什么」(token 而非文字),注意力决定「怎么交流」(打分-缩放-softmax-加权),六站流水线决定「怎么思考」(嵌入→交流→消化→堆叠→采样),KV Cache 决定「多贵」(省计算欠显存,一切推理工程围绕它)。把这四件事讲清楚,你就超过了绝大多数只会报模型名的人。配合 07-06 的宏观演进史与 07-01/07-02 的架构深讲,「从 Transformer 到前沿」这条线就闭环了。

参考资料(整理更新至 2026-08)