PyTorch 训练框架内核深度解析

前言:从 Lua 边缘到 AI 心脏

作者 杨艺韬 · 10,579 字 · 发布于 · 更新于

  • "PyTorch's primary goal is usability"
  • "A secondary goal is to have reasonable performance"

—— PyTorch 官方设计原则文档 docs/source/community/design.rst,Principle 1: Usability over Performance

一切的起点:从 Lush 到 Torch7

故事要从 Yann LeCun 与 Léon Bottou 的工具链讲起。

他们先后做过神经网络模拟器 SN(1988)和一种叫 Lush 的 Lisp 方言(参考手册 2002 年发布),张量是其中的一等公民。但它也带着 Lisp 与生俱来的孤独 —— 整个工业界都在用 C++ 与 Java,小众语言很难走出实验室。

Torch 这条线的主角是 Ronan Collobert:2002 年,他与 Samy Bengio、Johnny Mariéthoz 在 Idiap 发布了技术报告《Torch: a modular machine learning software library》。Torch7 的上一代 Torch5 已经用 Lua 做前端;2011 年,Collobert(Idiap)、Koray Kavukcuoglu(NEC Labs)与 Clément Farabet(NYU)发表论文《Torch7: A Matlab-like Environment for Machine Learning》,把 Torch7 定位为「扩展 Lua 的数值计算框架与机器学习库」,并在文中写明其张量设计主要受 SN 与 Lush 启发。

Torch7 用 Lua 写前端,C 写底层,配合一个叫 TH(TorcH 的缩写)的张量库做底层 BLAS 和 BLAS-like 的运算。它在学术界站稳了脚跟:DeepMind 在用、Facebook AI Research 在用、Twitter Cortex 在用。但 Lua 和 Python 之间的鸿沟,注定 Torch7 永远只能是少数派。每招进一个新研究员,团队都要先教会他写 Lua —— 这件事在 Python 一统机器学习的 2014 年,已经变得不可持续。

2016 年,Soumith Chintala、Adam Paszke、Sam Gross 等人在 Facebook AI Research 启动了一个新项目,把 Torch7 的 C 张量库搬到 Python 前端之下,再配上一套新写的自动微分引擎(主仓里 torch/autograd 的首个提交是 2016-08-19 的 "Add autograd")。不到一年,2017 年 1 月,PyTorch 首个公开版本 0.1.6 在 GitHub 上发布(主仓里 v0.1.6 tag 的日期是 2017-01-21)。

这就是今天每一篇顶会论文、每一份大模型权重、每一个国内外 AI 实验室默认背后的那个项目的开端。

Caffe2 合并:把推理框架"喂"进 PyTorch

PyTorch 0.x 时代有一个明显的短板:它只擅长研究,不擅长部署。Facebook 内部当时还有另一个深度学习框架叫 Caffe2,由贾扬清(贾扬清后来从 Facebook 去了阿里、又去了 LeptonAI)从 Caffe 演化而来,专门为生产推理优化 —— C++ 内核、轻量级、能跑在手机上。

2018 年 3 月底,Caffe2 的代码先并入 PyTorch 主仓(提交 90afedb6e2「Merge caffe2 with pytorch.」);5 月初,PyTorch 官方博客《The road to 1.0: production ready PyTorch》公布了 1.0 路线:把 PyTorch 与 Caffe2 结合起来,让 PyTorch 获得生产级能力。Caffe2 的张量库、运行时与 ONNX 模型互转能力,全部并入 PyTorch 仓库。这个决定带来了一个深远的影响 —— PyTorch 从此既能做研究也能做部署,学术和工业不必再"双框架"切换。

这次合并在源码上留下的痕迹今天还能看到:

PyTorch 1.0(2018 年 12 月)就是这次合并后的第一个稳定版本。理解 PyTorch 的"双底色" —— 研究友好 + 工业可部署 —— 就要回到 Caffe2 合并这个时间点。

PyTorch 大版本演进路线图

为了让你对 PyTorch 各代版本的"性格"有个直观认识,把关键里程碑列出来:

版本 时间 核心变化 代表性能力
0.1 2017-01 首次公开 动态图 + autograd
0.4 2018-04 合并 Tensor 与 Variable 用户不再区分两者
1.0 2018-12 Caffe2 合并 + JIT/TorchScript 同框架研究 → 部署
1.3 2019-10 量化、移动端、命名张量 工业部署能力补齐
1.5 2020-04 C++ 前端稳定、分布式 RPC 多机训练原生支持
1.8 2021-03 Pipeline Parallelism、DDP 通信 hook(梯度压缩) 大模型训练抽象层
1.10 2021-10 CUDA Graphs API 接入 性能基建持续打磨
1.13 2022-10 functorch 并入主仓(FSDP 已于 1.11 公开) 函数式 transform、ZeRO 风格
2.0 2023-03 torch.compile 公开 Dynamo + AOTAutograd + Inductor
2.2 2024-01 FlashAttention-2 接入 scaled_dot_product_attention(SDPA 本身 2.0 引入) LLM 训练加速
2.3 2024-04 FSDP-2 (fully_shard) 以私有 _composable 路径落地(v2.6 才公开) 新一代参数分片
2.6 2025-01 torch.load 默认 weights_only=True、FSDP-2 进入公开命名空间、torch.compiler.set_stance 安全默认值、分片 API 定型
2.8 2025-08 Compiled Autograd 打磨 反向图也能被 Inductor 编译
2.11 2026-03 本专栏使用版本 torch.compile 栈成熟、FSDP-2 成为推荐路径

