跳转至

1. 必要概念:用开发中熟悉的东西来理解

本章不讲数学。目标是让你在后面每一格代码执行时,知道屏幕上的数字是什么意思。每个概念都给一个开发中常见的类比(数据库、Git、Web 服务等);类比只帮助建立直觉,不等于严格定义。

本章速览

  • 本章做什么:从“模型是磁盘上的一个目录”讲起,依次讲 Token、推理、训练与 loss、SFT/LoRA/QLoRA、Step 与 Epoch、七个库的分工、显存
  • 需要准备:什么都不用装;有一台能看网页的电脑即可
  • 做完得到:后面 Notebook 里出现的每个参数和数字都能对上号;遇到报错知道该去哪个库找原因

8 节内容按“先有模型、再看它怎么读文字、怎么回答、怎么学习、怎么省显存”的顺序排列,后一节依赖前一节,建议按顺序读。

1.1 模型在磁盘上是一个目录

打开 models/qwen3-4b/,一个"模型"物理上就是这些文件。注意:在自己电脑上用 --tokenizer-only 下载的目录(第 2.8 节)只有分词器和配置,约 15 MB,没有三个权重分片;分片要到 GPU 主机上完整下载(第 3.4 节)后才会出现,这是正常的,不是下载中断。

文件 是什么 类比
config.json 结构定义:36 层、隐藏维度 2560、词表 151,936 等 数据库的建表语句(schema)
model-00001-of-00003.safetensors 到 model-00003-of-00003.safetensors 权重,约 40 亿个数字,BF16 共 8.0 GB,分 3 个分片;仅完整下载后才有 数据库的数据文件
model.safetensors.index.json 哪个权重在哪个分片 分片路由表
tokenizer.json、vocab.json、merges.txt 分词器:文字 ↔ 整数编号的规则 序列化协议
tokenizer_config.json 含 chat_template:system/user/assistant 怎样拼成一段文本 请求报文格式
generation_config.json 默认采样参数 服务端默认配置
model-lock.json 本教程记录的版本号(commit) package-lock.json

"加载模型"就是按 config.json 建好空结构,再把 safetensors 里的数字填进去,放到显存。后面会碰到三种目录:基座(上面这个)、adapter(只有 LoRA 增量,约 130 MB,必须搭配基座)、合并模型(基座 + 增量算好后的完整目录,又是 8 GB)。vLLM 加载的是基座或合并模型目录。

1.2 Token:模型眼里的文字

模型不读汉字,读整数。分词器把文本切成片段(Token),每个片段对应词表里的一个编号。用本项目的分词器切一句话:

"为什么白天亮、晚上黑?"
→ ['为什么', '白天', '亮', '、', '晚上', '黑', '?']
→ [100678, 106772, 99538, 5373, 104030, 56652, 11319]

"The Earth rotates."
→ ['The', ' Earth', ' rotates', '.']
→ [785, 9237, 89059, 13]

常见词是一个 Token,生僻字可能拆成几个;英文的空格附在下一个词上。所以 Token 数不等于字数,中文大约 1 个 Token ≈ 1.3~1.5 个汉字。本教程所有"长度上限 4096"都指 Token 数。

词表里还有几个特殊 Token,不对应任何文字,而是结构标记。Qwen 的对话格式(chat_template)把一次请求拼成这样:

<|im_start|>system
你是陪小学生探索科学的伙伴。……<|im_end|>
<|im_start|>user
为什么白天亮、晚上黑?<|im_end|>
<|im_start|>assistant
地球在不停转,我们住的地方转向太阳就是白天……<|im_end|>

<|im_end|>(编号 151645)是结束标记 EOS:模型生成到它就停。训练时必须让模型学会在回答末尾输出它,否则部署后回答停不下来。

1.3 参数与推理:一个有 40 亿个系数的函数

模型本质上是一个函数:输入一串 Token 编号,输出"下一个 Token 是词表中每一个的概率"。这个函数里有约 40 亿个可调系数,叫参数(或权重),就是 safetensors 里的数字。

推理就是反复调用这个函数:给它 prompt,取概率最高的 Token(贪心解码,do_sample=False),接到输入末尾,再调用一次,直到输出 EOS 或达到 max_new_tokens。参数在推理时一个都不改。

temperature=0 和贪心解码一样,每次都选概率最高的,所以同样输入会得到同样输出,便于对比;大于 0 时按概率随机抽,回答更多样但不可复现。

1.4 训练:用误差修正参数

训练是反过来:给模型看"上下文 + 参考回答",让它在参考回答的每个位置预测下一个 Token,再把预测和参考的差距转成一个数,叫 loss。

loss 大致范围 含义
约 3 以上 模型对参考回答的每个 Token 都很"意外";随机猜的水平是 ln(151936) ≈ 11.9
1~2 原模型加好提示词,面对本项目参考回答的典型起点
0.5 左右 模型已经很接近参考回答的措辞
接近 0 几乎逐字背下来了;在验证集上若同时上升,就是过拟合

loss 是"对参考回答有多意外",不是"科学知识得分"。它只能告诉你模型是否在学会训练数据的说法,不能告诉你回答是否正确。

有了 loss,就能算出每个参数"往哪个方向调一点能让 loss 变小",这就是梯度(反向传播算出来的)。优化器(AdamW)按梯度更新参数,学习率(1e-4)决定每次改多大步:太大会把原模型的能力冲掉,太小学不动。一次"算 loss → 算梯度 → 更新参数"叫一个 step。

