ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

深度学习中的异步计算:前端/后端解耦、屏障同步与性能优化(D2L 实战解析)

深度学习中的异步计算:前端/后端解耦、屏障同步与性能优化(D2L 实战解析) 文档教程人工智能深度学习NLP计算机视觉强化学习【免费下载链接】d2l-enInteractive deep learning book with multi-framework code, math, and discussions. Adopted at 500 universities from 70 countries including Stanford, MIT, Harvard, and Cambridge.项目地址https://gitcode.com/gh_mirrors/d2/d2l-en点击查看免费下载本文以 D2LDive into Deep Learning仓库中 chapter_computational-performance/async-computation.md 为主线系统讲解深度学习框架为何采用异步编程模型、前端Python与后端C 执行引擎如何解耦协作以及waitall、wait_to_read、print、asnumpy等显式与隐式屏障对性能的影响。读完本文你将掌握借助异步执行加速训练/推理的底层原理学会用基准实验量化异步收益并能在 MXNet 与 PyTorch 两种框架中正确使用同步原语、避免同步陷阱。为什么深度学习框架需要异步编程今天的计算机是高度并行的系统一块 CPU 拥有多个核心每个核心往往还有多线程一块 GPU 内含大量处理单元单台设备上甚至常常挂载多块 GPU。换句话说我们可以在同一时刻、在多个不同设备上处理大量互不相关的计算。遗憾的是Python 并不擅长编写并行与异步代码——Python 是单线程语言而且这一特性在可预见的未来不会改变。深度学习框架因此普遍采用异步编程模型来绕开 Python 单线程的限制MXNet 与 TensorFlow从设计上就采用异步执行模型把计算命令即发即忘地交给后端引擎以此换取吞吐性能PyTorch则主要依赖 Python 自身的调度器形成不同的性能取舍。默认情况下PyTorch 的GPU 操作是异步的当你调用一个使用 GPU 的函数时操作只是被排队到对应设备上并不保证立刻执行完毕。这种排队机制允许 CPU 或其它 GPU 上的计算与当前操作并行推进。理解异步编程的运作方式有助于我们主动减少计算量与相互依赖从而降低内存开销、提高处理器利用率写出更高效的训练程序。本文实验代码位于 async-computation.md需要引入框架与 D2L 工具库# MXNet 版本 from d2l import mxnet as d2l import numpy, os, subprocess from mxnet import autograd, gluon, np, npx from mxnet.gluon import nn npx.set_np()# PyTorch 版本 from d2l import torch as d2l import numpy, os, subprocess import torch from torch import nn其中d2l.Benchmark是 D2L 提供的计时上下文管理器其实现位于 d2l/mxnet.pyPyTorch 对应实现见 d2l/torch.py底层复用d2l.Timer见 d2l/mxnet.py记录区间耗时class Benchmark: For measuring running time. def __init__(self, descriptionDone): self.description description def __enter__(self): self.timer d2l.Timer() return self def __exit__(self, *args): print(f{self.description}: {self.timer.stop():.4f} sec)通过后端实现的异步一个热身基准实验为了直观感受异步执行我们先做一个玩具问题生成随机矩阵并做矩阵乘法分别在 NumPy 与框架张量中完成对比两者耗时。MXNet 侧同机对比numpy与mxnet.npwith d2l.Benchmark(numpy): for _ in range(10): a numpy.random.normal(size(1000, 1000)) b numpy.dot(a, a) with d2l.Benchmark(mxnet.np): for _ in range(10): a np.random.normal(size(1000, 1000)) b np.dot(a, a)PyTorch 侧注意PyTorch 张量定义在 GPU 上先做一次预热# Warmup for GPU computation device d2l.try_gpu() a torch.randn(size(1000, 1000), devicedevice) b torch.mm(a, a) with d2l.Benchmark(numpy): for _ in range(10): a numpy.random.normal(size(1000, 1000)) b numpy.dot(a, a) with d2l.Benchmark(torch): for _ in range(10): a torch.randn(size(1000, 1000), devicedevice) b torch.mm(a, a)运行结果往往出人意料框架张量侧的耗时比 NumPy 快若干个数量级。在 MXNet 场景中两者在同一处理器上执行因此唯一的解释是另有隐情计算其实由后端在后台执行而前端Python 侧早已把控制权交还给了解释器在 PyTorch 场景中NumPy 的dot在 CPU 上执行而 PyTorch 的矩阵乘法在 GPU 上执行后者更快本属预期但差距大到反常同样提示我们 PyTorch 的 GPU 操作默认就是异步的。d2l.try_gpu()用于优先选择 GPU、无 GPU 时回退到 CPU其实现位于 d2l/torch.pydef try_gpu(i0): Return gpu(i) if exists, otherwise return cpu(). if num_gpus() i 1: return gpu(i) return cpu()强制同步后再测时间真相浮出水面如果在计时结束前强制框架做完所有事再返回就能看到真实耗时# MXNetnpx.waitall() 等待全部计算完成 with d2l.Benchmark(): for _ in range(10): a np.random.normal(size(1000, 1000)) b np.dot(a, a) npx.waitall()# PyTorchtorch.cuda.synchronize(device) 同步指定设备 with d2l.Benchmark(): for _ in range(10): a torch.randn(size(1000, 1000), devicedevice) b torch.mm(a, a) torch.cuda.synchronize(device)加上强制同步后耗时与 NumPy 才在同一量级。这印证了核心结论计算由后端线程执行而前端早已返回控制权给 Python——这就是异步带来的假快。前端与后端两种角色一条依赖感知的任务队列宽泛地说深度学习框架包含两部分前端frontend直接与用户交互例如 Python也可以是 R、Scala、C 等其它语言后端backend系统真正执行计算的部分以 C 实现为主。无论前端使用哪种编程语言MXNet/PyTorch 程序的执行都主要发生在 C 后端。前端语言发出的操作被传递到后端执行后端管理自己的线程这些线程不断收集并执行排队的任务。要做到这一点后端必须能够追踪计算图中各个步骤之间的依赖关系因此相互依赖的操作无法被并行化而彼此独立的操作则可以被并发执行。下图展示了编程语言前端与深度学习框架后端的对应关系图片来源 img/frontends.png依赖图的玩具示例再看一个更小的例子理解依赖追踪# MXNet x np.ones((1, 2)) y np.ones((1, 2)) z x * y 2 z# PyTorch x torch.ones((1, 2), devicedevice) y torch.ones((1, 2), devicedevice) z x * y 2 z[](img/asyncgraph.svg)上述代码的执行过程如下Python 前端线程执行前三条语句时只是把任务投递到后端队列便立即返回直到最后一条语句的结果需要被打印时Python 前端线程才真正等待 C 后端线程算完变量z的结果。这种设计的一个显著收益是Python 前端线程完全不需要执行实际计算因此无论 Python 本身的性能如何都不会影响程序的整体性能。前端与后端线程的交互过程如下图所示图源 img/threading.svg屏障与阻塞哪些操作会让 Python 等待异步执行并不总是零等待。在 MXNet 中有若干操作会强制 Python 等待计算完成它们分为显式屏障与隐式屏障两类。显式屏障waitall与wait_to_readnpx.waitall()等待所有计算完成无论计算指令是何时发出的。这是最简单粗暴的同步手段但除非万不得已实践中不建议使用——它会显著损害性能z.wait_to_read()只等待特定变量可用。调用后MXNet 会阻塞 Python 直到变量z计算完成但其它计算仍可继续推进。对比实验如下with d2l.Benchmark(waitall): b np.dot(a, a) npx.waitall() with d2l.Benchmark(wait_to_read): b np.dot(a, a) b.wait_to_read()在文档的实验中两种操作耗时大致相同。需要说明的是这里的a是此前热身实验中的矩阵二次计算已经命中缓存与调度状态因此两者差距并不明显当任务队列中存在大量未完成计算时两者的语义差异才会放大——waitall需要清空整个队列而wait_to_read只需等待目标变量所在的依赖子图。隐式屏障print、asnumpy、item除了显而易见的阻塞操作还需警惕隐式阻塞打印变量print打印显然要求变量已就绪因此print就是一个阻塞点z.asnumpy()转换为 NumPy 数组是阻塞的因为 NumPy 没有异步的概念它需要像print一样真正访问到数值z.item()转换为标量同样阻塞。with d2l.Benchmark(numpy conversion): b np.dot(a, a) b.asnumpy() with d2l.Benchmark(scalar conversion): b np.dot(a, a) b.sum().item()隐式屏障为何伤性能频繁地把少量数据从框架的内存空间拷贝到 NumPy或反向拷贝会摧毁本可高效的代码性能。原因在于每次这样的转换都会强制计算图先求出得到该数值所需的全部中间结果然后才能继续做别的事。换句话说一次asnumpy()可能引发整条依赖链的同步求值把异步流水线的收益一次性抹平。改善计算异步调度到底省了多少时间在高度多线程的系统上普通笔记本也常有 4 个线程以上多路服务器上线程数可超过 256操作调度本身的开销会变得相当可观。因此让计算与调度异步、并行地进行是非常必要的。为了量化收益我们对比顺序执行与异步执行下将变量自增 1 一万次10000 次的耗时。同步版本通过在每个加法之间插入wait_to_read屏障来模拟# MXNet with d2l.Benchmark(synchronous): for _ in range(10000): y x 1 y.wait_to_read() with d2l.Benchmark(asynchronous): for _ in range(10000): y x 1 npx.waitall()实测中异步版本明显更快。背后的时序模型可以简化为三个阶段前端命令后端把计算任务y x 1插入队列——耗时记为 $t_1$后端从队列中取出任务并完成实际计算——耗时记为 $t_2$后端把计算结果返回给前端——耗时记为 $t_3$。如果不使用异步编程10000 次计算的总耗时约为 $10000(t_1 t_2 t_3)$而采用异步编程后前端无需在每一轮循环中等待后端返回结果总耗时可以压缩到$$t_1 10000,t_2 t_3 \quad (\text{在 } 10000,t_2 9999,t_1 \text{ 的前提下})$$即前端只负责首尾各一次交互中间的计算全部由后端流水线化完成。为什么需要假设 $10000,t_2 9999,t_1$该公式成立的前提是后端计算是整条流水线的瓶颈只有后端处理全部任务的总耗时超过前端投递剩余任务的总耗时前端才能在计算期间从容投递、永不等待。若反之前端投递过慢$10000,t_1$ 主导总耗时就会被前端投递速度卡住公式便不再适用。这也是理解练习 1 的关键异步优化的是前端等待后端的空转时间而无法消除真正的计算瓶颈本身。实践建议按小批量同步一次异步带来的是更灵敏的前端但也要注意不要无限填充任务队列——队列过深会导致内存占用飙升。实践中的推荐做法是每个小批量minibatch训练结束同步一次例如 PyTorch 中在反向传播与优化器更新之间借助设备同步点、MXNet 中周期性调用waitall使前端与后端大致保持同步既保留流水线收益又避免内存失控。相关章节与延伸阅读异步计算是计算性能专题的一环本章导航见 chapter_computational-performance/index.md。可与之配合阅读hybridize.md命令式编程与符号式编程的权衡与异步调度密切相关auto-parallelism.md基于计算图依赖关系在 CPU/GPU 之间以及多 GPU 之间自动并行其中再次复用了本文的asyncgraph依赖图概念训练时 GPU 选择工具try_gpu/try_all_gpus见 d2l/mxnet.py 与 d2l/torch.py。总结深度学习框架可以将 Python 前端与执行后端解耦实现命令的快速异步插入与相应并行这是其性能优势的重要来源异步带来响应灵敏的前端但需注意不要过度填充任务队列导致内存膨胀推荐每个小批量同步一次让前后端保持大致同步芯片厂商提供了成熟的性能分析工具如 NVIDIA 的 profiling 工具链可获得比框架级计时更细粒度的效率洞察特别留意从框架内存管理转换到 Python 的操作print、asnumpy、item等会强制后端等待特定变量就绪这有时是必要的但粗心地滥用同步会毁掉性能。练习与思考文档提到异步计算可将 10000 次计算的总耗时降为 $t_1 10000 t_2 t_3$。为什么必须假设 $10000,t_2 9999,t_1$提示从后端计算总耗时 vs 前端投递总耗时谁主导流水线的角度分析思路见上文为什么需要假设小节。实测对比waitall与wait_to_read的差异。提示先向队列投递一批指令再对某个中间结果同步观察剩余指令是否仍能继续执行。在 CPU 上重复本节的矩阵乘法基准PyTorch 在 CPU 上执行时是否仍能观察到后端异步提示PyTorch 默认的异步行为主要针对 GPUCUDA 流CPU 路径的行为有所不同。赞分享文档教程人工智能深度学习NLP计算机视觉强化学习【免费下载链接】d2l-enInteractive deep learning book with multi-framework code, math, and discussions. Adopted at 500 universities from 70 countries including Stanford, MIT, Harvard, and Cambridge.项目地址https://gitcode.com/gh_mirrors/d2/d2l-en点击查看免费下载相关推荐异步计算Asynchronous Computation实战深度学习框架前端/后端解耦与性能优化异步计算Asynchronous Computation实战深度学习框架前端/后端解耦与性能优化 导读 《动手学深度学习》d2l zh是一本面向中文读人工智能深度学习机器学习教程《动手学深度学习》异步计算深度解析前端-后端解耦、障碍器阻塞器与性能调优实战《动手学深度学习》异步计算深度解析前端 后端解耦、障碍器阻塞器与性能调优实战 本文深入讲解《动手学深度学习》中异步计算一章的核心原理与实战方法深度学习框人工智能深度学习机器学习教程深度学习计算性能实战D2L 中的命令式编程、异步执行与多 GPU 并行优化深度学习计算性能实战D2L 中的命令式编程、异步执行与多 GPU 并行优化 导读 深度学习中数据集与模型通常规模庞大计算开销高昂计算性能直接决定训练效率文档教程人工智能深度学习NLP计算机视觉强化学习上一篇HomeObjects-3K 数据集实战指南用 YOLO26 训练室内家居物体检测模型下一篇邮件服务器灰名单功能如何有效减少垃圾邮件的终极指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表