最值得停下来看一眼的是 2.0 这个分水岭:在 2.0 之前,PyTorch 的故事是"如何把动态图做得更好用";在 2.0 之后,故事变成了"如何在保留动态图体验的前提下,把性能拿到静态图水平"。这两条故事线在源码里留下的痕迹完全不同 —— 前者堆叠在 aten/ 与 torch/csrc/autograd/ 下,后者集中在 torch/_dynamo/、torch/_functorch/、torch/_inductor/ 三个目录。理解 PyTorch 源码结构,就要理解这条 2.0 分水岭。

与国内框架的对比

很多读者关心的另一个问题:国内的 PaddlePaddle、Mindspore、OneFlow 与 PyTorch 是什么关系?

简短的回答:

框架 主导方 与 PyTorch 的关系
PaddlePaddle 百度 早期类似 TF 1.x 静态图,后来推动动态图 + 静态图融合,API 上对 PyTorch 有大量兼容借鉴
Mindspore 华为 自研图模式 + Pynative 动态模式,长期主打"昇腾原生"。API 与 PyTorch 有差异
OneFlow 一流科技(2023 年并入光年之外,随后随光年之外被美团收购) 基于"全局视角并行" SBP(Split / Broadcast / Partial),API 接近 PyTorch,重视编译优化

国内框架的共同特点是 "PyTorch 兼容层 + 自研编译器后端 + 自研芯片支持" —— 用户写 PyTorch 风格的代码,框架在底层把它翻译到自家的运行时和加速卡上。理解 PyTorch 源码,相当于理解了国内框架们都在试图兼容的"事实标准"。这也是为什么本专栏的内容对所有 AI 工程师都有迁移价值,而不仅仅是 PyTorch 用户。

graph LR
    Lush["Lush (NYU Lisp 方言)<br/>1990s-2000s"]
    Torch7["Torch / Torch7 (Lua + C)<br/>Torch7 论文 2011"]
    PyTorch["PyTorch (Python + C++)<br/>2017-至今"]
    Caffe2["Caffe2 (C++)<br/>Facebook 推理框架<br/>2017-2018"]
    Lush --> Torch7
    Torch7 --> PyTorch
    Caffe2 -. "2018 合并" .-> PyTorch
    style PyTorch fill:#fef3c7,stroke:#f59e0b,stroke-width:2px

PyTorch 今天的位置

翻一遍近几年 NeurIPS / ICLR / ICML 的开源实现,绝大多数新方法的参考实现是 PyTorch 写的;JAX 在少数团队(尤其 Google 系)里稳住了一块阵地,TensorFlow 在研究场景已基本退居历史。这个格局没有一份可以直接引用的权威统计口径(Papers with Code 已于 2025 年关停,域名改为跳转到 HuggingFace),但只要在 GitHub 上按论文名找实现,你会亲眼看到它。

HuggingFace Hub 上的情况类似:开源权重的首发格式绝大多数是 PyTorch 生态的 .bin / .safetensors。Llama、Qwen、DeepSeek、Mistral、Gemma —— 每一个你在新闻里看到的开源大模型,背后都是一行 torch.save(model.state_dict(), ...) 或它的 safetensors 等价物。

再看硬件侧:全球用于训练的 GPU 数量与 PyTorch 在其上的占比都没有公开可核实的统计,但从 NVIDIA 的数据中心营收规模、以及 Megatron-LM / DeepSpeed / TorchTitan 这些主流训练框架清一色建在 PyTorch 之上这两点,可以判断 PyTorch 是当今 AI 训练算力上事实的第一调度层。每提升 1% 的训练效率,节省的都是数据中心规模的能耗。

理解 PyTorch 的内部机理,不是一项可有可无的修养;它是这个时代 AI 工程师的基本功。

一个被低估的事实:PyTorch 是"事实上"的训练协议

很多人以为 PyTorch 只是"一个框架"。但更准确的描述是:PyTorch 已经成为深度学习的事实上的协议层,类似于 HTTP 之于互联网、JVM 之于 Java 生态。

证据是:

这意味着,理解 PyTorch 不再只是"会用一个工具",而是 理解整个深度学习行业是怎么运转的。

为什么再做一个 PyTorch 专栏

如果你搜 "PyTorch tutorial" 或 "PyTorch 教程",能找到几千篇博客、几百小时视频、一份越来越厚的官方文档。它们大多数的套路是:

