目录
- 一、先看一眼:下载下来到底得到了什么
- 二、为什么叫「权重」,为什么它才是唯一值钱的东西
- 三、63.91M 是可以自己算出来的
- 四、PyTorch 到底提供了什么
- 五、为什么这件事非得用显卡
- 六、自检清单
- 附:本文的核查方式
- 附:延伸阅读
一、先看一眼:下载下来到底得到了什么
你写 client.chat.completions.create(model="xxx", ...) 的时候,那个字符串背后是什么?
如果自己去下载一个模型,得到的不是”一个程序”,而是一个文件夹。以本文反复用到的参照物 MiniMind 为例,它 README 里给出的目录结构是:
minimind (root dir)
├─<MiniMind-Model-Name>(例如 minimind-3)
| ├── config.json
| ├── generation_config.json
| ├── model_minimind.py (可选,取决于模型导出形式)
| ├── pytorch_model.bin 或 model.safetensors
| ├── special_tokens_map.json
| ├── tokenizer_config.json
| ├── tokenizer.json
看着零碎,其实就四类东西:
| 文件 | 是什么 | 类比 |
|---|---|---|
config.json + generation_config.json | 这个模型有多宽、多深、词表多大、默认怎么采样 | 图纸 |
tokenizer.json + special_tokens_map.json + tokenizer_config.json | 怎么把文字变成编号、再变回来 | 密码本 |
pytorch_model.bin 或 model.safetensors | 本体,几十 MB 到几百 GB 的数字 | 砌好的墙 |
model_minimind.py | 怎么用上面那些数字算出下一个字 | 施工说明 |
先交代一个后面处处要用到的词——token。
模型并不是按”字”来读文本的。它先把文本切成一个个片段,每个片段叫一个 token,对应词表里的一个编号。上面那个”密码本”干的就是这件事:文字 ↔ 编号。
打个比方:模型眼里的世界没有汉字,只有编号。你写”白日依山尽”,它看到的是类似
[3187, 402, 991, 55, 1204]这样一串数。所谓的”读懂”,是在这串数上做运算。一个 token 对应多少文字是变动的。按 MiniMind README 的自述口径,中文文本上大约 1.5–1.7 字符/token,纯英文大约 4–5 字符/token。下一篇讲训练数据时会具体用到这个换算。
(表里还有一句”默认怎么采样”。采样指的是:模型每生成一个词,并不是永远挑概率最高的那个,而是按概率去”抽签”——所以同一个问题连问两遍,回答会不一样。管这个”抽签有多随机”的旋钮叫 temperature,第三篇讲推理时会用到。)
关键在这句:真正”值钱”的只有第三项。
1.1 为什么选 MiniMind 当参照物
因为它小到可以完整读完,而链路一步不缺——从预训练、SFT、LoRA、蒸馏,到 DPO、PPO、GRPO、Agentic RL 全都有。
这一串缩写看着唬人,所以先别急着记。 它们其实只是同一个问题的八个阶段:“模型不会做某件事,怎么让它学会?”
先给一行版本,后面两篇逐个展开:
| 缩写 | 一句话 |
|---|---|
| 预训练 | 先让它读大量文本,学会”下一个词该是什么” |
| SFT(有监督微调) | 拿”提问 + 标准回答”的数据,教它”被问到时该怎么答” |
| LoRA | 一种省钱的小改法:原模型冻住不动,只训一小块 |
| 蒸馏 | 把强模型的输出当教材,教给弱模型 |
| DPO | 拿”好回答 / 差回答”成对的例子,教它分清好坏 |
| PPO / GRPO | 让它自己生成回答,按打分调整——答得好的做法被鼓励 |
| Agentic RL | 更进一步:让它学会用工具、多步完成一个任务 |
它们不是八个独立的技术,是同一件事的八个阶段。 这一整条链路,第三篇会走完。
按项目 README 的自述:64M 参数、单张 3090、预训练加 SFT 合计约 2.31 小时、租卡成本约 3 元。后面所有”几十亿参数的大模型”的复杂度,都是这个小东西的等比例放大——概念是完全一样的。
它的模型结构代码(model/model_minimind.py)一共 292 行。整个 LoRA 实现(model/model_lora.py)65 行,核心类 18 行。
这不是”玩具”的意思,反过来——它说明难的不是代码,是概念多。
二、为什么叫「权重」,为什么它才是唯一值钱的东西
如果你读过本号之前那篇《AI 是如何识别一只猫的》,应该记得这个结论:
一个神经元就是一个数学函数,有若干输入、一个输出。喂进去一些数字,它会吐出另一个数字。 输出是哪个?这取决于输入,也取决于另一组数字,叫做参数。其中一部分叫权重。
大模型就是这件事放大到极致:几十亿个这样的数字堆在一起,每个数字本身毫无意义,合起来却能让机器说人话。
2.1 结构 vs 权重:图纸和砌好的墙
| 结构(代码) | 权重(数值) | |
|---|---|---|
| 是什么 | 有几层、每层怎么连、怎么算 | 每个连接上的具体数字 |
| 有多少 | 几百行 | 几千万到几千亿个 |
| 谁的 | 公开、通用,所有 Transformer 长得差不多 | 训练的唯一产物 |
| 换一个会怎样 | 换个模型架构,工程量大 | 换个权重,完全变成另一个模型 |
同一份代码,配上不同的权重,就是完全不同的模型。
这不是修辞。MiniMind 仓库里同一套 model_minimind.py,配上 pretrain_768.pth 是个只会接着往下编的”接龙机器”,配上 full_sft_768.pth 就变成了会答话的助手,配上 agent_768.pth 则更擅长调用工具。代码一个字符没改。
所以”预训练一个模型”这句话,物理上就是:找了 6391 万个数字,一个一个挪到合适的位置上。
2.2 这些数字是怎么被写进文件里的
看一眼训练脚本里存权重的那两行(train_full_sft.py:65-66):
state_dict = raw_model.state_dict()
torch.save({k: v.half().cpu() for k, v in state_dict.items()}, ckp)
state_dict()返回的是一张 「参数名 → 张量」的表。这里冒出一个新词:张量就是”装数字的多维方格子”——一批数据、一层权重、一个中间结果,在代码里统统是张量。比如model.layers.0.self_attn.q_proj.weight对应的,就是一张 768 行 × 768 列的数字表.half()把每个数字从 32 位浮点数压成 16 位(fp16)。存储空间省一半,代价是精度低一些——训练时用低精度算、高精度存,是个常见的取舍.cpu()把这些数字从显存搬回内存。显存 = 显卡上的内存,和主板上的内存是两块物理上分开的存储,这个区别第五节还要用torch.save写盘
这就是你下载到的那个几百 MB 的文件。 它里面没有代码,只有一张巨大的表。
三、63.91M 是可以自己算出来的
「64M 参数」听起来像个营销数字。实际上它是能从 config 里推出来的——这一点值得亲自验一遍,因为它决定了你对后面所有数字的信任程度。
MiniMindConfig 的默认值散落在 model/model_minimind.py:12-30 的 __init__ 里。把关键的那几个摘出来(这是摘录,不是原文的连续片段,注释是我加的):
hidden_size=768 # 每个 token 用 768 个数字表示
num_hidden_layers=8 # 8 层
vocab_size=6400 # 词表 6400 个词
num_attention_heads=8 # 8 个查询头
num_key_value_heads=4 # 4 个键值头
head_dim = hidden_size // num_attention_heads # = 96
intermediate_size = ceil(hidden_size * pi / 64) * 64 # = 2432
顺带一提:
intermediate_size那个× π ÷ 64是真的(model_minimind.py:26)。前馈层的宽度按 768 × π ≈ 2413 向上取到 64 的倍数,得 2432。这属于实践惯例(取 hidden 的若干倍),不是什么理论规定——看到这种”长得像有讲究其实很随意”的常数,不用怀疑自己漏学了什么。
把每一层的参数量加起来:
| 组成 | 算式 | 参数量 |
|---|---|---|
| q/k/v/o 四个投影 | (8×96 + 4×96 + 4×96 + 8×96) × 768 | 2,359,296 |
| q_norm + k_norm | 96 × 2 | 192 |
| 前馈层(gate + up + down) | 768×2432×2 + 2432×768 | 5,603,328 |
| 两个 RMSNorm | 768 × 2 | 1,536 |
| 每层小计 | 7,374,528 | |
| × 8 层 | 58,996,224 | |
| 词嵌入层 | 6400 × 768 | 4,915,200 |
| 最后的归一化 | 768 | 768 |
| 合计 | 63,912,192 |
63,912,192,也就是 63.91M。 对上标称的 64M。
再除一下:175e9 / 63.91e6 ≈ 2738。README 说的”约为 GPT-3 的 1/2700”,指的就是这个。
这个数字链值得记住,因为它把”大模型”从一个庞然大物变成了一个可以按计算器验证的东西。后面看到”70B 参数”,你脑子里出现的应该是”一张 700 亿行的表”,而不是”某种神秘的复杂系统”。
表里有两个名字先不深究,但知道它们是什么就够了:
- 词嵌入层:一张查找表。6400 个 token,每个配 768 个数字,所以是
6400 × 768。它的活就是”把 token 编号换成一串数字”,好让后面的计算能进行。 - 前馈层:每一层里负责加工单个 token 的那部分。它不负责 token 之间的信息交换(那是注意力干的——让每个 token 去”看”前面那些 token,从中取信息),只在单个 token 内部做变换。
这两个的细节属于 Transformer 内部结构。Transformer 是今天几乎所有大模型用的那套骨架(2017 年提出),本系列三篇都把它当黑盒——只讲”外面这条训练链路怎么走通”,不拆它的内部零件(见文末「本文不涉及」)。
3.1 一个顺带的设计:词表为什么这么小
词嵌入层那一项 6400 × 768 = 4.9M,占了整个模型的 7.7%。
对照一下 README 给出的各家词表:
| Tokenizer | 词表大小 |
|---|---|
| Yi | 64,000 |
| Qwen2 | 151,643 |
| ChatGLM | 151,329 |
| Mistral | 32,000 |
| Llama 3 | 128,000 |
| MiniMind | 6,400 |
如果把 MiniMind 的词表换成 Qwen2 的 151,643,光嵌入层就会从 4.9M 涨到 116M——比整个模型还大。这正是小模型必须压词表的量化理由。
(还有个小细节:MiniMind 开了 tie_word_embeddings(model_minimind.py:30),嵌入层和输出层共用同一份权重,所以那 4.9M 只算一次。见 model_minimind.py:247。)
四、PyTorch 到底提供了什么
说「PyTorch 是深度学习框架」等于没说。它实际就给你三样东西,第三样是关键。
4.1 张量:能放到显卡上的多维数组
张量就是前面说的那种”装数字的多维方格子”。跟普通的多维数组(比如 NumPy 的 ndarray)比,它多了两个属性:
device:这堆数字现在在哪块卡上——主板内存,还是显存requires_grad:要不要为它算梯度。这个属性就是下一节 4.2 的入口
一个「层」在代码里长这样(model/model_minimind.py:50-60,逐字照抄,完整未删减):
class RMSNorm(torch.nn.Module):
def __init__(self, dim: int, eps: float = 1e-5):
super().__init__()
self.eps = eps
self.weight = nn.Parameter(torch.ones(dim))
def norm(self, x):
return x * torch.rsqrt(x.pow(2).mean(-1, keepdim=True) + self.eps)
def forward(self, x):
return (self.weight * self.norm(x.float())).type_as(x)
层 = 一个参数 + 一段计算。 nn.Parameter 那行就是”这里有一组要被训练的数字”,forward 就是”怎么用”。整个 292 行的模型,就是十来个这样的东西拼起来的。
4.2 autograd:自动求导——这才是它真正的价值
训练的核心动作是:算出”每个参数该往哪个方向挪”,然后挪一点点。
数学上这一堆叫链式法则求导。手推一遍几百层网络的导数对人类来说不现实。PyTorch 的做法是:只要你把计算写成了张量运算,它就自动把这个计算过程的导数图也建出来,你一句 .backward() 就能拿到所有参数的梯度。
这就是 loss.backward() 那一行在做的事(完整循环在下一篇展开)。
| 没有 autograd 的世界 | 有 autograd 的世界 |
|---|---|
| 每加一个新层,手推一遍导数公式 | 加层只是多写几行代码 |
| 抄论文还得抄对导数 | 论文里的公式直接用 |
它是”能快速试错”和”不能”之间的分界线。 这也是为什么整个深度学习生态都建立在 PyTorch(或同类)之上,而不是建立在 NumPy 之上——NumPy 只有 4.1,没有 4.2。
4.3 设备调度:把数据送到对的地方
你在训练脚本里会反复看到 .to(device)、autocast、scaler 这些词。它们都在处理同一件事:数据在 CPU 内存和显存之间来回搬,以及用低精度算、高精度存。
下一篇会展开,这里先记住它们不是可选项——显卡和内存是两块物理上分开的存储,忘了搬就是直接报错。
五、为什么这件事非得用显卡
5.1 训练时间都花在同一个动作上
把前面 RMSNorm.forward 那一行拆开看:
x * torch.rsqrt(x.pow(2).mean(-1, keepdim=True) + self.eps)
……它不是矩阵乘法。但 Attention 里的 self.q_proj(x)、FeedForward 里的每个 nn.Linear,全都是矩阵乘法(y = xW)。而 nn.Linear 在 8 层里出现了几十次,每次都要乘过整批数据。
一个粗略但够用的说法:神经网络训练里绝大多数计算量,是矩阵乘加。
5.2 CPU 和 GPU 的形状不一样
这里最容易讲错。不是”GPU 更快”,是”GPU 更宽”。
| CPU | GPU | |
|---|---|---|
| 核心数 | 几个到几十个 | 几千个 |
| 单核能力 | 强:分支预测、乱序执行、大缓存 | 弱:基本只会乘加 |
| 擅长 | 复杂的、有分支的逻辑 | 大量相同的简单运算 |
打个比方:CPU 是几个大厨,GPU 是几千个切菜工。
炒一道需要临场判断的菜,你得找大厨。但要切一万斤土豆,多请几个大厨没用——你需要的是能同时动手的人。而训练模型,恰好就是”切一万斤土豆”这类活:每一步的计算都简单到无聊,但量极其巨大。
这解释了一个常见困惑:为什么 GPU 打游戏时那么灵活,写起普通业务代码却没优势。因为业务代码是”炒菜”,分支判断多、每一步都不同;而矩阵乘法是”切土豆”,几千个核可以各自切各自的,互不干扰。
5.3 显存里装的不止权重
新手常见的一个误判:“模型 64M,那我 8G 显存够了吧。”
不够。权重只是第一项。训练时显存里至少还有四项——先说清两个会反复出现的词:
- 前向传播:数据从第一层流到最后一层,算出结果
- 反向传播:从结果倒推回去,算出每个参数该往哪个方向挪
(这两个词是训练循环的两半,下一篇的主线。)
| 占用项 | 是什么 |
|---|---|
| 权重 | 模型本身 |
| 梯度 | 反向传播算出来的”每个参数该往哪挪”,和权重一样大 |
| 优化器状态 | 优化器是”按梯度去挪参数”的那个部件。常用的 AdamW 要为每个参数额外记两个数——形象地说,一个记”最近一直往哪边挪”,一个记”挪的幅度有多大”,加起来又是权重的两倍 |
| 激活值 | 前向传播过程中每一层的中间结果。反向传播时要回头用,所以不能扔 |
README 里有一个现成的对照:PPO 因为要同时维护 Actor 和 Critic 两个网络,显存占用约为单网络方法的 1.5–2 倍。
(这两个词先不展开:Actor 是”负责说话”的那个网络,Critic 是”负责打分”的那个网络。 两件事各用一个网络干,显存自然翻倍。第三篇讲强化学习时会说明为什么要这么拆。)
反过来,这也是 LoRA 这类方法存在的理由。LoRA 的思路是:原模型冻住不动,只在旁边挂一小块可训练的参数。 既然只有那一小块需要梯度,梯度和优化器状态也只需要为它准备——显存大头一下就省掉了。MiniMind 的 LoRA 脚本会直接把这几个数打印出来:
LLM 总参数量: xx.xxx M
LoRA 参数量: x.xxx M
LoRA 参数占比: x.xx%
(trainer/train_lora.py:126-128,“xx” 处是运行时才有的实际值。)
这也是为什么谈模型大小要分”能不能跑起来”和”能不能训起来”——推理只需要权重,训练要的是权重的若干倍。 下一篇讲训练循环时会具体看到每一份数据在哪一行产生。
六、自检清单
读完这篇,如果下面每一条你都能答上来,说明这个开头接上了:
- 下载一个模型,得到的文件夹里那四类文件分别是什么?
- 为什么说”结构是公开的,权重才是训练产物”?同一份代码配上不同权重会发生什么?
- 63.91M 这个数是怎么从
config.json推出来的?为什么词表从 6400 换成 151,643 会让模型”变大得离谱”? -
intermediate_size里那个× π是什么来路? - PyTorch 的三样东西里,哪一样是”没有它整个生态就不成立”的?为什么 NumPy 不够?
- 为什么说 GPU 不是”更快”而是”更宽”?什么样的计算适合它,什么样的不适合?
- 训练时显存里除了权重还有什么?为什么 PPO 的显存是单网络的 1.5–2 倍?
- 为什么”这个模型我 8G 显卡跑得动”不代表”我 8G 显卡训得动”?
附:本文的核查方式
代码引用:本文所有代码块均逐字取自 jingyaogong/minimind 仓库(2026-10-16 拉取的 master),标注了文件名与行号。
本文的代码排版约定(后续两篇同):
- 标了「逐字照抄」的块,含缩进与空行与源码完全一致,你可以直接拿去比对全文,不会有一处出入。
- 唯一一处例外是 §3 那段 config 摘录——它不是源码的连续片段,是我为了对照方便拼的,注释也是我加的。之所以特别说明,是因为它看起来最像原文。
- 源码里那些看着像笔误的地方(比如
from_weight!= 'none'缺个空格、intermediate_size乘 π),保持原样,不做”顺手修一下”。
数字来源:
| 数字 | 来源 |
|---|---|
| 模型结构代码 292 行、LoRA 65 行、词表 6400 | 直接读仓库文件得出 |
| 63,912,192(63.91M)参数 | 按 config.json 各字段自行核算,非引述 |
| GPT-3 的约 1/2700 | 项目 README 自述;本文验算为 ≈1/2738 |
| 2.31 小时、约 3 元、单卡 3090 | 项目 README 的”训练开销”表(按作者自述) |
| 各家词表大小 | 项目 README 的对比表 |
| PPO 显存约为单网络 1.5–2 倍 | 项目 README 自述 |
关于”训练开销”一类的数字:本文中凡涉及训练时长、成本、显存倍数的,都来自 MiniMind 项目 README 的作者自述,我没有独立复现。它们的作用是给出量级感,不是承诺。
本文不涉及(有意略过):Transformer 内部 self-attention 具体怎么算、位置编码、MoE 的路由细节。这三点本系列三篇都不涉及——它们的定位是”这条训练链路怎么走通”,不是”模型内部怎么算”。需要速查版可以看本号的《AI 应用开发面试笔记》(那篇是面试速查向,密度不同)。
附:延伸阅读
- 参照项目:jingyaogong/minimind —— 本文与后续两篇都以它为例
- 本号相关:
- 《AI 是如何识别一只猫的:最好懂的神经网络解释》 —— 神经元、权重、参数的最基础解释,本文假设你读过
- 《模型正在被「故意变笨」》 —— 知识蒸馏,第三篇会回到这个话题
- 《AI 应用开发面试笔记》 —— 如果你需要的是能背下来的版本
- 下一篇:预训练与 SFT——这 6391 万个数字,是怎么被一个一个挪到位的