ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

《动手学深度学习》自动并行:基于计算图的多设备并行计算与通信原理实战

《动手学深度学习》自动并行:基于计算图的多设备并行计算与通信原理实战 人工智能深度学习机器学习教程【免费下载链接】d2l-zh《动手学深度学习》面向中文读者、能运行、可讨论。中英文版被70多个国家的500多所大学用于教学。项目地址https://gitcode.com/GitHub_Trending/d2/d2l-zh点击查看免费下载深度学习框架MXNet、飞桨与 PyTorch 等会在后端自动构建计算图。利用计算图系统可以掌握所有算子之间的依赖关系从而选择性地并行执行多个互不依赖的任务以提升运行速度。本文基于《动手学深度学习》d2l-zh仓库 chapter_computational-performance/auto-parallelism_origin.md 的中文版章节展开完整覆盖GPU 并行计算与计算与通信并行两个核心实验并结合仓库 d2l 目录下的源码实现d2l/mxnet.py、d2l/torch.py、d2l/paddle.py深入讲解其底层原理。读完本文你将掌握如何借助框架的计算图后端实现多 GPU 自动并行、如何利用non_blocking与waitall让 GPU 计算与 CPU 通信重叠执行以及如何在代码中验证并行化带来的实际加速。从异步计算到自动并行计算图的价值为什么单个设备不需要并行化现代计算机高度并行多个 CPU 核、多个 GPU、每个 GPU 内部又有多个处理单元与流。但深度学习框架的并行化收益与设备数量强相关。本节原文明确指出通常情况下单个操作符会使用所有 CPU 或单个 GPU 上的全部计算资源。例如dot算子矩阵点积即使在一台机器上有多个 CPU 处理器也会占用所有 CPU 上的所有核心和线程单个 GPU 同样如此。因此在单设备计算机上并行化的意义不大而多设备场景下并行化才真正重要。例如文献 Hadjis.Zhang.Mitliagkas.ea.2016 将结合 GPU 和 CPU 的训练应用到计算机视觉模型GPU 负责繁重的矩阵计算CPU 同时承担部分预处理或调度工作两者通过总线PCI Express协作。借助自动并行化框架的便利性我们只需几行 Python 代码即可实现同样的目标无需手写多线程调度。计算图是自动并行的基石本节主题承接上一节 异步计算sec_async。计算图由框架后端构建系统通过图结构了解全部依赖关系只对互不依赖的节点实施并行执行。例如sec_async中的fig_asyncgraph示例独立初始化两个变量系统即可选择并行执行它们。从 async-computation.md 的内容可以看到其背后机制Python 前端只负责入队每当 Python 前端线程执行一条语句时只是把任务提交到后端队列实际计算由 C 后端线程异步完成隐式阻塞器打印变量、asnumpy()转 NumPy、item()转标量都是阻塞器——它们要求变量立即可用从而被迫等待显式同步原语MXNet 的npx.waitall()等待所有计算完成PyTorch 的torch.cuda.synchronize()等待指定 CUDA 设备上所有流中的核心计算完成。理解这些同步原语是接下来设计并行实验的前提。实验前置运行环境与工具函数本节实验至少需要两块 GPU。代码通过d2l.try_all_gpus()获取设备列表该函数在仓库各框架实现中均有定义d2l/mxnet.py#L404-L409devices [npx.gpu(i) for i in range(npx.num_gpus())]若机器没有 GPU 则回退为[npx.cpu()]d2l/torch.py#L433-L439基于torch.cuda.device_count()构造cuda:{i}设备列表d2l/paddle.py#L444等价实现。计时使用d2l.Benchmark上下文管理器其实现如 d2l/torch.py#L1463-L1474、d2l/mxnet.py#L1351-L1362本质是进入时启动d2l.Timer退出时打印描述: 耗时 sec例如print(f{self.description}: {self.timer.stop():.4f} sec)。各框架的初始化代码分别如下# MXNet from d2l import mxnet as d2l from mxnet import np, npx npx.set_np()# PyTorch from d2l import torch as d2l import torch# 飞桨 from d2l import paddle as d2l import warnings warnings.filterwarnings(ignore) import paddle import numpy as np基于 GPU 的并行计算定义参考工作负载首先定义一个具有参考性的测试工作负载run函数对输入执行 50 次矩阵-矩阵乘法原文正文中的10 次指早期版本当前仓库代码为range(50)并将数据分配到分别位于不同设备上的两个变量x_gpu1与x_gpu2# MXNet devices d2l.try_all_gpus() def run(x): return [x.dot(x) for _ in range(50)] x_gpu1 np.random.uniform(size(4000, 4000), ctxdevices[0]) x_gpu2 np.random.uniform(size(4000, 4000), ctxdevices[1])# PyTorch devices d2l.try_all_gpus() def run(x): return [x.mm(x) for _ in range(50)] x_gpu1 torch.rand(size(4000, 4000), devicedevices[0]) x_gpu2 torch.rand(size(4000, 4000), devicedevices[1])# 飞桨 devices d2l.try_all_gpus() def run(x, index0): paddle.set_device(fgpu:{index}) return [x.matmul(x) for _ in range(50)] data np.random.rand(4000, 4000) x_gpu1 paddle.to_tensor(data, placedevices[0]) x_gpu2 paddle.to_tensor(data, placedevices[1])需要注意run返回的列表中包含了 50 次无依赖关系的乘法结果这正是用来测试框架能否自动并行调度同一列表内多个算子的素材。预热设备并分别测量单 GPU 耗时为了确保缓存显存分配器、算子库调优等不影响最终结果测量前先在两个设备上各执行一次run进行预热然后分别用d2l.Benchmark测量单设备耗时。MXNet 版本通过npx.waitall()等待设备全部排空后再进入下一段计时# MXNet run(x_gpu1) # 预热设备 run(x_gpu2) npx.waitall() with d2l.Benchmark(GPU1 时间): run(x_gpu1) npx.waitall() with d2l.Benchmark(GPU2 时间): run(x_gpu2) npx.waitall()PyTorch 版本中torch.cuda.synchronize()等待一个 CUDA 设备上所有流中的所有核完成计算它接受device参数指定需要同步的设备若为None默认值则使用current_device()找出的当前设备# PyTorch run(x_gpu1) run(x_gpu2) # 预热设备 torch.cuda.synchronize(devices[0]) torch.cuda.synchronize(devices[1]) with d2l.Benchmark(GPU1 time): run(x_gpu1) torch.cuda.synchronize(devices[0]) with d2l.Benchmark(GPU2 time): run(x_gpu2) torch.cuda.synchronize(devices[1])飞桨版本的同步原语为paddle.device.cuda.synchronize()语义与 PyTorch 一致# 飞桨 run(x_gpu1, 0) run(x_gpu2, 1) # 预热设备 paddle.device.cuda.synchronize(devices[0]) paddle.device.cuda.synchronize(devices[1]) with d2l.Benchmark(GPU1 time): run(x_gpu1, 0) paddle.device.cuda.synchronize(devices[0]) with d2l.Benchmark(GPU2 time): run(x_gpu2, 1) paddle.device.cuda.synchronize(devices[1])移除同步让系统自动并行如果删除两个任务之间的同步语句MXNet 的waitall、PyTorch/飞桨的synchronize系统就获得了在两个设备上自动并行计算的自由。此时把两个任务放进同一个计时块# MXNet with d2l.Benchmark(GPU1 GPU2): run(x_gpu1) run(x_gpu2) npx.waitall()# PyTorch with d2l.Benchmark(GPU1 GPU2): run(x_gpu1) run(x_gpu2) torch.cuda.synchronize()# 飞桨 with d2l.Benchmark(GPU1 GPU2): run(x_gpu1, 0) run(x_gpu2, 1) paddle.device.cuda.synchronize()实验结果是总执行时间小于两部分执行时间的总和。原因在于后端通过计算图发现run(x_gpu1)与run(x_gpu2)之间没有数据依赖于是把两块 GPU 上的算子调度到各自的执行流中并发运行无需用户编写任何多线程或多流代码。这正是自动并行与手写并行程序的核心区别——框架替用户完成了调度决策。并行计算与通信模拟分布式优化中的梯度聚合在许多场景下我们需要在不同设备之间移动数据CPU 与 GPU 之间、不同 GPU 之间。例如分布式优化中需要把多个加速卡上的梯度聚合到一处。本节通过在 GPU 上计算、再把结果复制回 CPU来模拟这一过程。首先定义copy_to_cpu并分别测量在 GPU1 上运行与复制到 CPU两个阶段# MXNet def copy_to_cpu(x): return [y.copyto(npx.cpu()) for y in x] with d2l.Benchmark(在GPU1上运行): y run(x_gpu1) npx.waitall() with d2l.Benchmark(复制到CPU): y_cpu copy_to_cpu(y) npx.waitall()# PyTorch def copy_to_cpu(x, non_blockingFalse): return [y.to(cpu, non_blockingnon_blocking) for y in x] with d2l.Benchmark(在GPU1上运行): y run(x_gpu1) torch.cuda.synchronize() with d2l.Benchmark(复制到CPU): y_cpu copy_to_cpu(y) torch.cuda.synchronize()# 飞桨 def copy_to_cpu(x): return [paddle.to_tensor(y, placepaddle.CPUPlace()) for y in x] with d2l.Benchmark(在GPU1上运行): y run(x_gpu1, 0) paddle.device.cuda.synchronize() with d2l.Benchmark(复制到CPU): y_cpu copy_to_cpu(y) paddle.device.cuda.synchronize()让计算与通信重叠消除不必要的同步上述串行方案是低效的当列表中的其余元素还在计算时y的早期元素已经可以复制到 CPU。真实训练中同样存在这种情况——计算一个小批量的反传梯度时某些参数的梯度比其他参数更早可用。因此在 GPU 仍在运行时就启动 PCI-Express 总线带宽来搬运数据是有利的。在 MXNet 中删除两个阶段之间的waitall即可让copyto排队在计算流之后、随算随拷模拟边计算边通信的场景# MXNet with d2l.Benchmark(在GPU1上运行并复制到CPU): y run(x_gpu1) y_cpu copy_to_cpu(y) npx.waitall()在 PyTorch 中to()与copy_()等函数允许显式的non_blocking参数让调用方在无需同步时绕过同步。设置non_blockingTrue即可模拟该场景# PyTorch with d2l.Benchmark(在GPU1上运行并复制到CPU): y run(x_gpu1) y_cpu copy_to_cpu(y, True) torch.cuda.synchronize()飞桨版本同样合并为一个计时块# 飞桨 with d2l.Benchmark(在GPU1上运行并复制到CPU): y run(x_gpu1) y_cpu copy_to_cpu(y) paddle.device.cuda.synchronize()为什么总时间小于各部分之和合并后两个操作所需的总时间如预期少于各部分时间之和。需要注意这个任务与前述并行计算本质不同它占用的是另一类资源——CPU 与 GPU 之间的总线。事实上我们可以在两个设备上同时进行计算和通信即计算与通信并行。这里存在一个依赖关系y[i]必须先被计算出来才能被复制到 CPU。幸运的是系统可以在计算y[i]的同时复制y[i-1]——流水线式地让计算与搬运重叠从而降低总运行时间。这正是多 GPU 数据并行训练中梯度异步聚合的原理基础。计算图与依赖关系为什么手动调度不可行文章最后给出了一个两层多层感知机在 CPU 与两块 GPU 上训练时的计算图及其依赖关系示意见开头的 img/twogpu.svg。图中节点包括CPU 上的输入与部分算子GPU1、GPU2 上各自的前向与反向计算设备间梯度/参数的复制与同步节点。这类并行程序的调度依赖关系错综复杂手动编写调度代码相当痛苦。这正是基于图的计算后端的优势所在框架把算子依赖构建成有向无环图DAG由调度器自动决定哪些节点可以并发、哪些必须串行等待从而在不牺牲正确性的前提下最大化设备与总线利用率。仓库 chapter_computational-performance/multiple-gpus.md 与 chapter_computational-performance/parameterserver.md 分别从数据并行与参数服务器的角度展示了同一套调度思想在生产级训练中的应用。小结现代系统拥有多种设备多个 GPU、多个 CPU可以并行且异步地使用它们现代系统还拥有多种通信资源PCI Express 总线、存储通常是固态硬盘或网络存储以及网络带宽为了达到最高效率可以并行使用框架后端通过计算图可以自动实现并行计算与并行通信从而提升整体性能实现要点在无依赖的任务之间移除不必要的同步原语waitall/cuda.synchronize并利用non_blocking或异步拷贝让计算与通信重叠即可获得接近理论峰值的设备利用率。练习与验证思路本节定义的run函数中执行了多次50 次无依赖的矩阵乘法设计实验观察框架是否会自动并行执行它们例如比较列表推导串行实现与run的耗时当单个算子工作量足够小时即使在单 CPU 或单 GPU 上并行化也有帮助设计实验验证这一点对比小尺寸矩阵乘法在同步与异步下的耗时设计一个实验在 CPU 与 GPU 两种设备上同时使用并行计算与通信例如 GPU 计算的同时向 CPU 拷贝前一批结果使用 NVIDIA Nsight 等调试器查看内核时间线与流调度验证代码是否真正高效设计带有更复杂数据依赖的计算任务验证在提高性能的同时能否获得与串行一致的正确结果。赞分享人工智能深度学习机器学习教程【免费下载链接】d2l-zh《动手学深度学习》面向中文读者、能运行、可讨论。中英文版被70多个国家的500多所大学用于教学。项目地址https://gitcode.com/GitHub_Trending/d2/d2l-zh点击查看免费下载相关推荐深度学习中自动并行计算的技术解析深度学习中自动并行计算的技术解析 引言为什么需要自动并行计算 在深度学习模型训练过程中我们经常面临计算资源瓶颈的挑战。现代神经网络模型参数数量动辄达到数十人工智能深度学习机器学习教程D2L项目解析深度学习中的自动并行计算技术D2L项目解析深度学习中的自动并行计算技术 引言 在现代深度学习实践中高效利用计算资源是提升模型训练和推理速度的关键。本文将深入探讨深度学习框架中的自动并行文档教程人工智能深度学习NLP计算机视觉强化学习Lightdash 透视Pivoting两阶段机制从 SQL 索引列到 PivotData 渲染的完整管道Lightdash 透视Pivoting两阶段机制从 SQL 索引列到 PivotData 渲染的完整管道 本文基于 Lightdash 仓库的 docs人工智能深度学习机器学习教程上一篇3步解决MacBook电池损耗Charge Limiter充电限制工具终极指南下一篇Argo Workflows 中 ClusterTrustBundleProjection 详解Java SDK 字段语义与投影卷实战创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表