这些都有用,但合起来仍然只是 "使用手册" —— 告诉你按哪些按钮,却不告诉你按钮背后有什么线路。

工程师真正想知道的

而工程师真正想知道的是另一组问题:

这些"为什么"藏在源码注释里、PR 讨论里、issue 追踪里、PyTorch 开发者大会某场闪电演讲的最后一张幻灯片上。你要把它们拼起来,才能真正理解 PyTorch。

本专栏的定位

本专栏做的就是这件事 —— 把源码、设计文档、PR 讨论、benchmark 数据和真实工程踩坑串成可读的章节 —— 不是源码导读,是源码之后的思考。

graph LR
    A["官方教程<br/>&quot;怎么用&quot;"] --> B["社区博客<br/>&quot;为什么这样用&quot;"]
    B --> C["源码<br/>&quot;到底是怎么做的&quot;"]
    C --> D["本专栏<br/>&quot;为什么这样设计&quot;"]
    style A fill:#fef3c7,stroke:#f59e0b
    style B fill:#fef3c7,stroke:#f59e0b
    style C fill:#dbeafe,stroke:#3b82f6
    style D fill:#dcfce7,stroke:#22c55e,stroke-width:2px

本专栏基于的源码版本

本专栏基于 PyTorch v2.11.0(2026-03 发布),对应 commit hash:

git tag:    v2.11.0
commit:     70d99e998b4955e0049d13a98d77ae1b14db1f45
committed:  2026-03-20
release:    2026-03-23(GitHub Release)

选择 v2.11 而非更早的 v2.0 / v2.4,是因为它是几条历史脉络汇合后的稳定形态:

理解 v2.11,就是理解 PyTorch 在"训练框架应该长什么样"这个问题上的当前答案。

源码对照指南

我建议你在阅读时同步打开源码对照:

# 拉取本专栏使用的精确版本
git clone --depth=1 --branch v2.11.0 \
    https://github.com/pytorch/pytorch.git pytorch-2.11
cd pytorch-2.11

# 五大核心路径
ls c10/                    # 核心抽象:Device、DType、Allocator、intrusive_ptr
ls aten/src/ATen/          # 算子库:native_functions.yaml + 实现
ls torch/csrc/             # Python 与 C++ 的桥梁,autograd C++ 引擎在这里
ls torch/                  # Python 前端:nn、optim、data、distributed
ls torch/_dynamo/          # TorchDynamo:CPython 帧拦截、字节码分析
ls torch/_inductor/        # TorchInductor:编译器后端,Triton 代码生成
ls torch/distributed/      # 分布式训练:DDP / FSDP / 进程组 / NCCL 集成

# 看代码生成出来的真实算子定义(很多人不知道这一步)
cat aten/src/ATen/native/native_functions.yaml | head -100

# 看 Dispatcher 注册了哪些 key
grep -r "TORCH_LIBRARY_IMPL" aten/src/ATen/native/ | head

引用标注规范

正文所有源码引用都标注了具体路径,形如:

aten/src/ATen/core/dispatch/Dispatcher.h:142    ← 精确行号引用
torch/_dynamo/symbolic_convert.py               ← 模块级引用
c10/core/TensorImpl.h:380-410                   ← 区间引用

遇到一个精确的行号引用,说明那一行代码足够关键,值得你停下来对照读一遍。

如果将来 PyTorch 升级到 v2.12 / v2.13,行号会漂移,但模块路径和符号名稳定性极高 —— 找不到具体行号时,按符号名搜一下基本能命中。

为什么是 PyTorch 而不是其他

在 PyTorch 之前,深度学习框架的赛场上至少有过六七个候选人:

框架 时代 强项 退场或衰落原因
Theano 2008-2017 自动微分先行者,蒙特利尔大学出品 编译慢、API 古怪、被 TensorFlow 完全替代
Torch / Torch7 2002-2017 学术圈实力派 Lua 语言生态太小
Caffe 2013-2017 CV 模型部署利器 配置文件式声明,灵活性差
Caffe2 2017-2018 Facebook 自家推理框架 2018 与 PyTorch 合并
MXNet 2015-2023 亚马逊主推,多语言绑定 2023 年退役,2024 年初移入 Apache Attic(即"项目坟场")
TensorFlow 2015-至今 Google 出品,工业部署强 1.x 过于声明式,2.x 体验混乱,研究圈大量逃离
JAX 2018-至今 函数式 + XLA 编译 API 抽象层级高,研究圈双语派系,工业落地慢
PyTorch 2017-至今 动态图 + Python 原生 + 庞大社区 —— 当下事实上的赢家

动态图革命:PyTorch 真正赢在哪里

PyTorch 不是第一个支持自动微分的库,也不是第一个 Python 优先的库。它真正的胜负手是 动态图(define-by-run)。

什么叫动态图?看这段代码:

import torch

W = [torch.randn(4, 4) for _ in range(5)]  # 每层一个权重
x = torch.randn(2, 4)

