内容简介
《PyTorch 深度实践:从 Tensor 到深度学习运行时》是一组共十篇的系列文章,面向有后端工程经验、尤其是 Java 背景、准备进入 AI-Infra 方向的工程师,系统梳理 PyTorch 从 Python API 到 C++、CUDA、编译器、性能分析和分布式运行时的主要机制。
它同时是《AI 算法工程师学习地图》 L1 工具箱的深入篇:算法侧的《算法工程师的工具箱》讲 PyTorch 的”用”——五个对象、二十行训练循环、显存的账——本系列讲”改”:Dispatcher、Autograd 引擎、编译、分布式。两张地图共享本系列,算法方向的读者按需读。
这个系列不是 PyTorch API 速查表,也不是机器学习算法教程,而是试图回答一个问题:
PyTorch 如何把张量计算表达成可求导、可扩展、可优化、可分布式执行的深度学习系统?
一个训练或推理调用,看起来可能只是:
output = model(inputs)
loss = criterion(output, targets)
loss.backward()
optimizer.step()
但这几行代码背后涉及一整套运行时机制:
- Tensor 如何描述数据、布局、类型和设备;
- Autograd 如何构建和遍历动态计算图;
nn.Module如何管理参数、子模块和状态;- Dispatcher 如何根据设备和运行时上下文选择算子实现;
- Python 如何连接到 C++、CUDA 和底层数学库;
- 编译器如何捕获动态图并生成优化后的执行代码;
- GPU 程序为什么会受到访存、同步、Kernel Launch 和数据搬运的影响;
- 多个进程如何通过通信协同完成一次训练;
- 一个新算子如何经过测试、Benchmark、构建和兼容性治理。
PyTorch 在 AI 系统中的位置可以概括为:
模型与训练代码
↓
PyTorch 编程模型
↓
Tensor / Autograd / Module
↓
Dispatcher / ATen / Kernel
↓
Compiler / Runtime / Distributed
↓
CPU / GPU / 加速器
这个系列的重点不是记住更多 API,而是建立从上层模型代码一直追踪到硬件执行的能力。
为什么写这个系列?
会调用 PyTorch,不等于理解 PyTorch
很多教程可以让读者很快完成下面的代码:
model = MyModel().cuda()
output = model(x.cuda())
loss = loss_fn(output, target.cuda())
loss.backward()
optimizer.step()
但真正进入训练框架、推理框架或算子开发之后,问题会变成:
- 为什么一个 Tensor 需要
shape、stride、dtype和device? - 为什么
view()有时不需要复制数据,有时却直接失败? loss.backward()究竟是如何得到梯度的?- 为什么
model(x)不等于直接调用model.forward(x)? nn.Parameter为什么会被 Optimizer 自动发现?- 同一个算子为什么能够同时运行在 CPU 和 CUDA 上?
- Python 调用是如何进入 C++ 和 CUDA 的?
torch.compile()为什么有时加速,有时反而变慢?- GPU 利用率不高时,问题究竟在模型、数据、Kernel 还是同步?
- DDP 与 FSDP 分别解决什么问题?
- 一个自定义算子怎样才能真正具备生产质量?
如果只停留在 API 层,这些问题往往只能依靠搜索和经验解决。这个系列希望把这些现象还原成可以理解和验证的机制。
PyTorch 不是一个单纯的 Python 库
PyTorch 的用户入口主要是 Python,但它的执行能力来自多个层次的协作:
Python API
↓
Python Binding
↓
Operator Schema
↓
Dispatcher
↓
ATen
↓
CPU / CUDA / Meta Kernel
↓
底层数学库与硬件
因此,理解 PyTorch 不能只看 torch.nn 的 Python 代码,也不能直接跳到 CUDA Kernel。需要沿着一条连续的学习路径逐层深入:
编程模型 → 数据抽象 → 自动求导 → 算子系统 → 扩展机制
→ 编译执行 → 性能工程 → 分布式运行时 → 工程治理
Java 的知识可以成为脚手架,但不能成为边界
Java 背景会帮助理解很多工程概念:
nn.Module可以类比为具有层级管理能力的对象容器;state_dict可以类比为结构化的状态快照;- DataLoader 的 worker 可以类比为生产者—消费者系统;
- Process Group 可以类比分布式通信域;
- Dispatcher 可以帮助联想到运行时分发;
- PyTorch 的测试、构建和兼容性问题与大型 Java 框架同样重要。
但也必须明确类比的边界:
- Tensor 不是
List<List<Float>>; - Autograd 不是普通事件回调;
- DDP 不是 RPC 或负载均衡;
torch.compile()不是简单的javac;- CUDA Stream 不是 Java Executor;
- checkpoint 不是普通的 Java 对象序列化。
本系列会使用 Java 作为参照系,但最终目标是建立 PyTorch 自己的运行时模型。
适合哪些读者?
从后端转向 AI-Infra 的工程师
这是本系列的主要读者。适合有 Java、Go、C++ 等后端工程经验,已经掌握基本 Python,准备参与以下工作的工程师:
- 模型训练平台;
- 推理服务;
- GPU 资源管理;
- 深度学习框架;
- 自定义算子与硬件适配;
- 分布式训练;
- AI 平台和中间件。
如果你希望理解下面这些问题,本系列会比较适合:
- Tensor 的内存布局如何影响算子性能?
- 为什么 GPU 计算经常需要显式同步?
- 为什么 DataLoader 会让 GPU“吃不饱”?
- PyTorch 如何在不同设备上找到正确的 Kernel?
torch.compile()的 graph break 是什么?- DDP 如何同步梯度?FSDP 为什么能够节省显存?
- 一个 C++/CUDA 扩展如何安全地接入 PyTorch?
- 如何为算子设计跨设备、跨 dtype 的测试?
想读懂 PyTorch 和训练框架源码的开发者
适合准备阅读或贡献 PyTorch、Transformers、DeepSpeed、TorchTitan、Triton 等项目的开发者。
这些项目中的很多复杂性并不来自 Python 语法,而来自不同层次的协作:
- Tensor metadata 决定数据如何解释;
- Autograd 决定梯度如何传播;
- Dispatcher 决定算子如何分发;
- C++ 和 CUDA 决定底层执行;
- 编译器决定计算图如何变换;
- 分布式运行时决定参数和梯度如何通信。
已经会训练模型、但希望提升工程能力的开发者
适合已经能够使用 PyTorch 训练模型,但遇到过以下问题的读者:
- 训练速度不稳定;
- GPU 利用率很低;
- 显存逐步增长;
- 换一个 batch size 就 OOM;
- 多卡训练速度没有线性提升;
- 模型在 eager 下正常,compile 后结果异常;
- 自定义算子无法处理非连续 Tensor;
- 代码只能在当前机器和当前 CUDA 环境运行。
希望进入深度学习框架和系统方向的开发者
如果你希望从“使用模型”进一步走向“理解和开发模型执行系统”,这套系列可以作为从 Python 工程到深度学习运行时之间的桥梁。
系列的整体主线
十篇文章按照 PyTorch 的内部抽象逐步展开:
第一篇:建立全局地图
↓
第二篇:理解 Tensor 如何表示数据
↓
第三篇:理解 Autograd 如何表示梯度计算
↓
第四篇:理解 Module 如何组织模型和训练
↓
第五篇:理解 Dispatcher 如何选择算子实现
↓
第六篇:理解 Python 如何扩展到 C++ 和 CUDA
↓
第七篇:理解动态图如何进入编译执行
↓
第八篇:理解程序为什么快或慢
↓
第九篇:理解多卡如何通过通信协同
↓
第十篇:理解一次改动如何经过测试、CI 和发布安全地到达用户
可以把这条主线进一步归纳为三条相互交织的线索:
抽象线:Tensor → Autograd → Module → Operator → Compiler
执行线:Python → C++ → CUDA → Kernel → Hardware
工程线:Training → Profiling → Distributed → Testing → Build
需要说明的是,这条主线按职责层推进,而不是按源码目录。PyTorch 的源码按库分层是 torch/(Python)→ torch/csrc/(绑定、Autograd 引擎、c10d)→ aten/src/ATen/(Dispatcher、算子)→ c10/(TensorImpl、Device、Allocator),两种分层并不重合:第二篇讲的 Tensor 元数据在源码上住在最底层的 c10,却是用户最先接触、其余一切所依赖的抽象,所以放在最前面。第一篇会给出一张”组件 → 源码位置 → 职责层 → 展开篇”的对照表;第二到九篇的小结各附一张”本篇涉及的源码位置”表,把该篇讨论的机制落到具体文件;第十篇的仓库地图则给出完整目录。读者可以随时在职责层和源码层两个坐标系之间切换。
把上面几条线索、源码坐标和后文”贯穿全系列的实践线”合在一起,十篇文章可以汇总成一张表:
| 篇次 | 标题 | 所在线索 | 核心源码目录 | 实践落点 |
|---|---|---|---|---|
| 1 | PyTorch 整体介绍 | 三条线的起点:全局地图 | 全仓库四层:torch/ → torch/csrc/ → aten/src/ATen/ → c10/ |
无独立项目,建立地图与对照表 |
| 2 | Tensor 与内存布局 | 抽象线:Tensor | c10/core/(TensorImpl、StorageImpl) |
简化版 Tensor:用 shape、stride、offset 实现索引、transpose 和 contiguous copy |
| 3 | 自动求导与动态计算图 | 抽象线:Autograd | torch/csrc/autograd/ |
Mini-Autograd:加法与乘法节点、保存父节点、拓扑排序、反向传播 |
| 4 | nn.Module 与训练系统 |
抽象线:Module;工程线:Training | torch/nn/ torch/optim/ torch/utils/data/ |
一个完整的训练程序 |
| 5 | Dispatcher 与算子系统 | 抽象线:Operator;执行线:C++ | aten/src/ATen/ torchgen/ |
原生算子 add / add_ / add.out 的完整路径 |
| 6 | C++ 扩展与自定义算子 | 执行线:Python → C++ → CUDA → Kernel | torch/csrc/ torch/utils/cpp_extension.py |
scale_shift 算子(myops 项目):Python 契约 → C++ CPU → CUDA → Autograd 与 Meta |
| 7 | 编译执行与图优化 | 抽象线:Compiler | torch/_dynamo/ torch/_functorch/ torch/_inductor/ torch/fx/ |
一个带 shape 分支的小函数,跟踪其四次调用 |
| 8 | 性能优化与调试 | 工程线:Profiling;执行线:Kernel → Hardware | torch/profiler/ c10/cuda/ torch/cuda/ |
一个 Transformer block 的训练 step:基线 → 采集 → 归类 → 处方 → 优化报告 |
| 9 | 分布式 PyTorch | 工程线:Distributed | torch/csrc/distributed/ torch/distributed/ |
同一个 Transformer block 从 8 卡扩到 4 机 32 卡,算清显存与通信账 |
| 10 | PyTorch 的工程体系 | 工程线:Testing → Build | test/ torch/testing/ tools/ .github/ |
把 myops 项目走完构建、正确性、性能、CI、发布、兼容七关 |
这不是严格的单向依赖:
- 第五篇会复用第二篇关于 stride、dtype 和 device 的知识;
- 第六篇会复用第五篇关于 Operator Schema 和 Dispatcher 的知识;
- 第七篇会同时复用 Tensor metadata 和算子系统;
- 第八篇会帮助判断第六篇的自定义 Kernel 是否真的有效;
- 第九篇会复用第八篇的性能分析方法,并把第八篇的案例模型扩到多卡;
- 第十篇会把第六篇的自定义算子重新拿出来,让它走完一次完整的工程生命周期。
章节结构与分章导读
1. PyTorch 整体介绍
第一篇从全局开始,介绍一段模型代码如何穿过 Python、C++、CUDA、Dispatcher、Compiler 和硬件执行层,帮助读者建立后续文章所需的整体地图。
这一篇会讨论:
- 深度学习框架解决什么问题;
- PyTorch 与 TensorFlow、JAX 等框架的设计差异;
- PyTorch 从动态图走向编译执行的架构演进;
- Eager Mode 的基本定位;
- Python、C++、CUDA 三层之间的关系;
- Tensor、Autograd、Dispatcher、ATen、Kernel 的关系;
torch.compile、分布式和扩展机制在整体架构中的位置。
核心问题是:
执行
torch.add(x, y)时,PyTorch 内部发生了什么?
本篇会给出一条用于建立心智模型的典型调用路径:
Python API
↓
Python Binding
↓
Operator Schema
↓
Dispatcher
↓
ATen Operator
↓
CPU / CUDA Kernel
↓
Hardware
这是一条概念路径,不意味着所有算子、所有版本都经过完全相同的源码调用栈。后续文章会分别展开其中的关键层次。
Eager Mode 在全系列中承担一条贯穿主线:第一篇介绍它是什么,第三篇解释它如何构建动态计算图,第七篇说明它如何进入编译执行,第八篇分析它的性能特征和代价。
2. Tensor 与内存布局
第二篇讨论 PyTorch 最核心的数据抽象。重点不是罗列 torch.zeros()、torch.ones() 等 API,而是理解 Tensor 如何解释一段设备上的数据。
这一篇会覆盖:
- Tensor、Storage 和数据指针;
- shape、dtype、device;
- stride;
- storage offset;
- contiguous 与 non-contiguous;
view、reshape、transpose、permute;- 广播与零拷贝视图;
- CPU Tensor 与 CUDA Tensor;
- dtype promotion;
- in-place 操作;
- 数据拷贝与设备迁移。
可以把一个简化版 Tensor 表示为:
Tensor
├── storage
├── sizes
├── strides
├── storage_offset
├── dtype
└── device
这一篇会配套实现一个只支持 CPU 的简化版 Tensor,用来理解:
- shape 如何描述逻辑形状;
- stride 如何解释底层存储;
- transpose 为什么可以只改变 metadata;
- view 为什么有时不需要拷贝;
- contiguous 为什么可能触发真实数据复制。
Tensor 不只是一个多维数组,它是数据、布局、类型和设备位置的组合。后面讲算子、Kernel、显存和性能时,都会回到这一抽象。
3. 自动求导与动态计算图
第三篇从链式法则出发,解释 PyTorch 如何把数学上的梯度计算转化为可执行的反向图遍历。
这一篇会讨论:
- 链式法则;
- 标量、向量和张量求导;
- Eager Mode 下的动态计算图;
- leaf Tensor;
requires_grad;grad_fn;- Autograd Function;
saved_tensors;- 梯度累积;
backward();detach();no_grad();inference_mode();- 自定义
autograd.Function。
本篇会实现一个 Mini-Autograd:
加法和乘法
↓
保存父节点
↓
保存局部导数
↓
拓扑排序
↓
反向传播
↓
梯度累积
核心问题不是“如何调用 loss.backward()”,而是:
PyTorch 如何把链式法则变成一次沿计算图执行的反向传播?
同时,本篇会解释每次 forward 为什么可以创建一张新的图,以及 Python 控制流为什么能够直接参与 Eager Mode 下的模型计算。
4. nn.Module 与训练系统
第四篇从模型对象开始,讨论 PyTorch 如何组织参数、子模块、状态、数据和训练循环。
这一篇会覆盖:
nn.Module;nn.Parameter;- 参数注册;
- 子模块注册;
register_buffer();state_dict();load_state_dict();train()与eval();- hooks;
- Optimizer;
- param groups;
- 梯度清零与梯度累积;
- Dataset、Sampler、DataLoader;
- pinned memory;
- CPU-GPU 数据传输;
- 混合精度训练的基本概念。
内部会分成几个部分:
4.1 模型对象与模块树
4.2 参数、Buffer 与 state_dict
4.3 Optimizer 与训练循环
4.4 Dataset、Sampler、DataLoader 与数据搬运
4.5 混合精度训练入门
这一篇重点回答:
- 为什么
Parameter会被 Optimizer 自动发现? - 为什么普通 Tensor 不会自动进入
state_dict? Module.__setattr__()如何管理参数和子模块?- 为什么 DataLoader 可能成为训练瓶颈?
- 为什么
model.eval()不等于关闭梯度?
Java 对照会帮助理解 Module 的层级管理、state_dict 的状态快照、DataLoader 的生产者—消费者模型,以及 Optimizer 对参数状态的批量更新。
5. Dispatcher 与算子系统
第五篇是从 PyTorch 使用者走向 PyTorch 源码阅读者的关键一步。
这一篇会以简单的 add 算子为入口,分析:
add
add_
add.out
并讨论:
- Operator Schema;
- functional、in-place 和 out variant;
- Dispatcher;
- Dispatch Key;
- CPU、CUDA、Autograd、Meta 后端;
- ATen;
- Native Functions;
- TensorIterator;
- Composite Kernel;
- Meta Kernel;
- 算子代码生成。
核心问题是:
- 同一个算子为什么可以支持 CPU 和 CUDA?
- Dispatcher 根据什么选择实现?
- Autograd Kernel 与设备 Kernel 是什么关系?
- TensorIterator 如何处理广播和不同 stride?
- Meta Kernel 为什么可以在没有真实数据的情况下推断结果?
- PyTorch 为什么需要算子代码生成?
这一篇不从复杂的 Attention 算子开始,而是先通过简单算子建立 Dispatcher、ATen 和 Kernel 之间的关系,再把这套方法迁移到更复杂的算子。
6. C++ 扩展与自定义算子
第六篇从“阅读框架”走向“扩展框架”,讨论 Python 如何连接到 C++ 和 CUDA。
这一篇会覆盖:
- PyTorch C++ Extension;
- pybind11;
- ATen C++ API;
at::Tensor;- Tensor metadata;
data_ptr();- CMake 与 Ninja;
- CPU 自定义算子;
- CUDA 自定义算子;
- 自定义 Autograd;
- 算子注册;
- ABI 和编译问题;
- Python 与 C++ 之间的数据传递。
实践项目分成四个阶段:
阶段 1:用 Python 实现一个算子
阶段 2:用 C++ 实现 CPU 算子
阶段 3:用 CUDA 实现 GPU 算子
阶段 4:补充 Autograd、测试和 Benchmark
这一篇的重点不是记住一套编译命令,而是理解:
Python API
↓
pybind11 / Python Binding
↓
C++ Tensor
↓
Tensor metadata
↓
CPU / CUDA Kernel
↓
Autograd / Dispatcher
一个可用的自定义算子不能只在连续的 float32 Tensor 上运行,还需要认真处理 dtype、device、stride、空 Tensor、生命周期、错误检查和 backward。
7. 编译执行与图优化
第七篇讨论 PyTorch 如何在保留 Eager Mode 灵活性的同时,通过编译器获得更高的执行效率。
这一篇会覆盖:
- FX Graph;
- Symbolic Tracing;
- Graph、Node、GraphModule;
- Graph Rewrite;
- TorchDynamo;
- Graph Break;
- Guard;
- Dynamic Shape;
- AOTAutograd;
- TorchInductor;
- Triton;
- 算子融合;
- 内存规划;
- 编译缓存。
贯穿全文的典型流程是:
Python Model
↓
TorchDynamo
↓
FX Graph
↓
AOTAutograd
↓
TorchInductor
↓
Triton / C++ / Library Call
↓
CPU / GPU
本篇会用一个刻意简单的函数贯穿全文:
def f(x, weight, bias):
y = x @ weight + bias
if x.shape[0] > 64:
return torch.relu(y)
return torch.tanh(y)
一段直线的矩阵计算让编译器的前端、中端、后端各有事可做;一个依赖输入 shape 的 Python 分支则暴露了”从 Python 程序中捕获图”的全部难点。本篇会跟踪这个函数的四次调用——冷编译、热路径、shape 变化、走到另一条分支——分别发生了什么。
重点区分:
- Eager Mode 中立即执行的算子;
- FX 对程序的图表示;
- 编译器内部的中间表示;
- 最终执行的 CPU/CUDA Kernel。
还会重点解释 graph break、guard、动态 shape 和编译缓存,因为 torch.compile() 并不是把任意 Python 代码直接转换成 CUDA,而是尝试捕获其中可分析的计算部分。
8. 性能优化与调试
第八篇围绕一个核心问题展开:
如何判断一个 PyTorch 程序慢,以及如何定位它为什么慢?
这一篇的重心是性能模型和测量方法,优化手段是模型推导出来的结论。它从一个事实出发——CPU 和 GPU 是两条通过队列连接的异步时间线——把性能问题分成两个维度:
时间维度:五类瓶颈
CPU 侧 Python-bound、Launch-bound
GPU 侧 Memory-bound、Compute-bound
两侧之间 Sync-bound
空间维度:显存
真的不够 · 碎片 · 泄漏 · 峰值
内部结构为:
8.1 度量与工具地图:异步执行模型、Benchmark 方法、Profiler 与 Nsight
8.2 时间维度:五类瓶颈及各自的判断依据与处方
8.3 空间维度:显存的构成、Caching Allocator、碎片、峰值、泄漏与时空互换
8.4 完整案例:一个 Transformer block 的训练 step
这一篇会覆盖:
- CPU 与 CUDA 异步执行、CUDA Stream 与 CUDA Event;
torch.cuda.synchronize()与隐式同步点;torch.utils.benchmark、PyTorch Profiler、Nsight Systems、Nsight Compute;- Kernel Launch 开销、CUDA Graphs;
- Memory Bandwidth、Compute Throughput、FLOPs、Arithmetic Intensity、Occupancy;
- 低精度(FP16、BF16、TF32)作为处方及其代价;
- CUDA Caching Allocator、
allocated与reserved、显存碎片; - 训练显存的构成与峰值;
- Activation Checkpointing 等时间与空间的互换。
所有性能结论都遵循同一套流程:
建立基线
↓
设计正确性测试
↓
采集性能数据
↓
定位瓶颈
↓
修改实现
↓
重新 Benchmark
↓
确认没有性能回归
优化时不仅要说明“快了多少”,还要说明:
- 节省的是计算、访存、同步还是 Kernel Launch;
- 是否增加了显存;
- 是否改变了数值精度;
- 是否只对特定输入 shape 有效;
- 是否把成本从运行时转移到了编译或初始化阶段。
9. 分布式 PyTorch
第九篇回答单卡放不下模型或跑不完数据之后的问题。它不按 API 组织(DDP 一章、FSDP 一章、张量并行一章),而是用一条主线把所有并行策略放进同一张表:
每种并行策略,都是对训练中的五类状态——数据、参数、梯度、优化器状态、激活值——各自做一个决定:复制还是分片。每个决定对应一种集合通信原语和一个通信时机;所有决定加起来,决定了显存占用和通信量。
全文分三层:
运行时与工程 torchrun 启动 · 数据切分 · 分布式 Checkpoint · 多机拓扑 · 通信性能分析 · hang 的排查
↑
并行策略 DDP · ZeRO / FSDP · TP · PP · CP · EP · 多维组合
↑
通信底座 进程 / Rank / 进程组 · 集合通信原语 · NCCL · α + β 成本模型
这一篇会覆盖:
- SPMD 执行模型、Process、Rank、World Size、Process Group、
DeviceMesh; - NCCL 与 Gloo;
- Broadcast、Reduce、AllReduce、AllGather、ReduceScatter、AllToAll、P2P;
- 通信成本模型:α + β、Ring 与 Tree、
algbw与busbw; - 通信的异步语义与通信/计算重叠;
- DDP:Reducer、Gradient Bucket、通信 hook;
- ZeRO 三阶段与 FSDP2:
fully_shard、DTensor、分片单元、预取、HSDP; - 张量并行(TP):列/行切分、Megatron 式共轭算子、词表并行、Sequence Parallel;
- 流水线并行(PP):GPipe、1F1B、交错调度、Zero Bubble、
torch.distributed.pipelining; - 上下文并行(CP):Ring Attention、负载均衡;
- 专家并行(EP):dispatch/combine、容量因子、负载均衡损失;
- 多维混合并行的组合原则,以及训练与推理的并行选择差异;
- 分布式 Checkpoint、多机拓扑、通信性能分析、hang 的排查与扩展效率。
完整案例把第八篇的 Transformer block 训练 step 从 8 卡扩到 4 机 32 卡,逐步说明每一次扩展为什么要换并行策略,以及每一步的显存与通信账。
这一篇的重点不是罗列分布式 API,而是回答:
- 为什么需要通信?通信发生在 step 的哪个时刻?
- 每种策略的显存和通信量怎么自己算出来?
- 为什么 FSDP 比 DDP 多 50% 通信量?为什么 TP 只能在节点内做?
- 什么时候该从数据并行换到模型并行,多种并行怎么组合?
- 为什么增加 GPU 数量不一定带来线性加速?
10. PyTorch 的工程体系:一次改动如何安全地到达用户
前九篇描述的都是一个已经存在、并且正确运行的系统。第十篇换一个问题:它是怎么做到一直正确、一直可用的?
一个框架的工程体系,就是一次改动从写下到进入用户生产环境所经过的全部关卡,以及每个关卡守住什么。
全文沿着一次改动的生命周期走一遍:
写下改动
↓
① 本地构建能跑 仓库地图 · 构建流程与 Codegen · Debug 构建 · 调试到 C++
↓
② 结果正确 五种 oracle · OpInfo · 设备与 dtype 泛化 · gradcheck 内部 · 确定性
↓
③ 没有变慢 微基准与指令数 · TorchBench 与编译器看板 · 噪声与阈值
↓
④ 审查与 CI 合入 CI 分层 · flaky 的流程化处理 · 审批、MergeBot 与回滚
↓
⑤ 随版本发布 节奏与 release 分支 · wheel 矩阵与 ABI · 平台支持窗口
↓
⑥ 用户升级不坏 Python API 弃用周期 · 算子 Schema 的 BC/FC · C++ 稳定子集 · state_dict 版本
↓
⑦ 使用者跟随演进 版本策略 · 升级 playbook · 兼容矩阵 · nightly 与 RC
前六关是框架维护者的视角,第七关是使用者的视角。七个关卡共同守住三件事:正确性(主要由 ②)、性能(主要由 ③)、兼容性(主要由 ⑥);构建是一切验证的前提,合入与发布把验证变成强制的、自动的流程。
框架与应用的区别在于组合爆炸:两千多个算子 × 十几种 dtype × 多个后端 × 各种 shape 与 layout × 多种执行模式。这个事实塑造了数据驱动的 OpInfo 测试、分层的 CI,以及按稳定程度分级的接口面。
实践终点是把第六篇的 scale_shift 算子(myops 项目)重新拿出来,让它走完这七关:复用 PyTorch 的测试基础设施、建立 Benchmark 基线与回归阈值、搭 CI 矩阵、守住 Schema 契约并完成一次向后兼容的演进、发布制品,最后站到使用者一侧看如何跟随 PyTorch 升级。
本篇最后给出全系列总结:回到总纲的那段代码和那串追问,逐一作答,并标出每个答案来自哪一篇。
贯穿全系列的实践线
为了避免每一篇都停留在孤立的 Foo、Bar 和玩具代码上,系列用两条实践线把机制落到代码上。它们不追求重新实现 PyTorch,而是通过受控的简化实验和一个持续演进的例子,理解 PyTorch 的关键机制。
第一条:用简化实现理解机制
前半段用两个自己动手写的最小实现,把”看懂”变成”能复现”:
第二篇 简化版 Tensor 用 shape、stride、offset 实现索引、transpose 和 contiguous copy
第三篇 Mini-Autograd 用加法和乘法节点实现保存父节点、拓扑排序和反向传播
第二条:贯穿后半段的两个例子
后半段用两个持续演进的例子代替零散的代码片段。
一个是刻意简单的自定义算子:
scale_shift(x, alpha, beta) = alpha * x + beta
第六篇 实现它 Python 契约 → C++ CPU → CUDA → Autograd 与 Meta,接入 Dispatcher
第十篇 发布它 把它所在的 myops 项目走完构建、正确性、性能、CI、发布、兼容七关
另一个是一段真实的训练负载:
第七篇 一个带 shape 分支的小函数 跟踪它的四次调用,看编译器前端、中端、后端和运行时各做了什么
第八篇 一个 Transformer block 以它的训练 step 为对象,走完基线 → 采集 → 归类 → 处方 → 优化报告
第九篇 同一个 Transformer block 从 8 卡扩到 4 机 32 卡,逐步换并行策略,算清每一步的显存与通信账
第四篇和第五篇没有独立的实践项目:第四篇以一个完整训练程序作为落点,第五篇则以原生算子 add 的完整路径作为源码阅读的样例。
各篇实践的重心不同:简化实现关注机制是否复现,算子项目关注正确性测试与 Benchmark,案例章节关注测量方法与优化报告。共同的要求只有一条:每个结论都能在代码或数据上验证,而不是停留在示意图。
最终目标不是造出一个新的深度学习框架,而是能够从用户代码一路追踪到运行时和硬件,并能解释不同设计的收益与代价。
阅读路径建议
完整学习路径
推荐按照以下顺序阅读:
1 → 2 → 3 → 4 → 5 → 6 → 7 → 8 → 9 → 10
这是从用户编程模型走向框架内部和工程体系的完整路径。
想先理解模型训练
1 → 2 → 3 → 4
先建立 Tensor、Autograd、Module 和训练循环的基本认知。
想尽快读懂 PyTorch 算子源码
1 → 2 → 5 → 6
重点理解 Tensor metadata、Operator Schema、Dispatcher 和 C++/CUDA 扩展。
想进入 GPU 性能优化
1 → 2 → 5 → 6 → 7 → 8
先理解 Tensor 和算子,再进入 Kernel、编译器和性能分析。
想进入分布式训练基础设施
1 → 4 → 8 → 9 → 10
重点关注训练状态、显存、通信、性能和工程治理。
想进入推理系统和 LLM Serving
1 → 2 → 4 → 8 → 9
推理引擎建立在 PyTorch 的模型计算、GPU 执行、显存管理和多卡通信之上。这条路径覆盖它们依赖的基础,Serving 系统自身的请求调度、KV Cache 和集群问题不在本系列范围。
前置要求与说明
前置要求
建议读者具备:
- 一门静态类型语言的工程经验,Java、C++、Go 均可;
- 基本 Python 语法和面向对象基础;
- 基本的 Linux 命令行使用能力;
- 向量、矩阵、导数和链式法则的基础概念;
- 能够阅读简单的 C++ 代码。
不要求一开始就掌握:
- CUDA Kernel 编程;
- 分布式训练;
- 编译器原理;
- 深度学习框架源码;
- 某个具体模型的完整实现。
数学内容的安排
本系列不会先单独展开一整套机器学习数学课程,而是将必要数学放在对应机制旁边:
| PyTorch 主题 | 同步补充的数学 |
|---|---|
| Tensor | 向量、矩阵和维度 |
| Broadcasting | 维度对齐 |
| Linear | 矩阵乘法 |
| Loss | 函数与误差 |
| Autograd | 导数与链式法则 |
| Backward | 梯度和 Jacobian |
| Attention | 点积和 softmax |
| Distributed | 矩阵切分与通信 |
数学在这里的目标不是完成考试,而是回答:
- 这个 Tensor 的 shape 为什么是这样?
- 这个算子的计算量是多少?
- 梯度从哪里来?
- 为什么 batch size 会影响显存和吞吐?
- 为什么某种并行策略会减少显存,却增加通信?
版本基线
正文以 PyTorch 2.x(2.4 及之后) 为基线。系列的重点是机制而不是可复现的基准数据,所以不逐篇给出实验环境;正文中涉及某个版本才引入或行为发生变化的 API(例如 torch.library.custom_op、FSDP2 的 fully_shard、torch.load 的 weights_only 默认值)时,会在使用处随文标注版本。文中出现的延迟、带宽、利用率等数字除明确标注硬件规格外均为示意,用于说明数量级与比例关系。
PyTorch 的以下部分变化较快:
- AMP API;
torch.compile内部实现;torch.export;- Triton 集成;
- 分布式训练 API;
- C++/CUDA 扩展构建方式;
- CUDA 与硬件后端支持。
因此,文章会区分两类内容:
- 长期稳定的机制:Tensor metadata、Autograd、算子分发、通信原语等;
- 版本敏感的实现:编译器内部路径、具体 API、构建命令和配置参数。
阅读时应以对应版本的官方文档和源码为准。机制和分析方法通常比某个版本的命令更持久。
关于 AI-Infra 本身
本系列聚焦 PyTorch 的编程模型、执行引擎、算子系统、编译器、性能和分布式运行时,不试图覆盖所有 AI-Infra 领域。以下内容与本系列紧邻,但不在范围内:
- Python 语言本身:CPython 的对象模型、GIL、异步、类型系统。本系列假设读者已经掌握,只在 Python 与 C++ 边界处涉及;
- C++ 语言本身:模板、RAII、静态注册等在源码阅读中反复出现的特性。本系列解释它们在 PyTorch 中的用途,不讲语言机制;
- 单个 CUDA Kernel 的内部优化:访存合并、共享内存、Tensor Core 编程。第六篇给出读懂一个 Kernel 所需的最小概念,第八篇在系统层面判断瓶颈类型,Kernel 内部如何写到硬件极限不展开;
- LLM 推理系统:请求生命周期、Continuous Batching、KV Cache 管理、Prefill/Decode 分离、集群路由。本系列覆盖它们所依赖的模型计算、GPU 执行、显存和通信基础,不讨论 Serving 系统自身的设计。
章节目录
- PyTorch 整体介绍
- Tensor 与内存布局
- 自动求导与动态计算图
nn.Module与训练系统- Dispatcher 与算子系统
- C++ 扩展与自定义算子
- 编译执行与图优化
- 性能优化与调试
- 分布式 PyTorch
- PyTorch 的工程体系:一次改动如何安全地到达用户
最终目标
读完这套系列之后,读者应该能够从以下代码出发:
output = model(inputs)
loss = criterion(output, targets)
loss.backward()
一路追问并回答:
Tensor 如何表示输入?(第二篇)
↓
Module 如何组织模型?(第四篇)
↓
Autograd 如何构建计算图?(第三篇)
↓
Dispatcher 如何选择算子?(第五篇)
↓
自定义算子如何接入 Dispatcher 与 Autograd?(第六篇)
↓
Compiler 如何对计算进行变换?(第七篇)
↓
Profiler 如何告诉我们瓶颈在哪里?(第八篇)
↓
Distributed Runtime 如何让多卡协同?(第九篇)
↓
Tests、Build 和 CI 如何保证系统可持续演进?(第十篇)
最终的目标不是“会用 PyTorch”,而是具备下面三种能力:
- 阅读能力:能够读懂 PyTorch、训练框架和推理框架中的关键源码;
- 诊断能力:能够解释错误、显存增长、性能下降和分布式故障的原因;
- 扩展能力:能够设计、实现、测试和优化一个新的算子或运行时组件。
这正是从后端工程师走向 AI-Infra 工程师时,PyTorch 最值得深入学习的部分。
本文由 arganzheng 创作,采用 CC BY 4.0 许可协议。在保留原文作者、署名以及完整原文链接(https://arganzheng.life/deep-dive-into-pytorch.html)的前提下,欢迎各种形式的转载、翻译或商业引用。
COMMENTS
评论存放在 GitHub Discussions, 用 GitHub 账号登录即可发表,支持 Markdown。 想针对正文某句话说?选中那段文字,点浮出的「评论」即可划线评论;觉得哪里写错了,发表时勾上「同时提交 Issue」。 有人回复你时 GitHub 会按你的通知设置发邮件,不用守在这里。