深度学习框架的演进史与双雄格局的形成
第 1 章 深度学习框架的演进史与双雄格局的形成
深度学习框架之争的本质,从来不是"哪个 API 更好用",而是"什么样的计算抽象更适合表达智能"。当 2014 年 Google Brain 团队决定将 DistBelief 重写为一个开源系统时,他们押注的是符号式计算图(symbolic computation graph)在工业部署中的可优化性;当 2016 年 Facebook AI Research(FAIR)在 Torch 7 的基础上推出 PyTorch 时,他们押注的是动态图(dynamic computational graph)在研究迭代中的灵活性。十年后回头看,这两笔押注都没有输——它们各自占据了深度学习价值链的不同环节,并最终形成了本书所讨论的"双雄格局"。本章要回答的核心问题是:这种并存格局是历史的偶然,还是工程哲学的必然?要理解这一点,我们必须先回到深度学习框架的起点。
1.1 从 Theano 到 TensorFlow:符号式编程的工业崛起
1.1.1 Theano 的奠基意义(2007–2017)
任何讨论现代深度学习框架的演进史,都无法绕过蒙特利尔大学 Yoshua Bengio 团队在 2007 年发布的 Theano。Theano 并不是第一个支持张量运算的库——NumPy 在 2006 年就已经在科学计算领域普及——但 Theano 是第一个明确将计算图抽象(computational graph abstraction)作为一等公民(first-class citizen)的系统。在 Theano 的设计哲学中,用户首先声明一个由 `tensor` 节点和 `apply` 节点构成的有向无环图(Directed Acyclic Graph,DAG),然后通过 `function` 编译节点并执行。这种"先定义、后运行"(define-and-run)的范式带来了三个关键好处:第一,编译器可以在执行前对图进行代数化简、常量折叠和死代码消除;第二,图的拓扑结构对设备无关的并行化至关重要,CPU 与 GPU 的执行路径可以共享同一份高层描述;第三,静态图天然适合序列化为协议缓冲区(Protocol Buffer),便于跨语言、跨进程部署。
然而 Theano 的工程实现也暴露了符号式范式的根本缺陷:当用户编写的表达式出现维度不匹配、类型错误或设备不兼容时,错误信息指向的是编译后的图节点,而非用户编写的 Python 代码。这种"错误栈与源码脱钩"的体验在调试深度模型时尤其痛苦——一个 batch 内的运行时错误,可能在静态图构建阶段就被推迟到 `theano.function()` 调用之后才浮现。Bengio 团队在 2010 年前后通过引入 `debug_mode`、`ProfileMode` 等机制部分缓解了这个问题,但底层抽象的固有限制并未消除。
1.1.2 Caffe、CNTK 与并行宇宙(2013–2015)
在 Theano 主导学术圈的同时,工业界出现了若干并行的尝试。2013 年,贾扬清在伯克利大学发布的 Caffe 采用了一种声明式模型描述(declarative model description)——用户通过 `prototxt` 文件定义网络层、损失函数和数据流,而 C++ 引擎负责解析并执行。这种设计在计算机视觉领域获得了巨大成功,2014 年 ImageNet 比赛的几乎所有参赛队伍都在使用 Caffe,但其代价是研究人员在修改模型时必须编辑外部配置文件,并在 Python 与 C++ 之间反复切换调试。
与此同时,Microsoft Research 的 CNTK(Cognitive Toolkit)展示了符号式框架在大规模分布式训练上的潜力。CNTK 的核心抽象是复合表达式(composite expression),它在跨服务器同步梯度时展现了显著的性能优势,但代价是 API 极其陡峭,且与 Python 生态系统的集成度较低。MXNet(2015 年由 Apache 孵化)则尝试在符号式 API 与命令式 API 之间提供双模式(imperative + symbolic)的混合抽象,李沐团队的 `gluon` 接口即是这一思路的延续。
1.1.3 TensorFlow 的工业级重写(2015)
2015 年 11 月,Google Brain 正式开源 TensorFlow。从表面看,TensorFlow 是 DistBelief 的开源化版本,但其架构经历了根本性重写。TensorFlow 引入了几个关键设计:
第一,数据流图(dataflow graph)作为统一抽象。TensorFlow 的图由两类节点组成:运算节点(`tf.Operation`)和张量节点(`tf.Tensor`),边表示张量在运算之间的流动。这种"图即程序"的抽象使得跨设备、跨进程的分布式执行成为可能——同一份图可以被 `tf.train.MonitoredTrainingSession` 切割并分发到参数服务器(parameter server)与工作节点(worker)。
第二,XLA(Accelerated Linear Algebra)编译器。2016 年底,XLA 项目开始整合到 TensorFlow 中。XLA 通过 JIT 编译将 TensorFlow 图转换为针对特定硬件(CPU、GPU、TPU)优化的 LLVM IR,这一抽象在 Google 自研的 TPU 上展现了惊人的吞吐量。
第三,SavedModel 与 Serving 基础设施。TensorFlow 从一开始就考虑了模型部署场景,提供了从 Python 训练到 C++ 推理的完整工具链:冻结图(freeze graph)、SavedModel 格式、`tensorflow-serving` 服务器。这一套工具链在 2016–2018 年间几乎定义了"工业部署"的事实标准。
但 TensorFlow 1.x 也继承了符号式范式的所有缺点:会话(`tf.Session`)的隐式状态、占位符(`tf.placeholder`)的繁琐定义、图构建与图执行的分离导致的调试困难。这些痛点在 2017 年 FAIR 发布 PyTorch 后被无限放大,并最终迫使 Google 在 2019 年进行 TF 2.0 的范式重构(详见 1.3 节)。
1.2 Torch 与 Chainer 的遗产:动态图的学术血统
1.2.1 Torch 7 与 Lua 的兴衰(2002–2018)
Torch 的历史可追溯至 2002 年 Ronan Collobert 在 IDIAP 的工作,但真正让它在深度学习研究圈流行的是 NYU 的 Yann LeCun 团队和 Facebook 的人工智能研究部门。Torch 的核心数据结构是 `torch.Tensor`,其 API 深受 MATLAB 与 LISP 影响,采用命令式、面向对象的 Lua 脚本作为前端。这种设计带来了两个重要特征:
第一,算法即代码。在 Torch 中编写神经网络意味着直接操作张量对象:`model:forward(input)` 与 `model:backward(gradOutput)` 是普通的 Lua 方法调用,研究人员可以在任何地方打断点、修改中间变量、检查梯度数值。这种"程序即模型"的体验在调试复杂损失函数时尤其珍贵。
第二,手动反向传播。Torch 7 没有自动微分(autograd),用户必须显式实现 `updateGradInput` 与 `accGradParameters` 方法。这一限制虽然提高了门槛,但迫使研究者深入理解反向传播的每一步细节,并允许实现自定义梯度流(如 GAN、记忆网络、神经图灵机)而无需绕过框架的限制。
然而 Lua 生态的边缘化是 Torch 7 走向衰落的根本原因。随着 Python 在数据科学领域(NumPy、SciPy、pandas、scikit-learn)的统治地位确立,使用 Lua 编写深度学习模型变成了一种"语言税"。研究人员必须同时维护 Lua 模型代码与 Python 数据预处理管道,这在大规模实验中的摩擦成本不可忽视。
1.2.2 Chainer 的"先运行后定义"革命(2015)
2015 年,日本创业公司 Preferred Networks 发布的 Chainer 引入了一个看似简单却具有深远影响的创新:define-by-run(运行时定义)。在 Chainer 中,不存在"先构建图、再执行"的两阶段流程——相反,网络的拓扑结构在用户调用前向传播的过程中动态生成。这意味着每一次前向传播都可以产生不同的计算图,使变长序列、动态路由、递归结构等场景的实现变得自然。
Chainer 的技术核心是其 `Variable` 与 `Function` 节点构成的双链表结构。每个 `Variable` 持有 `creator` 引用,指向产生它的 `Function`;每个 `Function` 持有 `inputs` 与 `output_indices`,反向传播时沿着 `creator` 链回溯。这种"动态构建反向图"的设计是 PyTorch autograd 引擎的直接灵感来源。事实上,PyTorch 的核心开发者之一 Adam Paszke 在 2017 年的 NeurIPS 演讲中明确承认了 Chainer 的影响。
1.2.3 PyTorch 的诞生与 FAIR 的战略(2016–2018)
2016 年 10 月,FAIR 在 Torch 7 的基础上重写了整个框架,并将其命名为 PyTorch。PyTorch 保留了 Torch 的命令式哲学,但用 Python 替代了 Lua 作为前端语言,并深度整合了 Chainer 的 define-by-run 自动微分机制。几个关键决策塑造了 PyTorch 的产品形态:
第一,Python-first。PyTorch 的 C++ 后端(ATen、libtorch)几乎完全被 Python 接口所驱动,这与 TensorFlow 1.x 的"Python 是图构建的接口"形成了鲜明对比。在 PyTorch 中,张量是真正的 Python 对象,可以直接被 NumPy 接受,也可以被 Python 的 debugger(如 pdb、PyCharm)所支持。
第二,GPU 张量作为一等公民。`tensor.cuda()` 一行调用即可将张量从 CPU 内存迁移到 GPU 显存,这种"零成本"的设备切换使研究人员能够无缝地进行混合设备实验。
第三,torch.nn 与 torch.optim 的模块化设计。PyTorch 将模型定义为 `nn.Module` 的子类,将优化器抽象为 `optim.Optimizer` 的实例。这种"一切皆 Python 对象"的设计使得模型的保存、加载、克隆、组合都遵循 Python 的标准语义(pickle、深拷贝等)。
到 2018 年 PyTorch 1.0 发布时,它已经吸收了 Caffe2 的工业部署能力(ONNX 导出、移动端推理),并在学术界建立了压倒性的优势。FAIR 的这一战略选择,本质上是将"研究体验"作为产品差异化的核心——这与 Google 优先考虑"部署效率"的 TensorFlow 路线形成了清晰的对比。
1.3 2019 年范式转换:TF 2.0 与 PyTorch 1.0 的同化现象
1.3.1 TensorFlow 2.0 的"自我革命"
2019 年 9 月,Google 发布 TensorFlow 2.0。这一版本的核心改动可以概括为三个"默认":默认启用 Eager Execution(命令式执行)、默认使用 Keras 作为高层 API、默认启用 `tf.function` 自动图转换。这三个"默认"实质上承认了 PyTorch 路线的工程优越性:
- **Eager Execution** 消除了 `tf.Session` 与 `tf.placeholder` 的概念,使得 TensorFlow 的开发体验接近 NumPy/PyTorch;
- **Keras 作为默认 API** 提供了简洁的模型定义接口(`model = tf.keras.Sequential([...])`),与 PyTorch 的 `nn.Module` 在易用性上趋于一致;
- **`tf.function` 与 AutoGraph** 通过追踪(tracing)机制将 Python 函数转换为可优化的计算图,试图在保留命令式开发体验的同时获得符号式执行性能。
但 TF 2.0 的这一转型也带来了显著的内部张力。`tf.function` 的追踪机制要求用户在 Python 代码中标注"哪些部分是图、哪些部分是 Python"——这种隐式的边界引入了新的认知负担("我的代码为什么在 `@tf.function` 下行为不一致?")。同时,Keras 的高层抽象虽然易用,但在研究场景中往往成为限制——当用户需要实现自定义训练循环、自定义梯度或复杂的损失函数时,Keras 的封装反而成了障碍。
1.3.2 PyTorch 1.0 的"工业补课"
与 TensorFlow 的转型同步,PyTorch 在 2018–2020 年间也在悄悄补齐其工业部署能力:
- **TorchScript**(2018 年 PyTorch 1.0 引入)提供了将 Python 模型转换为静态图的能力,使其可以在没有 Python 解释器的环境中运行;
- **ONNX(Open Neural Network Exchange)** 作为中间表示格式,使 PyTorch 模型可以导出到 TensorRT、CoreML、TensorFlow Lite 等多种推理后端;
- **torch.distributed** 重写了分布式训练 API,引入了 DDP(DistributedDataParallel)、RPC、FSDP 等模块,对标 TensorFlow 的 `tf.distribute.Strategy`;
- **TorchServe**(2020 年与 AWS 合作发布)填补了 PyTorch 在模型服务化方面的空白。
值得注意的是,PyTorch 2.0(2022 年发布)引入了 `torch.compile`,这是对"动态图 vs 静态图"二元对立的又一次挑战。`torch.compile` 通过 TorchDynamo 提取 Python 字节码中的图片段,并通过 AOTAutograd、Inductor 等后端进行编译优化。其设计哲学与 TF 2.0 的 `tf.function` 惊人地相似——这表明两大框架正在向"命令式前端 + 编译优化后端"的统一架构收敛。
1.3.3 同化的工程意义
2019 年的这次"范式同化"具有深远的产业意义。它证明了一个判断:符号式与命令式不是非此即彼的设计选择,而是可以分层组合的工程模块。TensorFlow 学会了"以命令式为前端",PyTorch 学会了"以编译为后端",两者在架构上呈现出明显的趋同。这种趋同并非偶然,而是反映了深度学习工作负载的内在结构——上层需要灵活的 Python 编程接口,下层需要针对硬件的极致优化,两层之间通过某种形式的图表示进行桥接。
但同化并不意味着趋同。两大框架在默认 API、调试器集成、分布式原语、移动端支持、文档生态等细节层面仍存在显著差异,这些差异在 2024 年的 LLM 时代仍将持续影响开发者的选择。本书后续章节将逐一拆解这些差异的技术根源。
1.4 十年关键节点与技术拐点全景图
为了建立清晰的历史坐标系,本节以可视化方式呈现 2014–2024 年间深度学习框架演进的关键事件:
```mermaid flowchart TD A[Theano 2007 - symbolic graph] --> B[Caffe 2013 - prototxt] A --> C[TensorFlow 2015.11 - Google open source] B --> C D[Torch7 2002 - Lua] --> E[Chainer 2015 - define by run] E --> F[PyTorch 2016.10 - FAIR] C --> G[TF 1.x era 2016-2018] F --> H[PyTorch 1.0 2018.10 - TorchScript] G --> I[TF 2.0 2019.09 - eager default] H --> J[PyTorch 1.5 2020 - distributed refactor] I --> K[TF 2.x stable 2020-2022] J --> L[PyTorch 2.0 2022.03 - torch.compile] K --> M[JAX 2021 - functional transform] L --> N[LLM era 2023-2024] M --> N N --> O[Framework convergence and coexistence] ```
这张时间轴揭示了三个关键洞察:
第一,工业部署与学术研究的时间差。TensorFlow 2015 年发布,2016 年起在工业部署中迅速普及;但学术界对 PyTorch 的接受要到 2017–2018 年才完成(典型标志是 NeurIPS 2018 接收论文中 PyTorch 实现占比首次超过 TensorFlow)。这一时间差反映了工业与学术界对"易用性 vs 可部署性"权衡的不同偏好。
第二,框架竞争与硬件演进的耦合。2016 年 Google 发布 TPU 1.0,2018 年发布 TPU 2.0,2019 年发布 TPU 3.0。TPU 的发展史与 TensorFlow 的演进深度耦合,XLA 编译器是 TPU 编程的必备工具。NVIDIA 在 2017 年推出 Volta 架构(V100)与 TensorCore 之后,PyTorch 在 GPU 上的优化能力迅速追赶,并在 2020 年后与 TensorFlow 在性能上并驾齐驱。
第三,JAX 的"第三条道路"。2018 年由 Matt Johnson 等人开始内部孵化、2021 年正式对外的 JAX 提出了"函数式变换(functional