def model(x, depth):
    h = x
    for i in range(depth):  # depth 在每一次调用时可以不同
        if h.sum() > 0:     # 控制流可以依赖中间结果
            h = torch.relu(h @ W[i])
        else:
            h = torch.tanh(h @ W[i])
    return h

# depth=3 跑出来一个图
y1 = model(x, 3)
# depth=5 又是另一个图
y2 = model(x, 5)

在 PyTorch 里,计算图是在 Python 解释器执行的过程中"边走边长"的。每次调用都构建一个新图,反向传播完就丢掉。Python 的 if / for / 异常都直接是图的控制流。

而 TensorFlow 1.x 走的是静态图(define-and-run):

# TF 1.x 的写法(已经被淘汰)
x = tf.placeholder(tf.float32)
W = tf.Variable(...)
y = tf.matmul(x, W)         # 这一行不执行计算,只是在描述图

with tf.Session() as sess:
    out = sess.run(y, {x: real_data})  # 真正的执行在这里

你必须先用 tf.placeholder / tf.Variable / tf.matmul 这些 API 把整张图描述出来,再用 Session.run() 喂数据真跑。控制流必须用 tf.cond / tf.while_loop 这些图原语,不能直接用 Python 的 if / for。

对工业部署,静态图有它的好处:可序列化、可优化、可静态分析。但对研究,静态图是噩梦:调试要 tf.print,动态形状要小心,新论文里那些"看一眼输入决定走哪条分支"的 trick 全部要重新建模成图原语。

PyTorch 用动态图把研究门槛打到了几乎为零:你写的就是普通 Python,pdb 能打断点,print 能立刻看到张量值。这一招让 PyTorch 在 2018-2020 年间几乎独占了学术界 —— 新论文用 PyTorch 实现,工业界为了跑这些论文不得不切到 PyTorch,进而 PyTorch 又积累了更多生态。这是一个完整的飞轮。

到 2019 年,TensorFlow 团队意识到不对,推出了默认启用 Eager 模式的 TF 2.0,想模仿动态图。但势已成 —— 研究者已经用 PyTorch 习惯了,新模型的参考实现大多先有 PyTorch 版。PyTorch 赢在 timing:在动态图最被需要的时刻,把它做对了。

那 torch.compile 算什么?

讽刺的是,PyTorch 在赢得动态图之战后,又在 2022 年 12 月的 PyTorch Conference 上亮出了 PyTorch 2.0 与 torch.compile(2023 年 3 月正式发布)—— 本质上把动态图重新编译回静态图。

但这一次,编译是可选的、自动的、Python 透明的。你不用重写代码、不用学新 API,只要给函数加一个装饰器:

W, b = torch.randn(4, 4), torch.randn(4)

@torch.compile
def model(x):
    return torch.relu(x @ W) + b

后面的事,PyTorch 通过 TorchDynamo 拦截 CPython 的字节码、用 AOTAutograd 把正反向图都翻出来、再用 TorchInductor 生成融合后的 Triton kernel —— 既保留了动态图开发的舒适,又拿回了静态图的性能。

torch.compile 这套编译器栈是 PyTorch 2.x 以来投入最集中的方向。这也是为什么本专栏要在第六篇用整整 4 章来拆解它。

为什么是源码而不是论文

PyTorch 没有官方"白皮书"。它有一篇 NeurIPS 2019 的论文(《PyTorch: An Imperative Style, High-Performance Deep Learning Library》),但那是项目早期的浓缩,描述的是 v1.0 时代的设计。今天的 PyTorch 已经长得几乎是另一套系统:dispatcher 重写过、torch.compile 整个生态从无到有 —— 真正承载当代 PyTorch 设计的,只有源码本身。

这是 PyTorch 与传统系统的不同之处:

维度 传统系统软件 PyTorch
主要参考 RFC + 论文 + 书 源码 + PR 讨论 + Developer Podcast
设计文档 完备且权威 散落在 issue、design docs、Blog post
核心抽象稳定性 几十年不变 每隔几个大版本就有子系统大重构
学习路径 自顶向下 自顶向下读源码 + 自底向上看 commit

这意味着如果你只读教程或者只读论文,你拿到的永远是滞后的 PyTorch。要紧跟 PyTorch,必须能自如地读它的源码。

源码读到什么深度算够

读源码不是越深越好。我把"源码深度"分成五个层级:

层级 描述 适合谁
L1 看官方文档、知道 API 怎么用 所有用户
L2 知道每个 API 在哪个 Python 文件、调用了哪些子函数 进阶算法工程师
L3 能跟 Python → C++ → CUDA 的完整调用链、知道 dispatcher / autograd / allocator 这些底层机制 系统工程师、本专栏的核心读者
L4 能修改 PyTorch 主仓代码、提交 PR、参与 RFC 讨论 PyTorch 贡献者
L5 能独立维护 PyTorch fork、做自家硬件后端集成 大厂底层平台团队