类比:loss 像 CI 的失败用例数,梯度像告诉你每个配置项该往哪边调,学习率像每次只敢改多少,step 像一次提交。

1.5 SFT、LoRA、QLoRA:三个独立的选择

三个词分别回答三个问题,可以独立组合:

SFT(监督微调)回答"用什么教"。 给模型看成对的"输入 → 期望输出",也就是第 2 章的对话数据。本项目只对"参考回答 + EOS"的位置算 loss,上下文部分不算(第 2.5 节会亲眼看到这个掩码)。

LoRA 回答"改哪些参数"。 全量微调要更新 40 亿个参数,每个参数还要配梯度和优化器状态,单卡放不下,而且每个实验版本都要保存 8 GB。LoRA 的做法是:原权重 W 一个都不动,在旁边加两个小矩阵 A、B,只训练它们:

实际使用的权重 = W(冻结) + (alpha / r) × B × A

类比 Git:基座是主分支,LoRA 是一个很小的 diff,训练只改 diff,adapter/ 目录保存的就是这个 diff。"合并"等于把 diff 应用到主分支生成一份新的完整文件。

r(秩)决定小矩阵的宽度,也就是 diff 的容量。本项目 r=16,7 类线性层 × 36 层加起来约 3300 万个可训练参数,占总参数的 0.8%,adapter 文件约 130 MB。alpha 是缩放系数,本项目固定 alpha = 2 × r。

QLoRA 回答"冻结的基座怎么放进显存"。 冻结的权重反正不改,就把它们从 BF16(每个数 2 字节)压缩成 4 bit(每个数 0.5 字节)存在显存里,计算时临时还原成 BF16。4B 模型的权重从 8 GB 降到约 2.5 GB,24 GB 的卡才有余量放训练中间结果。NF4 是压缩格式的名字,double_quant 是把压缩用的辅助参数也压缩一下。LoRA 的小矩阵本身仍是 BF16,不压缩。

深入阅读:LoRA 原论文、QLoRA 原论文。

1.6 样本、Batch、Step、Epoch

概念 本项目的值 含义
样本 1150 条 一条"上下文 + 一个参考回答";一段 2 轮对话展开成 2 条
Micro-batch 1 一次前向/反向放进显存的样本数
梯度累积 16 攒 16 条样本的梯度再更新一次参数;效果接近 batch=16,显存只占 1 条
Step 72 优化器更新次数 = ⌈1150 ÷ 16⌉;进度条和 loss 曲线的横轴
Epoch 1 所有样本过一遍
Checkpoint 每 25 步 可恢复训练的快照:adapter + 优化器状态 + 进度

类比:样本是一条消息,梯度累积是攒够 16 条再批量写库,step 是一次写库,epoch 是把队列消费一遍。

1.7 七个库各管什么

训练环境里会装七个库,报错信息里会出现它们的名字:

库 负责 你会在哪里看到它
PyTorch 张量计算、GPU 调度、自动求梯度;其他库都建在它上面 torch.cuda.*、显存统计
Transformers 按 config.json 搭建模型结构、加载权重、分词器、聊天模板、generate() AutoModelForCausalLM、AutoTokenizer
PEFT 给模型加 LoRA 层、冻结原参数、保存/加载 adapter、合并 LoraConfig、get_peft_model、PeftModel
TRL 为 SFT 定制的训练器:按掩码算 loss、管理 step/epoch/检查点 SFTConfig、SFTTrainer
bitsandbytes 4 bit 量化的存储格式和 GPU 计算核 BitsAndBytesConfig,只能在 NVIDIA GPU 上用
accelerate 把模型放到指定设备、混合精度;TRL 内部依赖它 device_map
datasets 训练器要求的数据容器 Dataset.from_list

部署时换另一个库:vLLM 自带推理引擎和 HTTP 服务,只需要模型目录,不需要上面七个库里的 PEFT 和 TRL。

调用关系:Transformers 加载模型 → bitsandbytes 把权重压成 4 bit → PEFT 加上 LoRA 层 → datasets 装好样本 → TRL 跑训练循环 → PEFT 保存 adapter → PEFT 合并 → vLLM 部署。

1.8 内存、显存与磁盘

资源 主要用途 容易误解的地方
CPU 文本处理、分词、加载与调度 核心数多不等于适合大模型训练
RAM(主机内存) 加载权重时的中转、CPU 上合并模型 100 GB RAM 不能替代 24 GB 显存:训练计算在 GPU 上,数据必须在显存里
GPU 大量矩阵运算 CUDA 主线依赖 NVIDIA;Mac 的 MPS 是另一条路线,本教程不支持
显存(VRAM) 权重、激活、梯度、优化器状态、推理 KV Cache 模型文件 8 GB 不等于显存占 8 GB:训练要加中间结果,推理要加 KV Cache
磁盘 基座、环境、检查点、合并模型 每个检查点含优化器状态;每次合并又是 8 GB

训练显存的组成和实际读数见第 4.3 节;推理显存的估算见第 5.3 节。

验收: 能说出一个模型目录里每个文件是什么;能解释 loss 从 2 降到 0.6 意味着什么、为什么不能据此说回答更好;能说出 1150 条样本为什么是 72 步;能说出七个库中哪个负责 LoRA、哪个负责训练循环。