Run this notebook online:\ |Binder| or Colab: |Colab| .. |Binder| image:: https://mybinder.org/badge_logo.svg :target: https://mybinder.org/v2/gh/deepjavalibrary/d2l-java/master?filepath=chapter_computational-performance/auto-parallelism.ipynb .. |Colab| image:: https://colab.research.google.com/assets/colab-badge.svg :target: https://colab.research.google.com/github/deepjavalibrary/d2l-java/blob/colab/chapter_computational-performance/auto-parallelism.ipynb .. _sec_auto_para: 自动并行 ======== MXNet在后端自动构造计算图。使用计算图,系统知道所有的依赖关系,并且可以选择性地并行执行多个不相互依赖的任务提高速度。例如:numref:\ ``fig_asyncgraph``\ in:numref:\ ``sec_async``\ 独立初始化两个变量。因此,系统可以选择并行执行它们。 通常,单个操作员将使用所有CPU或单个GPU上的所有计算资源。例如,\ ``dot``\ 操作符将使用所有CPU上的所有核心(和线程),即使在一台机器上有多个CPU处理器。这同样适用于单个GPU。因此,并行化并不是很有用的单设备计算机。对于多个设备,事情更重要。虽然并行化通常在多个gpu之间最相关,但是添加本地CPU将略微提高性能。参见例如:引用:\`哈吉斯.张.米利亚卡.ea.2016年的一篇论文,重点是训练结合了GPU和CPU的计算机视觉模型。利用自动并行化框架的便利性,我们可以用几行Python代码来实现相同的目标。更广泛地说,我们对自动并行计算的讨论集中在使用cpu和gpu的并行计算以及计算和通信的并行化。 我们首先导入所需的包和模块。请注意,我们至少需要一个GPU来运行本节中的实验。 .. code:: java %load ../utils/djl-imports %load ../utils/StopWatch.java CPU与GPU的并行计算 ------------------ 让我们通过选择下面的一个数据矩阵来执行分配给cpu的两个变量的乘法运算。 .. code:: java public NDArray run(NDArray X){ for(int i=0; i < 10; i++){ X = X.dot(X); } return X; } NDManager manager = NDManager.newBaseManager(); NDArray x_cpu = manager.randomUniform(0f, 1f, new Shape(2000, 2000), DataType.FLOAT32, Device.cpu()); NDArray x_gpu = manager.randomUniform(0f, 1f, new Shape(6000, 6000), DataType.FLOAT32, Device.gpu()); 现在我们将函数应用于数据。为了确保缓存不会在结果中起作用,我们在测量之前对每个设备执行一次循环来预热设备。 .. code:: java // 设备初始化预热 run(x_cpu); run(x_gpu); // 计算CPU计算时间 StopWatch stopWatch0 = new StopWatch(); stopWatch0.start(); run(x_cpu); stopWatch0.stop(); ArrayList times = stopWatch0.getTimes(); System.out.println("CPU time: " + times.get(times.size() - 1) + " nanoseconds "); // 计算GPU计算时间 StopWatch stopWatch1 = new StopWatch(); stopWatch1.start(); run(x_gpu); stopWatch1.stop(); times = stopWatch1.getTimes(); System.out.println("GPU time: " + times.get(times.size() - 1) + " nanoseconds "); .. parsed-literal:: :class: output CPU time: 0.03755235 nanoseconds GPU time: 0.039931347 nanoseconds .. code:: java // 计算CPU和GPU的组合计算时间 StopWatch stopWatch = new StopWatch(); stopWatch.start(); run(x_cpu); run(x_gpu); stopWatch.stop(); times = stopWatch.getTimes(); System.out.println("CPU & GPU: " + times.get(times.size() - 1) + " nanoseconds "); .. parsed-literal:: :class: output CPU & GPU: 0.066029815 nanoseconds 在上述情况下,总执行时间小于各部分的总和,因为MXNet自动地在CPU和GPU设备上调度计算,而不需要用户编写复杂的代码。 并行计算与通信 -------------- 在许多情况下,我们需要在不同的设备之间移动数据,比如在CPU和GPU之间,或者在不同的GPU之间。例如,当我们要执行分布式优化时,我们需要在多个加速卡上聚合渐变。让我们通过在GPU上计算然后将结果复制回CPU来模拟这一点。 .. code:: java public NDArray copyToCPU(NDArray X){ Y = X.toDevice(Device.cpu(), true); return Y; } // 计算GPU计算时间 StopWatch stopWatch = new StopWatch(); stopWatch.start(); NDArray Y = run(x_gpu); stopWatch.stop(); times = stopWatch.getTimes(); System.out.println("Run on GPU: " + times.get(times.size() - 1) + " nanoseconds "); // 计算复制到CPU的时间 StopWatch stopWatch1 = new StopWatch(); stopWatch1.start(); NDArray y_cpu = copyToCPU(Y); stopWatch1.stop(); times = stopWatch1.getTimes(); System.out.println("Copy to CPU: " + times.get(times.size() - 1) + " nanoseconds "); .. parsed-literal:: :class: output Run on GPU: 0.0617593 nanoseconds Copy to CPU: 0.034062174 nanoseconds 这有点低效。注意,当列表的其余部分仍在计算时,我们可以开始将“Y”的一部分复制到CPU。例如,当我们计算一个小批量的(backprop)梯度时,就会出现这种情况。一些参数的梯度将比其他参数更早可用。因此,当GPU仍在运行时,开始使用PCI-Express总线带宽对我们是有利的。 .. code:: java // 计算组合GPU计算和复制到CPU时间。 StopWatch stopWatch = new StopWatch(); stopWatch.start(); NDArray Y = run(x_gpu); NDArray y_cpu = copyToCPU(Y); stopWatch.stop(); times = stopWatch.getTimes(); System.out.println("Run on GPU and copy to CPU: " + times.get(times.size() - 1) + " nanoseconds "); .. parsed-literal:: :class: output Run on GPU and copy to CPU: 0.042300837 nanoseconds 两种操作所需的总时间(如预期)大大少于它们各部分的总和。注意,这个任务不同于并行计算,因为它使用不同的资源:CPU和GPU之间的总线。事实上,我们可以同时在两个设备上进行计算和通信。如上所述,在计算和通信之间有一个依赖关系:必须先计算Y[i],然后才能将其复制到CPU。幸运的是,系统可以在计算Y[i]的同时复制Y[i-1],以减少总的运行时间。 最后,我们给出了一个简单的两层MLP在一个CPU和两个GPU上训练时的计算图及其依赖关系,如:numref:\ ``fig_twogpu``\ 。手动调度由此产生的并行程序将非常痛苦。这是一个有利于优化的基于图形的计算后端的地方。 |CPU和2个GPU上的两层MLP| .. _fig_twogpu: 总结 ---- - 现代系统有各种各样的设备,比如多个gpu和cpu。它们可以并行、异步地使用。 - 现代系统也有各种各样的通信资源,如PCI-Express、存储器(通常是SSD或通过网络)和网络带宽。它们可以并行使用,以达到最高效率。 - 后端通过自动并行计算和通信来提高性能。 练习 ---- 1. 在本节定义的“run”函数中执行了10个操作。它们之间没有依赖关系。设计一个实验,看看MXNet是否会自动并行执行它们。 2. 当单个操作员的工作负载足够小时,即使在单个CPU或GPU上,并行化也有帮助。设计一个实验来验证这一点。 3. 设计了一个在CPU、GPU和两个设备之间进行并行计算的实验。 4. 使用调试器(如NVIDIA的Nsight)验证代码是否有效。 5. 设计包含更复杂的数据依赖关系的计算任务,并运行实验,看看是否可以在提高性能的同时获得正确的结果。 .. |CPU和2个GPU上的两层MLP| image:: https://raw.githubusercontent.com/d2l-ai/d2l-en/8884afa3d1d3f6acd40fcc3aea0a3cf288461989/img/twogpu.svg