本专栏目标把读者带到 L3。再深的需求超出篇幅,但读完本专栏之后,你已经具备从 L3 自学到 L4 的所有"地形知识"。

一个例子:为什么读源码能救命

举个典型场景:你在用 8 卡 H100 训练一个十亿参数级的模型,发现训练速度只有理论值的 60%。各种 profile 之后发现 GPU 利用率飘忽不定,平均 70% 左右。

如果只看博客,你能找到的建议大概是 "用 FSDP"、"用 mixed precision"、"调 batch size"、"看看 NCCL 通信"。这些都对,但都是碎片化的猜测。

如果你读过 PyTorch 的源码(特别是本专栏第 4 章和第 17 章),你会立刻想到几个具体怀疑点:

这些都是藏在源码里的工程经验,没有任何博客能一次告诉你这么细。读过源码的人可以从问题描述快速跳到嫌疑代码段,省下大量盲目试错的时间。这就是源码的价值。

本专栏的目标就是把"读源码"这件事从苦力变成乐趣 —— 给你一张地图,标好哪些路是干道、哪些路是死胡同、哪些拐角藏着真正的宝藏。

读源码的"工作流"

一个常见的误区是:以为读源码就是从 __init__.py 开始,一行一行往下翻。这种线性读法对 100 行的小项目可能可行,但对 PyTorch 这种v2.11 下 torch/ + aten/ + c10/ + torchgen/ + functorch/ 合计约 233 万行 C++/CUDA/Python 混合的代码库就是灾难 —— 你会在第一周耗光精力,连 dispatcher 的入口都没找到。

真正可行的工作流是带着问题读:

  1. 先从 Python 用户视角写一段最小复现代码,比如 "我想知道 tensor.add 是怎么调起来的",写一个 5 行的 demo
  2. 用 gdb / lldb 或 py-spy 给关键函数下断点,在 Python 层和 C++ 层都打栈
  3. 从断点回溯调用栈,把每一层的关键文件路径记下来 —— 这就是这个子系统的"骨架"
  4. 挑骨架上的 1-2 个核心文件精读,其他先 grep 关键词跳过
  5. 写一段微型代码验证你理解的行为,看预期与实测是否一致

这就是本专栏背后的方法论。多数章节都沿着这条线展开:先一段最小复现代码 → 调用栈或时序图 → 几个核心文件精读 → 一个微型实验。章节结构的本身就是教你怎么读源码。

几条避坑提示

读 PyTorch 源码时,有几条坑是大多数人会踩的:

本专栏的读者画像

本专栏面向这几类人:

读者 典型问题 对应章节
AI 算法工程师 "我的训练为什么慢?为什么 OOM?" ch04 / ch11 / ch21
AI 系统工程师 训练框架选型、自定义算子、平台对接 ch05 / ch22 / ch16-ch18
大模型研究者 想理解 FSDP / 混合精度 / 分布式训练原理 ch15 / ch17-ch20
编译器爱好者 torch.compile 的字节码拦截、IR 设计、Triton codegen ch12-ch15
推理优化工程师 训练侧的内存管理与导出格式如何影响推理 ch04 / ch06 / ch19 / ch20
技术总监 / 架构师 评估 PyTorch 与 JAX / 国产框架的工程权衡 ch01 / ch23

前置知识

你不需要精通 C++ 模板元编程,但需要具备:

如果其中某些点你不熟悉,没关系,本专栏在每次首次出现时都会简短铺垫。但完全没接触过深度学习的话,建议先读一遍 Andrej Karpathy 的《Neural Networks: Zero to Hero》课程,再回来读本专栏。

不需要的前置

为了避免不必要的劝退,明确说一下你不需要会的:

如果有一天你想再下一层,这些主题各有自己的专著(NVIDIA 的 CUDA 编程指南、《Triton》论文、LLVM 官方教程) —— 本专栏的边界在 PyTorch 这一层。

与本系列其他专栏的关联阅读

本专栏是 AI 训练 + 推理双子卷 的训练侧,与已经完成的 《vLLM 推理内核深度解析》专栏 互补:

把两个专栏放在一起读,你会看到一个完整的 LLM 工程世界 —— 训练时千卡跑出权重,推理时千机服务用户,中间靠 PyTorch 的张量、HuggingFace 的格式与 vLLM 的引擎串起来。

更广泛的关联:

本专栏的章节组织

全专栏 23 章 + 前言,按 "从外到内 → 从基础设施到编译器栈 → 从单机到分布式 → 从核心到周边" 的顺序组织:

