第 0 章 环境准备与第一次运行
|
本章导读:把环境一次性配置到位,并用 学完本章你将能够:
产出与代码: |
0.1 硬件与软件要求
全书不依赖 GPU,没有显卡也能编译并运行全部章节。真正的门槛在版本配套: LibTorch 与 CUDA 必须互相匹配。新手环境问题大多出在这里。请直接使用下表实测过的组合:
| 组件 | 要求 | 说明 |
|---|---|---|
LibTorch |
2.x CUDA 版 |
实测 2.13.0+cu130;解压到 |
CUDA Toolkit |
与 LibTorch 配套 |
实测 CUDA 13.0;没有 GPU 也能编译运行全部章节 |
编译器 |
GCC ≥ 8(C++17) |
实测 GCC 11;系统默认版本过旧时,用 |
CMake |
≥ 3.16 |
— |
RE2 |
|
仅第 2 章需要:torchtext 官方 GPT-2 BPE 的正则依赖 |
Ollama |
任意近期版本 |
仅第 7 章 LLM-as-judge 评估用;不装不影响 0–6、8–13 章 |
GPU |
可选 |
有则自动启用;medium(355M)训练建议 ≥ 12 GB 显存 |
LibTorch 解压后的目录结构如下。后面遇到 cannot open shared object file 类报错时,
先确认这几个 .so 文件的位置:
/opt/libtorch
├── include/
├── lib/
│ ├── libtorch.so
│ ├── libtorch_cpu.so
│ └── libtorch_cuda.so
└── share/cmake/Torch/
0.2 一键准备
仓库 scripts/ 把「下载数据 → 下载权重 → 启动 Ollama → 构建」四件事统一成一个入口:
./scripts/setup.sh --all
脚本幂等:数据或权重已存在时自动跳过,中途中断后重跑即可。下载优先直连 (hf-mirror、github raw),失败自动回退代理。脚本无报错退出后,数据与权重就位, 构建完成,可直接跳到 0.5 运行验证。
0.3 分步准备
首次安装建议分步执行,便于确认每一步的结果:
./scripts/download-data.sh # 各章数据集:the-verdict.txt、GPT-2 词表、SMS Spam、指令数据
./scripts/download-weights.sh # GPT-2 small/medium 官方权重(约 2 GB)
./scripts/start-ollama.sh --pull # 启动 Ollama + 拉取 llama3(仅第 7 章用,约 4.7 GB)
./scripts/build.sh # 配置并构建全部章节
四条命令的输出会显示各自的进度与结果;全部成功后,进入 0.5 运行验证。 另外三个细节能省下不少排查时间:
-
下载源可切换:
download-weights.sh支持HF_MIRROR=huggingface切回官方源, 或ALL_PROXY=socks5h://host:port走代理。大陆网络默认走 hf-mirror,实测约 10 MB/s。 -
路径集中管理:所有路径集中在
scripts/common.sh。换机器时,修改其中的LIBTORCH_ROOT、CUDA_VERSION等变量即可,不用在仓库里逐处搜索。 -
构建缓存异常时清理重建:
build.sh --clean会删除build/并重新配置。 遇到原因不明的编译错误,先执行它再排查。
0.4 构建系统概览
顶层 CMakeLists.txt 经 chapters/CMakeLists.txt 把第 0–7 章的子目录
add_subdirectory 进来,即把每章作为子工程纳入同一次构建;每章同时也是一个
可独立构建的子工程。第 8–13 章不进统一构建:chapter08_training_loop、
chapter09_lora、chapter10_serving、chapter11_nl2bash、chapter12_toolcalling、
chapter13_reasoning 六个独立工程,单独 cmake -S 配置或用各自 scripts/ 构建,
详见各自 CMakeLists.txt 头注与对应章节。
定位 LibTorch 的逻辑集中在 cmake/libtorch.cmake,查找顺序为:
-DTORCH_ROOT=<dir> → 环境变量 $TORCH_ROOT → 默认 /opt/libtorch。
该文件还提供 add_libtorch_executable(<target> <source>) 函数,于是每章的
CMakeLists.txt 只需要三行。新增章节时套用这个模板:
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/../../cmake")
include(libtorch)
add_libtorch_executable(chapter00_hello_torch main.cpp)
第 0–7 章的构建产物统一落在 build/chapters/<章节名>/ 下;
独立构建,产物在各自目录的 build/ 下。
0.5 运行入口
运行第 0–7 章的程序,统一使用 scripts/run.sh:
./scripts/run.sh chapter00_hello_torch
run.sh 做三件事:
-
在
build/chapters/下定位可执行文件,参数支持完整目录名或去掉前缀的章节名; -
设置
LD_LIBRARY_PATH指向 LibTorch 与 CUDA 运行库。绕开脚本直接运行可执行文件时, 报libtorch.so: cannot open shared object file,原因就是缺了这一步; -
把剩余参数透传给程序,例如:
./scripts/run.sh chapter05_pretraining 10 # 覆盖预训练轮数
0.6 张量热身
chapters/chapter00_hello_torch/main.cpp 是一个自包含程序,不依赖任何数据集,
编译后即可运行。第 2–7 章、第 9–13 章会用到的 LibTorch C++ API,
大多在源码内按 §A–§K 分节演示。本节按「后面哪里会用到」逐段讲解。
§A 设备、种子与 TF32:每章 main() 的开头设置
每章程序的 main() 都以同样几行开场:
torch::Device select_device() {
if (torch::cuda::is_available()) return torch::Device(torch::kCUDA);
return torch::Device(torch::kCPU);
}
torch::Device device = select_device(); // 优先 CUDA,无 GPU 时回落 CPU
torch::manual_seed(123); // 固定随机种子
at::globalContext().setAllowTF32CuBLAS(false); // 教学约束:禁用 TF32
这几行对应两件事:
-
manual_seed(123)固定 PyTorch 的全局随机种子。此后每次运行, 随机初始化得到的数值完全一致——这是全书数值可复现的基础。 -
禁用 TF32:TF32 是 NVIDIA Ampere 之后 GPU 上 matmul 的默认加速格式 (19 位格式,尾数 10 位),速度更快、精度略降。若保持开启,同一份权重 在 GPU 上算出的 loss 会比 CPU 的 float32 结果在小数点后几位上出现偏差, 难以核对。 本书为保证实验逐位一致而统一关闭 TF32,代价是速度略慢。
|
生产训练请删除这一行,重新打开 TF32——那是工程取舍,不是对错问题。 教学要求数值可核对,生产要求跑得快。 |
§B–§D 张量工厂、形状变换与运算
auto x = torch::tensor({{1.0f, 2.0f, 3.0f},
{4.0f, 5.0f, 6.0f}}); // (2,3) Float
auto w = torch::tensor({{1.0f}, {1.0f}, {1.0f}}); // (3,1)
auto y_mat = x.matmul(w); // (2,3)@(3,1) = (2,1)
auto y_soft = torch::softmax(x, /*dim=*/1); // 沿最后一维归一化
auto y_mask = x.masked_fill(x > 3.0f, -1e9); // 条件填充 → 注意力掩码的雏形
auto y_topk = std::get<0>(x.topk(/*k=*/2, /*dim=*/1)); // 每行 top-2 → 解码策略用
这段代码覆盖了第 3 章注意力机制的全部运算:matmul 计算相似度,
softmax 把分数归一成权重,masked_fill 屏蔽「未来」位置,
topk 用于第 5 章解码时的 Top-k 采样。
索引与切片是另一族高频写法,从第 3 章起反复出现;这一族在 chapter00_hello_torch 源码里
没有集中演示,先在这里认识语法:
auto row = x.index({1}); // 第 2 行(下标从 0 起)→ 形状 (3)
auto col = x.index({torch::indexing::Slice(), 2}); // 每行第 3 列 → 形状 (2)
.index({…}) 按维度从高到低写位置:单下标取一行,Slice() 表示这一维全取,
-1 表示最后一个元素。三维张量同构:第 5 章生成循环用
logits.index({torch::indexing::Slice(), -1, torch::indexing::Slice()})
取出每个样本最后一个词元的 logits;第 3 章取查询向量、第 6 章分类头取
「最后一个词元」,用的都是这族写法。
形状变换一族(reshape/view/transpose/permute/contiguous)将在多头注意力的
「权重切分 + batched matmul」里反复出现,第 3 章展开。
§E 自动求导
auto xg = torch::tensor({1.0, 2.0, 3.0}, torch::kFloat).requires_grad_(true);
auto yg = (xg * xg).sum(); // y = Σx²,dy/dx = 2x
yg.backward();
std::cout << xg.grad(); // [2, 4, 6]
这三行是全书训练代码的引擎。先说明它解决什么问题:
训练的目标是调整模型的参数(对 GPT-2 来说是上亿个数字),让输出更接近期望。 调整方向取决于每个参数的梯度:把某个参数稍微调大,loss 会变大还是变小、变化多快。 参数数量太大,无法手工推导,所以由框架自动计算——这就是「自动求导」(autograd)。
三行代码各自的作用:
-
requires_grad_()(带下划线,表示原地修改):让框架记下这个张量之后参与的每一步运算; -
(xg * xg).sum():普通前向计算,框架同时把运算记录成一张图; -
backward():从 y 出发沿记录的图反向遍历,用链式法则算出偏导数,写入xg.grad()。
输出 [2, 4, 6] 可以手算验证:\(y = x^2\) 的导数是 \(2x\),对 \(x_1 = 1\)、\(x_2 = 2\)、\(x_3 = 3\)
分别是 2、4、6。先手推、再运行验证——这个习惯在第 3 章、第 8 章会反复用到。
一条容易踩坑的工程纪律:backward() 的建图记录占用内存,而推理、评估阶段
不需要梯度。这两类代码要用 torch::NoGradGuard 包住以省显存,
第 5 章评估 loss、第 7 章推理生成都依赖它。没包住也不会报错,
症状是推理阶段显存占用涨到训练水平,运行变慢,严重时 OOM。
§F–§I nn::Module 子类化
下面是注册一个模块的完整写法,全书所有模型都沿用这五个约定:
class DemoModuleImpl : public torch::nn::Module {
public:
DemoModuleImpl(int64_t in_dim, int64_t hidden_dim) {
fc_ = register_module("fc", torch::nn::Linear(in_dim, hidden_dim));
dropout_ = register_module("drop", torch::nn::Dropout(0.1));
scale_ = register_parameter("scale", torch::tensor(1.0).reshape({1}));
bias_ = register_buffer("bias", torch::arange(0, hidden_dim, torch::kFloat));
}
torch::Tensor forward(torch::Tensor x) {
auto h = torch::relu(fc_->forward(x));
h = h * scale_; // 广播:(B,H) * (1,)
h = h.masked_fill(h <= 0, -1e9);
return h.mean(/*dim=*/-1, /*keepdim=*/true);
}
private:
torch::nn::Linear fc_{nullptr};
torch::nn::Dropout dropout_{nullptr};
torch::Tensor scale_;
torch::Tensor bias_;
};
TORCH_MODULE(DemoModule); // 生成 ModuleHolder<DemoModuleImpl>
forward 里有一行值得停一下:h * scale_ 是全书第一次出现广播(broadcast),
即形状 (B,H) 与 (1,) 相乘。规则:两个形状从最后一维向前逐维对齐,
大小为 1 的维度自动复制扩展到对方——(1,) 扩成 (B,H),逐元素相乘后形状不变。
第 2 章把位置嵌入 [4,256] 加到 [8,4,256] 上,走的是同一条规则。
| 约定 | 说明 |
|---|---|
实现类叫 |
真正的逻辑写在 |
|
三种注册各司其职:子模块参与 |
|
|
|
切换 Dropout/BN 行为;第 6 章评估准确率前必须调用 |
|
放进匿名 namespace 会切断 |
参数与子模块的遍历接口,第 4 章用来统计参数量,第 6/9 章用来冻结参数:
for (const auto& p : model->parameters()) { ... } // 优化器入口
for (const auto& np : model->named_parameters(/*recurse=*/true)) { ... } // 名字 + 形状
for (const auto& nm : model->named_modules()) { ... } // 子模块树
§H 优化器与训练五步
auto opt = torch::optim::AdamW(
model->parameters(),
torch::optim::AdamWOptions(1e-3).betas({0.9, 0.95}).weight_decay(0.01));
opt.zero_grad(); // 1. 清零梯度
auto loss = ((logits - target).pow(2)).mean(); // 2. 前向 + 算 loss
loss.backward(); // 3. 反向传播
opt.step(); // 4. 更新参数
// 5.(循环回到 1)
所有训练循环都是这五步的展开:清梯度 → 前向 → 反向 → 更新 → 回到开头。 第 5–9 章的训练代码同此模板。
每次迭代都必须调用 zero_grad(),因为 PyTorch 的梯度是累积的:
backward() 把新梯度加到 p.grad() 上,而不是覆盖。
跨 batch 累积梯度等场景依赖这一行为。每轮开始前要手动清零,
否则本轮梯度会混入上一轮的值,更新方向出错。
动态调整学习率由一行调用完成,第 8 章的预热与余弦衰减依赖它:
opt.param_groups().front().options().set_lr(1e-4);
§J 序列化:整模型存取
torch::save(model, "/tmp/demo.pt");
torch::load(model_loaded, "/tmp/demo.pt");
整模型存取就这两行。第 5 章会改为「只存权重」(state_dict 视角),
并比较两种方式的取舍;加载 OpenAI 官方权重则使用第 5 章的 safetensors 解析器。
§A–§K 全景速查表
下表汇总 §A–§K 的主题与用途,后续章节用到时可随时翻回本表:
| 节 | 主题 | 后面哪里用 |
|---|---|---|
A |
设备 / seed / TF32 |
每章 main() 开头 |
B |
张量工厂(tensor/zeros/ones/arange/randn/from_blob) |
第 2 章嵌入层 |
C |
形状变换(reshape/view/transpose/permute/contiguous) |
第 3 章多头注意力 |
D |
运算(matmul/softmax/cat/masked_fill/argmax/topk) |
第 3–5 章 |
E |
autograd(requires_grad_/backward/NoGradGuard) |
第 5–9 章 |
F |
nn::Module 子类化(register_* / TORCH_MODULE) |
第 3–4 章起全部模型 |
G |
现成模块(Linear/Embedding/Dropout)与 functional |
第 2–4 章 |
H |
AdamW 与训练五步、动态 lr |
第 5–9 章 |
I |
parameters/named_parameters/named_modules 遍历 |
第 4/6/9 章(数参数、冻结) |
J |
torch::save/load |
第 5 章(state_dict 详解) |
K |
c10::optional / TensorOptions |
第 5 章(top_k 可选参数) |
0.7 验证:预期输出
构建完成后运行程序:
./scripts/run.sh chapter00_hello_torch
正常输出如下(节选):
=== Chapter 00: Hello LibTorch ===
Torch version: 2.13.0
----- A. Device & Global Settings -----
CUDA available: yes
Using device: cuda ← 无 GPU 的机器显示 cpu
manual_seed(123) done on CPU; weights will migrate to device later
TF32 (CuBLAS) disabled to match book floats
----- B. Tensor Factories -----
x shape=[2, 3] dtype=Float device=cpu
rng:
0.4869 0.7131 0.2427
0.5931 0.1054 0.3393 ← 随机数每次相同(seed 已固定)
...
----- E. Autograd -----
y = sum(x*x) ; grad at x: 2 4 6 ← 与手推一致,确定性输出
...
trainable params (DemoModule) = 17 ← Linear(3→4): 12+4,再加 scale 1 个
fc weight shape: [4, 3]
...
one train step loss=0.6xxx
after set_lr: 0.0001
----- End of Chapter 00 prelude -----
Original first demo -> x @ w =
6
15 ← 确定性输出
重点核对三处确定性输出:grad = [2, 4, 6]、x @ w = [6, 15]、
可训练参数 = 17。三者与上面一致,说明环境正常,可以进入第 1 章;
不一致则多半是编译链接到了错误版本的 LibTorch,回到 0.1 核对版本配套。
本章小结
-
环境与构建:
scripts/setup.sh --all一键完成数据、分词器、GPT-2 权重下载与构建;scripts/run.sh统一了第 0–7 章程序的运行入口。 -
核心写法:
chapter00_hello_torch的main.cpp通过 §A–§K 的示例给出 LibTorch 的核心 API—— 张量创建与索引、广播、autograd、nn::Module注册约定。 -
工程约定:CPU 上固定种子初始化后迁移设备、禁用 TF32 保证数值可复现、
train()/eval()模式切换。 -
验收标准:
chapter00_hello_torch的全部确定性输出与本章示例逐位一致。
自测清单
能不回看就答出这些,本章就算过关:
-
./scripts/setup.sh --all无报错结束(或四条分步命令各自成功) -
第 0–7 章程序在
build/chapters/下生成;第 8–13 章对应目录按各自 CMakeLists 独立构建成功 -
./scripts/run.sh chapter00_hello_torch输出Torch version与设备信息 -
grad = [2, 4, 6]与x @ w = [6, 15]与上面一致 -
有 GPU 的机器:
CUDA available: yes;无 GPU 也能全部跑通
第 10–13 章的四个独立工程(chapters/chapter10_serving、chapters/chapter11_nl2bash、
chapters/chapter12_toolcalling、chapters/chapter13_reasoning)有各自的构建入口:
chapters/chapter10_serving 与 chapters/chapter13_reasoning 用各自目录里的
./scripts/build.sh,chapters/chapter12_toolcalling 用 ./scripts/run.sh build,
chapters/chapter11_nl2bash 直接 cmake -B build && cmake --build build -j。
构建方式与本章相同,到对应章节再构建即可。
习题
-
把
DemoModuleImpl的hidden_dim从 4 改成 8,先手算可训练参数个数, 再运行验证(提示:fc变为 3×8+8,scale不变)。 -
把
forward里masked_fill(h ⇐ 0, -1e9)的-1e9改成0.0, 观察输出变化,并解释第 3 章的注意力掩码为什么必须用「极大的负数」。 -
用 §B 的工厂函数创建一个
(2,3,4)张量,用permute变换为(2,4,3), 再用contiguous() + view({-1})展平——这是第 3 章多头切分的标准操作。
拓展阅读
-
主仓库环境页:
docs/modules/ROOT/pages/prerequisites.adoc -
LibTorch 定位与帮助函数:
cmake/libtorch.cmake;路径与下载函数集中管理:scripts/common.sh -
LibTorch 官方 API 参考: pytorch.org/cppdocs
未接触过深度学习?先读 附录 A:深度学习的心智模型 —— 梯度、反向传播、batch、概率采样,从零讲透,再回到主线。