状态:草稿(2026-10-16),部分数据为实测。 torch.compile、内存布局与 cuDNN 设置是在家里的 8GB 笔记本级单 GPU 上通过一次性 Pod 测得(PyTorch 2.9,预热 20 次后 50 次 × 5 组的中位数)。融合原理通过笔记本 CPU 示例进行确认(
code/03_fusion.py,数秒)。由于实验镜像中没有 ONNX Runtime 和 TensorRT,故为 未实测(未下载)。 系列文章《视觉模型解读 3》第 3 篇。精度降低相关内容在 第 2 篇中。
目录
- 1. 直觉:厨师与冰箱
- 2. 动手尝试(笔记本 CPU)
- 3. 图编译器究竟做了什么
- 4. ONNX 与运行时:模型的便携格式
- 5. 实测:在球体模型上应用 torch.compile
- 6. 局限性
- 7. 与我们项目的关联
- 参考文献
1. 直觉:厨师与冰箱
GPU 的计算单元非常快,但数据存放在芯片外部的内存(冰箱)中。eager 模式会分别执行“卷积 → 批归一化 → ReLU → 相加”。卷积结果被存入冰箱,批归一化将其取出修改后再存入,ReLU 接着取出并存入。对于较大的特征图,存取的时间往往长于计算时间。 算子融合则是指“既然取出来了,就把四项工作做完,只存入一次”。
融合分为两种:
- 提前折叠数值:在推理时,批归一化(BN)对于每个通道而言是固定的乘加运算,因此可以提前合并到前置卷积的权重和偏置中。这样算子便会直接消失。
- 合并内核:将 ReLU、相加、改变格式等逐元素操作与前后内核合并为一个内核。
BN 접기: y = γ·(W∗x + b − μ)/√(σ²+ε) + β
= (γ/√(σ²+ε))·W ∗ x + (γ·(b − μ)/√(σ²+ε) + β)
= W' ∗ x + b' ← 합성곱 하나로
2. 动手尝试(笔记本 CPU)
code/03_fusion.py 在笔记本的单 CPU 线程上运行(7 次采样的中位数)。
| 实验 | 结果 |
|---|---|
| 折叠卷积(32→64, 3×3, 输入 144×256)与 BN | 最大差异 4.77e-06(FP32 四舍五入水平)。耗时 5.80 → 5.80ms(无差异) |
| 逐元素链式运算 relu(x·1.5 + 0.5)·0.9,元素 1,677 万个 | eager 模式 9.7ms → torch.compile 2.4ms(4.1 倍),结果差为 0 |
| 对手动原地(in-place)运算的相同链式 | 7.4ms |
| 将 102 万个元素的加法拆分为 1,000 次调用 | 0.13 → 0.88ms,每次调用约 0.8µs |
- 折叠 BN 虽在数学上得到了相同的结果,但在该 CPU 上耗时并未减少。在大型卷积旁边,单次 BN 的占比很小(第一次运行减少了 2.6%,第二次为 0% —— 处于噪声水平)。
- 逐元素链式运算则截然不同。eager 模式需要对大型数组进行四次读写,而编译版本只需读一次、写一次。计算逻辑相同,仅减少了内存往返,速度就提升了 4 倍。在该机器上,首次编译在无缓存时耗时 5.4 秒,有缓存时耗时 0.6 秒。
- 即使用手动的原地(in-place)运算也仅达到 7.4ms。它减少了新数组的分配,但依然需要四次读写。 人类在 Python 层面能做的优化是有限的,必须下沉到下一层(内核)去改变 才能大幅减少开销。
3. 图编译器究竟做了什么
TVM(Chen 等人,2018) 接收模型图并将算子分为四类:逐元素操作(injective)、规约操作(reduction)、可在输出端附加逐元素操作的复杂操作(如卷积等 complex-out-fusable),以及无法融合的操作。它根据规则将前三类进行融合。仅靠算子融合就提速了 1.2–2 倍(2015 年桌面级 GPU,包含卷积+BN+ReLU 等 4 个任务,见第 3 节图 4),相较于其他框架,终端性能达到 1.6–3.8 倍(6.1 节)。
PyTorch 2(Ansel 等人,2024) 的 torch.compile 分为两部分:
- TorchDynamo:拦截 Python 字节码以捕获计算图。如果遇到无法构建为图的 Python 代码,它会断开其前后端,其余部分保持原样运行。在数据中心 GPU 上,捕获图的开销在推理中约为 5%(表 2)。
- TorchInductor:将捕获到的图进行融合,生成适用于 GPU 的 Triton 内核(或适用于 CPU 的 C++代码)。
在数据中心 GPU 上,62 个 TIMM 图像模型的推理速度比 eager 模式在 FP32 下快 2.48 倍,在 FP16 下快 2.77 倍(表 3)。如果同时关闭融合与内联,速度反而会降至 eager 模式的 0.80 倍(表 4,HuggingFace FP16 推理)。 这说明绝大部分速度提升来源于融合。绿色部分的“推理 2.27 倍”未能在正文中确认是哪种组合的平均值,因此此处仅采用表 3 和表 4 的数值。
4. ONNX 与运行时:模型的便携格式
torch.compile 运行于 PyTorch 内部。如果你不希望在服务器上部署 Python 和 PyTorch,或者需要在浏览器和手机上运行,则必须将模型导出为 标准图文件。
- ONNX:一种定义了算子类型和计算图的公开格式。从 PyTorch 导出后可供其他运行时读取。
- ONNX Runtime:用于执行 ONNX 的运行时。它分三个阶段进行图优化:基础阶段(常量折叠、消除卷积+BN 等重复项)、扩展阶段(GELU、注意力机制等复杂融合,适用于 CPU、CUDA 等)、布局阶段(将内存布局调整为适配 CPU)。默认设置会全部开启(官方文档)。实际计算由 执行提供程序(Execution Provider)完成 —— 包括 CPU、CUDA、TensorRT、Core ML 以及浏览器的 WebGPU、WASM 等。跨设备携带同一个文件正是 ONNX 的价值所在(第 7 篇)。
- TensorRT:NVIDIA GPU 专用的优化器。它会融合层、实际运行测试各种内核候选并选择最快的、将精度降至 FP16 或 INT8。其结果是针对该特定 GPU 量身定制的“引擎”文件,若迁移到其他 GPU 则需要重新生成。过去它使用通过校准数据确定 INT8 范围的“校准器”(熵值、最大最小值),但根据官方文档,从 TensorRT 10.1 开始已废弃校准器,转而采用在模型内部插入量化与反量化节点的显式方式。
5. 实测:在球体模型上应用 torch.compile
我们在实验 GPU 上测试了 TT Score 的球体模型(BlurBall,3 帧窗口,512×288,批次大小 1)。
| 执行方式 | FP32 | FP16 | 首次调用(编译) |
|---|---|---|---|
| eager 模式(当前运行方式) | 15.09ms | 9.77ms | — |
| eager 模式,关闭 cuDNN 自动调优 | 15.08ms | 9.75ms | — |
| eager 模式,channels_last(NHWC) | 18.62ms | 9.02ms | — |
| torch.compile 默认模式 | 12.91ms(1.17 倍) | 4.61ms(2.12 倍) | 10–11 秒 |
| torch.compile reduce-overhead(CUDA 图) | 12.55ms | 4.34ms(2.25 倍) | 7–8 秒 |
| torch.compile max-autotune(无 CUDA 图) | 12.79ms | 4.48ms | 41–46 秒 |
(FP16 为 autocast。倍数对比相同精度的 eager 模式。)
通过性能分析器(Profiler)观察相同的执行过程,单次 FP32 eager 模式调用了 478 个 CUDA 内核,在 GPU 总工作时间 14.88ms 中,卷积占了 8.09ms(72 次),批归一化占了 3.27ms(71 次,占 22%)。由于内核耗时总和几乎等于墙钟时间(15.09ms),说明 GPU 一直在满负荷运转。因此问题不在于调用开销,而在于 频繁访问内存的逐元素算子,我们推测编译器通过合并消除了这些开销(未统计编译后版本的内核数量)。
解读如下。
- FP16 取得了更大的优势。 FP16 的 eager 模式下,卷积虽然变快了,但 BN、ReLU 和格式转换依然保留,导致占比变大,而编译器恰好消除了这些部分。第 2 篇的降低精度与第 3 篇的融合效果相乘(15.09 → 4.34ms,3.5 倍)。
- CUDA 图(reduce-overhead)带来了进一步的缩减。 由于将单次运行的内核打包成图一次性下发给 GPU,消除了每个内核产生的 Python 与驱动调用开销。由于该模型本身的 GPU 已经处于高负载状态,因此收益较小(4.61 → 4.34ms)。
- 长时间调优并未带来更多提速。 max-autotune 耗费了 45 秒,但速度并不比默认模式快。甚至还收到了警告,称该 GPU 的计算规模太小,不适合使用矩阵乘法自动调优。
- 内存布局(NHWC)仅在 FP16 下带来收益(9.77 → 9.02ms),而在 FP32 下则会导致性能下降(15.09 → 18.62ms)。cuDNN 自动调优没有表现出差异。比起“这种设置更快”的泛泛而谈,针对特定模型和精度亲自测量才是最准确的。
6. 局限性
- 首次调用耗时较长。 默认模式也需要 10 秒。实时工作线程在启动时会进行预热,因此可以掩盖这一开销,但如果输入形状(批次大小)发生变化,可能会触发重新编译。需要对批次大小 1、2、3 分别进行预热(第 6 篇)。未测试将编译缓存保留在磁盘上以减少重启时间(未实测)。
- 必须单独验证结果是否一致。 融合会改变加法顺序,因此末尾的数值可能会发生变化。本次未测量球体位置是否保持一致(未实测)。需要像第 2 篇的 FP16 对比那样,通过基准帧确认无误后再启用。
- 未能测量 ONNX Runtime 与 TensorRT。 实验镜像中没有,本次工作也没有下载新包。TensorRT 的 FP16 和 INT8 引擎是否比 torch.compile 更快,本文不作回答。
- 这是单 GPU、单模型的结果。对于以大矩阵乘法为核心的模型(如 Transformer),融合的效果可能会有所不同。
7. 与我们项目的关联
- 对于运维人员来说,现在直接执行 FP32。按照第 5 节的结论,通过 FP16 + 编译,单个窗口可以从 15ms 缩短至 4–5ms,这样在使用同一 GPU 承载更多会话或与其他任务混用时会有更多余量。在开启之前有两件事要做:使用录制比赛的标准帧检查球的位置和判定是否一致(第 2 篇的方法),以及在生产 GPU 上重新测量(更换 GPU 会改变收益 —— 第 6 篇)。
- 运维人员在启动时预热批次 1、2、3。开启编译后,该预热过程即为编译过程,因此准备时间会增加十几秒。准备就绪检查(
/healthz) 已经在预热之后打开,因此用户不会看到这段时间。 - 要在手机或浏览器上运行的模型,首先应将其导出为 ONNX(第 7 篇)。
TT 的总结。 做同样的菜,少开了几次冰箱门,速度就快了一倍。食材(权重)和菜谱(模型)明明都一样!
参考文献
- Chen, T. et al. (2018). TVM: An Automated End-to-End Optimizing Compiler for Deep Learning. OSDI 2018. https://arxiv.org/abs/1802.04799
- Ansel, J. et al. (2024). PyTorch 2: Faster Machine Learning Through Dynamic Python Bytecode Transformation and Graph Compilation. ASPLOS 2024. https://doi.org/10.1145/3620665.3640366
- ONNX. https://onnx.ai/ · ONNX Runtime 图优化. https://onnxruntime.ai/docs/performance/model-optimizations/graph-optimizations.html
- NVIDIA TensorRT 文档(How TensorRT Works, Explicit Quantization)。 https://docs.nvidia.com/deeplearning/tensorrt/latest/architecture/how-trt-works.html
- 我们的测量:本系列实测(原始数据见代码库外的研究记录)
- 示例代码与输出:此文件夹中的
code/03_fusion.py,code/03_fusion.out.txt