graph TD
    P[前言]
    P --> P1[第一篇 全景<br/>第1章]
    P1 --> P2[第二篇 张量层与运行时<br/>第2-4章]
    P2 --> P3[第三篇 Dispatcher 与算子<br/>第5-6章]
    P3 --> P4[第四篇 Autograd<br/>第7-8章]
    P4 --> P5[第五篇 nn 与训练栈<br/>第9-11章]
    P5 --> P6[第六篇 torch.compile<br/>第12-15章]
    P6 --> P7[第七篇 分布式训练<br/>第16-18章]
    P7 --> P8[第八篇 工程与生态<br/>第19-23章]
    style P1 fill:#fef3c7
    style P2 fill:#dbeafe
    style P3 fill:#dbeafe
    style P4 fill:#dcfce7
    style P5 fill:#dcfce7
    style P6 fill:#fce7f3
    style P7 fill:#fce7f3
    style P8 fill:#f3e8ff

第一篇 全景(第 1 章)——第 1 章先把 PyTorch 的全貌摊开:从一个 c = a + b 的张量加法切入,俯瞰 Python 前端 → CPython C API 绑定(THPVariable_* / PythonArgParser)→ c10 抽象 → Dispatcher → ATen 算子 → CUDA kernel → CUDA Caching Allocator 整条调用链。这一章看完,后面所有章节都能在这张地图上找到位置。

第二篇 张量层与运行时(第 2-4 章)——PyTorch 的"皮肤"是 Tensor,"骨架"是 c10。第 2 章拆 Tensor / Storage / TensorImpl 的三件套结构 —— 为什么 view 操作不复制内存、为什么多次 view 出来的张量共享同一块 Storage。第 3 章下到 c10 层:Device、ScalarType、Layout、DispatchKey、intrusive_ptr —— PyTorch 不用 std::shared_ptr 而是自己造了 intrusive_ptr,理由值得细说。第 4 章解剖 CUDA Caching Allocator —— PyTorch 不调原生 cudaMalloc,而是有一个自己的"显存池",理解它就能理解为什么 torch.cuda.empty_cache() 常常不如预期。

第三篇 Dispatcher 与算子(第 5-6 章)——PyTorch 的"心脏"是 Dispatcher。一个张量加法被调用时,它必须在十几个候选实现中选一个:CPU 还是 CUDA?要不要走 autograd?要不要走 autocast?要不要被 functionalize?(float32 还是 bfloat16 则在选中的 kernel 内部再分派。)这一切由 Dispatcher 用一个 DispatchKeySet 位图 + key 优先级排序的极简机制完成。第 5 章把它讲透。第 6 章则讲 PyTorch 怎么用 native_functions.yaml + 代码生成 一次性生成数千个算子的 C++ 与 Python 包装代码 —— 这是 PyTorch 工程化的最佳实践之一。

第四篇 Autograd 自动求导(第 7-8 章)——loss.backward() 是每个 PyTorch 用户每天都在调用、却很少有人能讲清楚原理的接口。第 7 章讲 反向图怎么在前向时被偷偷地构建 —— 每个支持 autograd 的算子都会在前向时记录一个 grad_fn,这些 grad_fn 串成一张反向图。第 8 章讲反向图怎么被 按设备分队列的多线程引擎 执行 —— 这部分代码在 torch/csrc/autograd/engine.cpp,它与 Tokio 调度器既相似又不同:同样是多线程 worker,却不做 work-stealing。

第五篇 nn 与训练栈(第 9-11 章)——第 9 章讲 nn.Module 这个 PyTorch 用户最熟悉的类是怎么实现的:__setattr__ 偷偷拦截参数赋值、Hooks 怎么挂载、state_dict 怎么递归收集。第 10 章讲 torch.optim 各种优化器(SGD / Adam / AdamW)的实现,以及最近几个版本引入的 Foreach / Fused / Capturable 三种性能模式。第 11 章讲 DataLoader 的多进程 worker —— 它用 torch.multiprocessing 启动 worker、用每个 worker 一条的 index 队列派任务、一条共享的结果队列收数据、用 pin_memory_thread 异步搬运数据,是一台精密的多进程机器。

第六篇 torch.compile 编译器栈(第 12-15 章)——这是本专栏的技术高峰。第 12 章讲 TorchDynamo 怎么用 CPython 的 PEP 523 帧求值 API(3.6 引入,v2.11 要求 Python ≥ 3.10)拦截 Python 函数调用,逐字节码解析、构建 FX Graph、生成 guards。第 13 章讲 AOTAutograd 怎么把这个图做"函数化"(去除原地操作)、再用 min_cut_rematerialization_partition 切分出最优的正向 / 反向子图。第 14 章讲 TorchInductor 怎么把 ATen IR 一路 lower 到 Triton DSL、再编译成 PTX。第 15 章把整条链路串起来,并讲 CUDA Graph 与 torch.compile 的协作 —— 在小 batch、kernel 多而短的场景里,把逐次 launch 换成一次 graph replay 能显著削掉 CPU 侧的启动开销,这也是 vLLM 等推理引擎默认开 CUDA Graph 的原因。

