大模型训练笔记(一):模型下载下来到底是什么,以及为什么它得靠显卡

目录


一、先看一眼:下载下来到底得到了什么

你写 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) × 7682,359,296
q_norm + k_norm96 × 2192
前馈层(gate + up + down)768×2432×2 + 2432×7685,603,328
两个 RMSNorm768 × 21,536
每层小计7,374,528
× 8 层58,996,224
词嵌入层6400 × 7684,915,200
最后的归一化768768
合计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词表大小
Yi64,000
Qwen2151,643
ChatGLM151,329
Mistral32,000
Llama 3128,000
MiniMind6,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 更宽”。

CPUGPU
核心数几个到几十个几千个
单核能力强:分支预测、乱序执行、大缓存弱:基本只会乘加
擅长复杂的、有分支的逻辑大量相同的简单运算

打个比方: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 应用开发面试笔记》(那篇是面试速查向,密度不同)。


附:延伸阅读