跳转至

项目结构与环境搭建

这本书的代码就是一个目录里的十几个文件:一个 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.pyqwen/attention.py, qwen/deltanet.py, qwen/layer.py, qwen/model.pych10_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 上编辑此页提交问题