第七篇 分布式训练(第 16-18 章)——第 16 章讲 PyTorch 的分布式底座:进程组、torch.distributed.init_process_group、ProcessGroupNCCL 怎么把 NCCL 集成进 PyTorch 的张量。第 17 章讲 DDP:DistributedDataParallel 怎么用反向传播 hook 触发梯度的环形 AllReduce、用 梯度桶(Gradient Bucket) 把小梯度合并成大消息减少通信开销。第 18 章讲 FSDP:参数怎么被切分到 N 个 rank、前向时怎么 unshard、反向时怎么 reshard、为什么这种"用通信换显存"的策略在大模型训练中是必须的。

第八篇 工程与生态(第 19-23 章)——最后一篇收尾整个训练栈外围。第 19 章讲序列化:从 pickle 到 safetensors,从 torch.save 到 torch.distributed.checkpoint。第 20 章讲量化与混合精度:autocast 上下文管理器、已迁往 torchao 的 PT2E 量化流程、bf16/fp8/int8 的硬件支持。第 21 章讲 Profiler 与 Kineto:怎么用一行 torch.profiler.profile 拿到 ChromeTrace,怎么读懂 GPU 时间线。第 22 章讲怎么写一个真正"生产级"的自定义算子 —— 用 torch.library.custom_op 或 TORCH_LIBRARY 注册、加 autograd、加 fake(meta)函数、写对 mutates_args 让 functionalize 正确处理、加 inductor lowering。第 23 章作为收官,盘点 PyTorch 一路走来的设计决策与未来方向。

阅读路径

不同读者推荐不同的阅读顺序:

flowchart LR
    A[前言 + ch1] --> B{读者类型}
    B -->|算法工程师| AB[ch9 → ch10 → ch7 → ch11 → ch15 → ch20]
    B -->|系统工程师| SB[ch1-ch6 → ch22 → ch4 → ch16-18]
    B -->|编译器爱好者| CB[ch1 → ch5-6 → ch12-15 → ch23]
    B -->|大模型训练| LB[ch1 → ch7-8 → ch16-18 → ch20 → ch15]
    style AB fill:#dcfce7
    style SB fill:#dbeafe
    style CB fill:#fce7f3
    style LB fill:#fef3c7

如果你只有一个周末的时间:读前言 + 第 1 章 + 第 5 章(Dispatcher)+ 第 7 章(Autograd)+ 第 23 章。这五章拼起来就是一份"PyTorch 心智模型"的最小闭环 —— 之后再按需要回来读其他章。

PyTorch 的工程文化:理解源码的另一个维度

读 PyTorch 源码,除了看代码本身,还要看代码背后的工程文化 —— 这种文化解释了为什么源码长成现在这样,而不是另一种合理的样子。

极致的向后兼容承诺

PyTorch 团队对 BC(Backward Compatibility)的态度近乎偏执。一个 Python API 一旦发布,几年之内不会被破坏 —— 即便它的 C++ 内部已经被重写过两三次。这种承诺在源码里留下的痕迹是:

这种"不破坏用户代码"的文化,让 PyTorch 在大公司能放心用 —— 但代价是源码里有大量"历史包袱"。读源码时遇到看似冗余的代码时,先想想"这是不是为某个老 API 留的",常常能解开困惑。

"性能数据驱动 PR" 的传统

PyTorch 的核心 PR 几乎都附带 benchmark 表格:哪些模型上提速多少、哪些场景下回归。这种"数据先行"的文化让源码演进有迹可循 —— 一个动到 Inductor 或 allocator 的 PR,通常会附上 TorchBench / HuggingFace / TIMM 三套基准(benchmarks/dynamo/ 下的 torchbench.py、huggingface.py、timm_models.py)的逐模型加速比与回归清单。

本专栏引用性能结论时会尽量给出可核实的出处(PR 编号、官方文档、可复现的基准脚本);拿不到出处的数字,宁可只说方向、不编精度。

"小步快跑 + 实验性 flag" 的演进

PyTorch 许多大特性都是先以 torch._inductor / torch._dynamo / torch._C._functorch 这种带下划线前缀的"内部 API"形式上线,跑一两个版本后再 graduate 到正式 API。比如:

理解这种演进节奏,你就能看懂为什么 PyTorch 主仓里有大量 torch._xxx 模块 —— 其中不少并非永久的"私有 API",而是"正在孵化的下一代正式 API";也有像 torch._dynamo 这样一直作为实现模块留在下划线之下的。

关于 PyTorch 的几个常见误解

写在前面,先把几个广为流传但其实错的说法澄清一下,免得你带着错误前提开始读后面的章节:

误解 1:PyTorch 是"纯 Python"。

完全错。在 v2.11 的 torch/、aten/、c10/ 三个目录里,C++/CUDA 约 122 万行、Python 约 107 万行 —— 两者量级相当,C++ 还略多一些。Python 层只是面向用户的接口;真正干活的 dispatcher、autograd 引擎、ATen 算子、内存分配器、CUDA Graph 全部是 C++。Python 端的每一个张量操作最终都会通过 CPython C API 绑定(由模板 tools/autograd/templates/python_variable_methods.cpp 生成的绑定代码 + PythonArgParser 解参数)跨进 C++;pybind11 只用于其他一部分绑定。

