项目结构与环境搭建¶
这本书的代码就是一个目录里的十几个文件:一个 qwen/ 包(一章一个文件)加上每章一个脚本,权重放在 models/ 下。这一页把目录结构、环境怎么装、每一章新建和修改了哪些文件、怎么运行和核对一次说清楚;之后每一章开头都会标出那一章动了哪些文件。
目录结构¶
可以直接用仓库里的 transformer/(examples/ 下的文件是从正文逐字导出的),也可以自己建一个空目录,拷一份 pyproject.toml,按章节顺序把文件敲进去:
transformer/ ← 仓库里的这本书;自己从零建的话照这个结构
├── pyproject.toml uv.lock uv 项目:依赖清单与锁定的版本,uv sync 一条命令装好
├── .venv/ uv sync 建的虚拟环境(不进 git)
├── models/
│ └── Qwen3.5-0.8B/ 权重与 tokenizer,hf download 下来的(1.7 GB,不进 git)
├── examples/ 正文里的代码(从页面逐字导出):
│ ├── qwen/ 这本书写的包:一章一个文件,到第 16 章共 17 个
│ │ ├── weights.py (一)读 config.json 和 safetensors
│ │ ├── …
│ │ └── moe.py (十六)
│ ├── main.py map.py 第 0 步
│ └── ch01_weights.py … ch16_scale.py 每章一个脚本:uv run python examples/chNN_xxx.py
├── docs/ docs-en/ 正文(唯一的源)
└── tools/check_code.py 校验脚本:按导航顺序把全书接力跑一遍
全书写完之后 qwen/ 里是这些文件(由各章的代码块自动算出来;第 10 章改了四个文件):
qwen/ ├── weights.py (一) ├── embedding.py (二) ├── norm.py (三) ├── mlp.py (四) ├── rope.py (五) ├── attention.py (六),(十)修改 ├── deltanet.py (七),(十)修改 ├── chunk.py (八) ├── layer.py (九),(十)修改 ├── model.py (九),(十)修改 ├── cache.py (十) ├── sample.py (十一) ├── chat.py (十二) ├── batch.py (十三) ├── mtp.py (十四) └── moe.py (十六) main.py … ch16_scale.py 每章一个脚本
正文(docs/)是唯一的源,examples/ 是它的导出,构建时会校验两边一致;models/ 在仓库里可以是指向根目录 models/ 的软链接(Mac 的一键脚本会帮你建好),也可以直接把权重下到这里。
环境¶
项目用 uv 管理:依赖写在 pyproject.toml 里,uv sync 一条命令建好虚拟环境、装好指定版本的依赖(Linux 上自动从 PyTorch 的 CPU 索引装 torch,macOS 上 PyPI 的轮子自带 MPS),之后所有脚本都用 uv run 跑,不用手动激活环境。
# 1. 装 uv(一次)
curl -LsSf https://astral.sh/uv/install.sh | sh
# 2. 在仓库的 transformer/ 目录下建环境、装依赖(自己从零建目录的话,把 pyproject.toml 拷过去再 uv sync)
cd transformer
uv sync
# 3. 权重 1.7 GB;国内先 export HF_ENDPOINT=https://hf-mirror.com
uv run hf download Qwen/Qwen3.5-0.8B --local-dir models/Qwen3.5-0.8B
# 验证:能 import、权重在
uv run python -c "import torch, transformers, safetensors, tokenizers; print(torch.__version__, transformers.__version__)"
ls models/Qwen3.5-0.8B/config.json
pyproject.toml 全文如下。有 NVIDIA 显卡时把 [tool.uv.sources] 里的索引换成 CUDA 版(比如 https://download.pytorch.org/whl/cu128)再 uv sync 即可:
[project]
name = "transformer-by-hand"
version = "0.1.0"
description = "《手写 Transformer》的代码:从 main.py 开始、一章一个文件写出 Qwen3.5 的混合架构"
requires-python = ">=3.10"
dependencies = [
"torch>=2.4", # CPU 版就够:Linux 上从下面的 pytorch-cpu 索引装,macOS 的 PyPI 轮子自带 MPS
"safetensors>=0.4", # 读权重文件
"tokenizers>=0.20", # 第 2 章起直接读 tokenizer.json
"transformers>=5.0", # 只有第 0、9、16 章用:当黑盒跑终点、整模型对 logits、MoE 层对答案(5.0 起才有 Qwen3.5)
"huggingface_hub>=0.30", # hf download 命令
]
[tool.uv]
package = false # 只是一个脚本目录,不打包、不安装自己
[tool.uv.sources]
torch = [{ index = "pytorch-cpu", marker = "sys_platform == 'linux'" }] # 有 NVIDIA 显卡时改成 CUDA 索引(见 setup 页)
[[tool.uv.index]]
name = "pytorch-cpu"
url = "https://download.pytorch.org/whl/cpu"
explicit = true
| 依赖 | 哪几章用 | 做什么 |
|---|---|---|
torch |
全部 | 张量、矩阵乘、nn.Module |
safetensors |
第 1 章起 | 读权重文件(只读目录、按名字取张量) |
tokenizers |
第 2 章起 | 直接读 tokenizer.json,不经过 transformers |
transformers |
第 0、9、16 章 | 第 0 步当黑盒跑终点,第 9 章整模型对 logits,第 16 章对 MoE 层 |
huggingface_hub |
下载 | hf download 命令 |
用仓库的一键脚本:Mac 上 bash env/setup-macos.sh 会装好整套手册共用的 Python 环境、PyTorch 和编译工具链,下载包括 Qwen3.5-0.8B 在内的三个小模型并建好 transformer/models 的软链接(这本书也可以直接用那个共用环境:PYTHON=.venv/bin/python python transformer/tools/check_code.py);之后 python tools/mac_check.py transformer 把前十章接力跑一遍当自检。有 NVIDIA 显卡的机器用根目录的 bash setup-gpu.sh。
内存与设备:fp32 的模型 3 GB;第 0 步和第 9 章同时加载 HF 的参考实现,峰值约 6 GB(load_state_dict(assign=True) 不复制权重,否则是 9 GB)。脚本默认在 CPU 上跑,线程数设为逻辑核的一半——第 15 章量过,32 核的机器开满 32 线程反而慢 60 倍;Mac 不用管。所有代码都能 .to("cuda") / .to("mps"),第 15 章的脚本会自动选设备。
怎么运行、怎么核对¶
每一章的脚本只依赖前面各章写好的文件,所以按章节顺序在同一个目录里运行就行:
cd transformer # 在这本书的目录下,用 uv run 跑每一章的脚本
uv run python examples/main.py # 第 0 步
uv run python examples/ch01_weights.py # 第 1 章……以此类推
QWEN_MODEL=/data/models/Qwen3.5-0.8B uv run python examples/ch09_model.py # 权重不在 models/ 下时用环境变量指
脚本都从当前目录往上找 models/Qwen3.5-0.8B,所以在 transformer/ 下或者它的任何子目录里跑都能找到。
校验脚本用同一个环境,确认页面上的输出和自己机器上的一致:
uv run python tools/check_code.py # 全书接力跑一遍(约 10 分钟,CPU)
uv run python tools/check_code.py docs/cache.md --only # 只跑第 10 章(前面的章只把文件写进工作目录)
它按导航顺序把每一章的代码块写进 build/examples/tutorial/,运行每章的脚本,把标准输出和页面上的「输出」块逐行比对(含耗时的行只比对数字以外的部分);解释器默认用 transformer/.venv(uv sync 建的),也可以用环境变量 PYTHON 指定。
每一章新建和修改的文件¶
下表由各章的代码块自动算出来。只有第 10 章改了已有的文件(注意力、线性注意力、解码层、整模型各加一个 state 参数),其余各章只新建:
| 章 | 新建 | 修改 | 脚本 |
|---|---|---|---|
| 第 0 步 | – | – | main.py, map.py |
| (一) | qwen/weights.py | – | ch01_weights.py |
| (二) | qwen/embedding.py | – | ch02_embedding.py |
| (三) | qwen/norm.py | – | ch03_norm.py |
| (四) | qwen/mlp.py | – | ch04_mlp.py |
| (五) | qwen/rope.py | – | ch05_rope.py |
| (六) | qwen/attention.py | – | ch06_toy.py, ch06_gate_toy.py, ch06_gate_sgd.py, ch06_attention.py |
| (七) | qwen/deltanet.py | – | ch07_toy.py, ch07_deltanet.py |
| (八) | qwen/chunk.py | – | ch08_toy.py, ch08_chunk.py |
| (九) | qwen/layer.py, qwen/model.py | – | ch09_model.py, ch09_inside.py |
| (十) | qwen/cache.py | qwen/attention.py, qwen/deltanet.py, qwen/layer.py, qwen/model.py | ch10_cache.py |
| (十一) | qwen/sample.py | – | ch11_sample.py |
| (十二) | qwen/chat.py | – | ch12_chat.py, ch12_turns.py |
| (十三) | qwen/batch.py | – | ch13_batch.py |
| (十四) | qwen/mtp.py | – | ch14_mtp.py |
| (十五) | – | – | ch15_speed.py |
| (十六) | qwen/moe.py | – | ch16_scale.py |
每一章正文的开头也有同样的信息:那一章之后 qwen/ 里有哪些文件,哪些是本章新建(+)、哪些是本章修改(~)。
怎么讲清楚
讲"一个项目怎么组织",说清三件事:源文件在哪、生成物在哪、怎么验证两者一致。这本书的答案是:正文是唯一的源,examples/ 是导出,check_code.py 把正文接力跑一遍并和页面上的输出比对——改了代码而没改输出,构建就会失败。这个"源 → 导出 → 校验"的闭环,和推理框架里"配置 → 生成的 kernel → 数值对齐测试"是同一个思路。
小结¶
- 一个目录:
pyproject.toml(uv 项目)、models/(权重)、examples/qwen/(17 个文件)、每章一个脚本。 - 三步装环境:装 uv、
uv sync、uv run hf download;之后一律uv run。Mac 也可以用env/setup-macos.sh的共用环境。 - 按章顺序运行;
check_code.py接力核对;每章开头标出新建和修改的文件。
发现错误或有更好的写法?在 GitHub 上编辑此页提交问题