误解 2:with torch.no_grad(): 就能完全关掉自动求导开销。

torch.no_grad() 关闭了反向图的构建,但 dispatcher 仍然会走 Autograd key 上的 autograd kernel(只是 kernel 发现 GradMode 关闭、不记录 grad_fn)。如果你想要进一步关掉这层开销,需要用 torch.inference_mode() —— 它把 Autograd 放进线程局部的 excluded 集合、并让新建的张量成为 inference 张量,dispatcher 在 key bitmap 上根本就不会进入 autograd 路径(见 c10/core/InferenceMode.h)。第 5 章会讲这个细节。

误解 3:torch.compile 会让所有代码都变快。

不一定。torch.compile 在以下场景反而可能变慢:函数体很小、调用频次很低(编译开销摊不开)、有大量动态形状(每次都触发重编译)、有 graph break(图被切碎导致每段都需要 launch overhead)。第 12 章会讲怎么诊断 torch.compile 的失效。

误解 4:DDP 比 FSDP 通信少所以更快。

只在小模型上成立。DDP 每一步都要全量 AllReduce 一遍梯度;FSDP(ZeRO-3)在前向和反向各 AllGather 一次参数、再 ReduceScatter 一次梯度,总通信量约为 DDP 的 1.5 倍(ZeRO 论文 SC'20 的分析),换来的是每卡只常驻 1/N 的参数、梯度与优化器状态。当模型大到单卡放不下时,DDP 根本启动不起来,必须改用 FSDP 这类分片方案。第 18 章 §18.2 会按 ZeRO 论文口径给出对比。

误解 5:tensor.cuda() 之后需要 torch.cuda.synchronize() 才能确保拷贝完成。

tensor.cuda()(即 Tensor.to('cuda'))默认 non_blocking=False:H2D 拷贝排进当前 CUDA stream 后,会等这个 stream 同步完才返回(aten/src/ATen/native/cuda/Copy.cu 里的 at::cuda::memcpy_and_sync),不需要再手动同步。传 non_blocking=True(配合 pinned memory)时才是异步的 —— 立即返回,但后续在同一个 stream 上的 kernel 都会保证看到搬运结果(同一 stream 内按序执行)。synchronize() 只在跨 stream 或者你想在 CPU 端确认时才需要。这是 PyTorch 异步执行模型最容易被误解的一点。

把这几条记在心里,后面读章节时会少走很多弯路。

字数与篇幅

本专栏每章正文 9000 字起步,篇幅最大的几章(自定义算子、架构总览等)超过 13000 字。全专栏 23 章 + 前言合计约 30 万字。站点页头显示的字数口径是中文字符 + 英文单词、且含代码块;若把代码块排除,正文约 26 万字。

我的目标不是做一份"快速入门",也不是做一份"参考手册" —— 这两类书市面上已经够多了。我的目标是做一个你会反复回来翻的专栏:第一遍读时建立直觉,第二遍读时核对源码,第三遍读时拿出来当工具书查 ATen 算子注册或 Inductor IR 的细节。

致谢与信息源

本专栏写作过程中参考的权威信息源,按重要性排列:

  1. PyTorch 主仓源码 (github.com/pytorch/pytorch v2.11.0):第一手材料,所有引用都标注精确路径
  2. PyTorch 官方设计文档(散落在 docs/source/notes/、dev-discuss.pytorch.org 论坛与 pytorch/rfcs 仓库):dispatcher、autograd、torch.compile 都有官方设计笔记
  3. 关键 PR 与 RFC:每章会列出重要 PR,作为设计意图的最权威说明
  4. PyTorch Developer Podcast(Edward Yang 主理的 PyTorch 内部技术短讲系列)
  5. PyTorch Conference 历年演讲:Soumith Chintala、Edward Yang、Jason Ansel、Horace He 等核心开发者的 KeyNote
  6. NeurIPS 2019 PyTorch 论文《PyTorch: An Imperative Style, High-Performance Deep Learning Library》:项目早期设计的浓缩
  7. Triton 论文与 Triton 官方教程:第 14 章 TorchInductor 章节的下游依赖
  8. CUDA Programming Guide:底层显存、stream 模型的权威说明

所有引用都标注了出处。如果你在本专栏看到一段"PyTorch 团队某某说",那一定有 PR、issue、conference 视频或者博客作为支撑。我对自己的承诺是:绝不编造来源、绝不凭记忆杜撰行号、绝不为流畅而牺牲准确。

如果发现正文有错误(哪怕是行号偏移),欢迎在站点反馈页留言或发邮件。这个专栏会随 PyTorch 版本跟进维护。

版权声明

本专栏内容为 杨艺韬 版权所有,保留一切权利。欢迎分享链接;引用少量内容用于评论、教学或研究时,请署名 杨艺韬 并附上原文链接。

"The best way to predict the future is to invent it."

—— Alan Kay