diff --git a/chapter_backend_and_runtime/compute_schedule_and_execute.md b/chapter_backend_and_runtime/compute_schedule_and_execute.md index 3d191bd..9bdb12b 100644 --- a/chapter_backend_and_runtime/compute_schedule_and_execute.md +++ b/chapter_backend_and_runtime/compute_schedule_and_execute.md @@ -4,7 +4,7 @@ ### 单算子调度 -单算子调度是相对于计算图而言,算法或者模型中包含的算子通过Python语言的运行时被逐个调度执行。例如PyTorch的默认执行方式,TensorFlow的eager模式,以及MindSpore的PyNative模式。以如下MindSpore示例代码所示: +单算子调度是相对于计算图而言,算法或者模型中包含的算子通过Python语言的运行时被逐个调度执行。例如PyTorch的默认执行方式,TensorFlow的eager模式,以及MindSpore的PyNative模式。以MindSpore为例,如代码所示。 ```python import mindspore.nn as nn @@ -29,7 +29,7 @@ print(c) 上述脚本将所有的计算逻辑定义在Computation类的construct方法中,由于在脚本开头的context中预先设置了单算子执行模式,construct中的计算将被Python的运行时逐行调用执行,同时可以在代码中的任意位置添加print命令以便打印中间的计算结果。 -单算子执行的调用链路如 :numref:`single_op_exec`所示,算子在Python侧被触发执行后,会经过AI框架初始化,其中需要确定包括算子的精度,输入与输出的类型和大小以及对应的硬件设备等信息,接着框架会为该算子分配计算所需的内存,最后交给具体的硬件计算设备完成计算的执行。 +单算子执行的调用链路如 :numref:`single_op_exec`所示,算子在Python侧被触发执行后,会经过机器学习框架初始化,其中需要确定包括算子的精度,输入与输出的类型和大小以及对应的硬件设备等信息,接着框架会为该算子分配计算所需的内存,最后交给具体的硬件计算设备完成计算的执行。 ![单算子执行](../img/ch05/single_op_exec.PNG) :width:`800px` @@ -65,7 +65,7 @@ print(c) 异构计算图能够被正确表达的首要条件是准确标识算子执行所在的设备,例如异构计算图 :numref:`computation_graph`中所标识的CPU、GPU和Ascend Kernel,以及被标记为被Python语言运行时执行的Python -Kernel。主流框架均提供了指定算子所在运行设备的能力,以MindSpore为例,一段简单的异构计算代码如下所示: +Kernel。主流框架均提供了指定算子所在运行设备的能力,以MindSpore为例,一段简单的异构计算代码如下所示。 ```python import numpy as np @@ -92,13 +92,12 @@ z = Tensor(np.ones([2, 2]).astype(np.float32)) output = compute(x, y, z) ``` -上述代码片段完成了x + y - -z的计算逻辑,其中Add算子被设置为在CPU上执行,Sub算子被设置为在GPU上执行,从而形成了CPU与GPU协同的异构计算,通过类似的标签机制,可以实现任意复杂的多硬件协同的异构计算表达。 -另外一类较为特殊的异构是Python算子,Python语言的优势在于表达的灵活性和开发效率,以及丰富的周边生态,因此将Python算子引入到计算图中和其它异构硬件的算子协同计算,对计算的灵活性会产生非常大的帮助。与CPU、GPU分别执行在不同设备上的异构不同,Python算子和C++实现的CPU算子都是通过主机侧的CPU核执行,差异在于Python算子是通过统一的计算图进行描述,因此也需要在计算图的执行引擎中被触发执行。为了在计算图中能够表达Python算子,框架需要提供相应的支持。 +上述代码片段完成了x + y - z的计算逻辑,其中Add算子被设置为在CPU上执行,Sub算子被设置为在GPU上执行,从而形成了CPU与GPU协同的异构计算,通过类似的标签机制,可以实现任意复杂的多硬件协同的异构计算表达。 +另外一类较为特殊的异构是Python算子,Python语言的优势在于表达的灵活性和开发效率,以及丰富的周边生态,因此将Python算子引入到计算图中和其他异构硬件的算子协同计算,对计算的灵活性会产生非常大的帮助。与CPU、GPU分别执行在不同设备上的异构不同,Python算子和C++实现的CPU算子都是通过主机侧的CPU核执行,差异在于Python算子是通过统一的计算图进行描述,因此也需要在后端运行时中触发执行。为了在计算图中能够表达Python算子,框架需要提供相应的支持。 -完成计算图中算子对应设备的标记以后,计算图已经准备好被调度与执行,根据硬件能力的差异,可以将异构计算图的执行分为三种模式,分别是逐算子交互式执行,整图下沉执行与子图下沉执行。交互式执行主要针对CPU和GPU的场景,计算图中的算子按照输入和输出的依赖关系被逐个调度与执行;而整图下沉执行模式主要是针对NPU芯片而言,这类芯片主要的优势是能够将整个神经网络的计算图一次性下发到设备上,无需借助主机的CPU能力而独立完成计算图中所有算子的调度与执行,减少了主机和芯片的交互次数,借助NPU的Tensor加速能力,提高了计算效率和性能;子图下沉执行模式是前面两种执行模式的结合,由于计算图自身表达的灵活性,对于复杂场景的计算图在NPU芯片上进行整图下沉执行的效率不一定能达到最优,因此可以将对于NPU芯片执行效率低下的部分分离出来,交给CPU或者GPU等执行效率更高的设备处理,而将部分更适合NPU计算的子图下沉到NPU进行计算,这样可以兼顾性能和灵活性两方面。 +完成计算图中算子对应设备的标记以后,计算图已经准备好被调度与执行,根据硬件能力的差异,可以将异构计算图的执行分为三种模式,分别是逐算子交互式执行,整图下沉执行与子图下沉执行。交互式执行主要针对CPU和GPU的场景,计算图中的算子按照输入和输出的依赖关系被逐个调度与执行;而整图下沉执行模式主要是针对NPU芯片而言,这类芯片主要的优势是能够将整个神经网络的计算图一次性下发到设备上,无需借助主机的CPU能力而独立完成计算图中所有算子的调度与执行,减少了主机和芯片的交互次数,借助NPU的张量加速能力,提高了计算效率和性能;子图下沉执行模式是前面两种执行模式的结合,由于计算图自身表达的灵活性,对于复杂场景的计算图在NPU芯片上进行整图下沉执行的效率不一定能达到最优,因此可以将对于NPU芯片执行效率低下的部分分离出来,交给CPU或者GPU等执行效率更高的设备处理,而将部分更适合NPU计算的子图下沉到NPU进行计算,这样可以兼顾性能和灵活性两方面。 -上述异构计算图可以实现两个目的,一个是异构硬件加速,将特定的计算放置到合适的硬件上执行;第二个是实现算子间的并发执行,从计算图上可以看出,Kernel_1和Kernel_2之间没有依赖关系,Kernel_3和Kernel_4之间也没有依赖关系,因此这两组CPU和GPU算子在逻辑上可以被框架并发调用,而Kernel_5依赖Kernel_3和Kernel_4的输出作为输入,因此Kernel_5需要等待Kernel_3和Kernel_4执行完成后再被触发执行。 +上述异构计算图可以实现两个目的,一个是异构硬件加速,将特定的计算放置到合适的硬件上执行;第二个是实现算子间的并发执行,从计算图上可以看出,kernel_1和kernel_2之间没有依赖关系,kernel_3和kernel_4之间也没有依赖关系,因此这两组CPU和GPU算子在逻辑上可以被框架并发调用,而kernel_5依赖kernel_3和kernel_4的输出作为输入,因此kernel_5需要等待kernel_3和kernel_4执行完成后再被触发执行。 虽然在计算图上可以充分表达算子间的并发关系,在实际代码中会产生由于并发而引起的一些不预期的副作用场景,例如如下代码所示: @@ -132,13 +131,13 @@ compute(y, z) x = x - y ``` -这段简单的计算逻辑翻译到计算图上可以表示为: +这段简单的计算逻辑翻译到计算图上可以表示为 :numref:`side_effect_1`所示。 ![并发算子执行](../img/ch05/side_effect_1.png) :width:`800px` :label:`side_effect_1` -代码中所示三行计算之间并没有依赖关系,因此这三个算子在计算图的逻辑上可以被并发执行,并发关系如 :numref:`side_effect_1`所示,然而根据代码的语义,显而易见是需要确保程序能够被顺序执行,这里引入的问题被称为副作用,副作用是指函数修改了在函数外部定义的状态变量的行为。由于副作用的引入而导致了错误并发关系的发生,一种解决方案是在计算图编译阶段通过添加算子间的依赖,将并发执行逻辑转换为顺序执行逻辑,转换后的计算图如 :numref:`side_effect_2`所示: +代码中所示三行计算之间并没有依赖关系,因此这三个算子在计算图的逻辑上可以被并发执行,然而根据代码的语义,显而易见是需要确保程序能够被顺序执行,这里引入的问题被称为副作用,副作用是指修改了在函数外部定义的状态变量的行为。由于副作用的引入而导致了错误并发关系的发生,一种解决方案是在计算图编译阶段通过添加算子间的依赖,将并发执行逻辑转换为顺序执行逻辑,转换后的计算图如 :numref:`side_effect_2`所示。 ![消除副作用](../img/ch05/side_effect_2.png) :width:`800px` diff --git a/chapter_backend_and_runtime/graph_optimizer.md b/chapter_backend_and_runtime/graph_optimizer.md index 1d8bb27..68475f7 100644 --- a/chapter_backend_and_runtime/graph_optimizer.md +++ b/chapter_backend_and_runtime/graph_optimizer.md @@ -4,25 +4,20 @@ ### 通用硬件优化 -通用硬件优化主要指与特定硬件类型无关系的计算图优化,优化的核心是子图的等价变换:在计算图中尝试匹配特定的子图结构,找到目标子图结构后,通过等价替换方式,将其替换成对硬件更友好的子图结构。 +通用硬件优化主要指与特定硬件类型无关的计算图优化,优化的核心是子图的等价变换:在计算图中尝试匹配特定的子图结构,找到目标子图结构后,通过等价替换方式,将其替换成对硬件更友好的子图结构。 以优化内存IO为例。深度学习算子按其对资源的需求可以分为两类: 计算密集型算子,这些算子的时间绝大部分花在计算上,如卷积、全连接等; -访存密集型算子,这些算子的时间绝大部分花在访存上,他们大部分是Element-Wise算子,例如 -ReLU、Element-Wise Sum等。 -在典型的深度学习模型中,一般计算密集型和访存密集型算子是相伴出现的,最简单的例子是"Conv + -ReLU"。Conv卷积算子是计算密集型,ReLU算子是访存密集型算子,ReLU算子可以直接取Conv算子的计算结果进行计算,因此我们可以将二者融合成一个算子来进行计算,从而减少内存访问延时和带宽压力,提高执行效率。 +访存密集型算子,这些算子的时间绝大部分花在访存上,他们大部分是Element-Wise算子,例如 ReLU、Element-Wise Sum等。 +在典型的深度学习模型中,一般计算密集型和访存密集型算子是相伴出现的,最简单的例子是“Conv + ReLU”。Conv卷积算子是计算密集型,ReLU算子是访存密集型算子,ReLU算子可以直接取Conv算子的计算结果进行计算,因此可以将二者融合成一个算子来进行计算,从而减少内存访问延时和带宽压力,提高执行效率。 -例如:"Conv + Conv + Sum + -ReLU"的融合,从 :numref:`conv_sum_relu`中我们可以看到融合后的算子减少了两个内存的读和写的操作,优化了Conv的输出和Sum的输出的读和写的操作。 +例如:“Conv + Conv + Sum + ReLU”的融合,从图\ref{fig:ch07/ch07-compiler-backend-03}中可以看到融合后的算子减少了两个内存的读和写的操作,优化了Conv的输出和Sum的输出的读和写的操作。 ![Elementwise算子融合](../img/ch05/conv_sum_relu.png) :width:`800px` :label:`conv_sum_relu` -除了上述针对特定算子类型结构的融合优化外,基于自动算子生成技术,还可以实现更灵活、更极致的通用优化。以 -MindSpore -的图算融合技术为例,图算融合通过"算子拆解、算子聚合、算子重建"三个主要阶段(如图)让计算图中的计算更密集,并进一步减少低效的内存访问。 +除了上述针对特定算子类型结构的融合优化外,基于自动算子生成技术,还可以实现更灵活、更极致的通用优化。以 MindSpore 的图算融合技术为例,图算融合通过“算子拆解、算子聚合、算子重建”三个主要阶段让计算图中的计算更密集,并进一步减少低效的内存访问。 ![图算融合](../img/ch05/graph_kernel.png) :width:`800px` diff --git a/chapter_backend_and_runtime/index.md b/chapter_backend_and_runtime/index.md index 5d166f9..e52d5a8 100644 --- a/chapter_backend_and_runtime/index.md +++ b/chapter_backend_and_runtime/index.md @@ -1,9 +1,8 @@ # 编译器后端和运行时 -在上一章节,我们详细讲述了一个编译器前端的主要功能,重点介绍了中间表示以及自动微分。在得到中间表示后,如何充分利用硬件资源高效地执行,是编译器后端和运行时要解决的问题。 +在上一章节,详细讲述了一个AI编译器前端的主要功能,重点介绍了中间表示以及自动微分。在得到中间表示后,如何充分利用硬件资源高效地执行,是编译器后端和运行时要解决的问题。 -在本章节中, -我们将会介绍编译器后端的一些基本概念,详细描述后端的计算图优化、算子选择等流程。通过对编译器前端提供的中间表示进行优化,充分发挥硬件能力,从而提高程序的执行效率。在此基础上,介绍运行时是如何对计算任务进行内存分配以及高效的调度执行。 +在本章节中, 将会介绍AI编译器后端的一些基本概念,详细描述后端的计算图优化、算子选择等流程。通过对编译器前端提供的中间表示进行优化,充分发挥硬件能力,从而提高程序的执行效率。在此基础上,介绍运行时是如何对计算任务进行内存分配以及高效地调度执行。 本章的学习目标包括: @@ -17,6 +16,8 @@ - 掌握计算图调度和执行的常用方法 +- 了解目前算子编译器的基本特点以及其尚未收敛的几个问题 + ```toc :maxdepth: 2 @@ -25,5 +26,6 @@ graph_optimizer kernel_selecter memory_allocator compute_schedule_and_execute +op_compiler summary ``` \ No newline at end of file diff --git a/chapter_backend_and_runtime/kernel_selecter.md b/chapter_backend_and_runtime/kernel_selecter.md index 57a1d0e..869f607 100644 --- a/chapter_backend_and_runtime/kernel_selecter.md +++ b/chapter_backend_and_runtime/kernel_selecter.md @@ -1,10 +1,10 @@ ## 算子选择 -经过计算图优化后,需要对IR图上的每个节点进行算子选择,才能生成真正在设备上执行的算子序列。由于IR图上的节点可能有后端的很多算子与其对应,不同规格的算子在不同的情况下执行效率各不相同,在算子选择阶段的主要任务就是如何根据IR图中的信息在众多算子中选择出最合适的一个算子去目标设备上执行。 +过计算图优化后,需要对IR图上的每个节点进行算子选择,才能生成真正在设备上执行的算子序列。由于IR图上的节点可能有后端的很多算子与其对应,不同规格的算子在不同的情况下执行效率各不相同,在算子选择阶段的主要任务就是如何根据IR图中的信息在众多算子中选择出最合适的一个算子去目标设备上执行。 ### 算子选择的基础概念 -经历了后端的图优化后,IR图中的每一个节点都有一组算子与之对应。此时的IR图中的每一个节点可以认为是用户可见的最小硬件执行单元。但是此时IR图中的一个节点代表了用户代码的一个操作,对于这个操作还没有具体生成有关设备信息的细节描述。这些信息是算子选择所选择的内容信息,我们称之为算子信息。算子信息主要包括以下内容: +经历了后端的图优化后,IR图中的每一个节点都有一组算子与之对应。此时的IR图中的每一个节点可以认为是用户可见的最小硬件执行单元,代表了用户代码的一个操作,对于这个操作还没有具体生成有关设备信息的细节描述。这些信息是算子选择所选择的内容信息,称之为算子信息。算子信息主要包括以下内容: 1. 针对不同特点的计算平台和不同的算子,为了追求最好的性能,一般都需要选择不同的数据排布格式。机器学习系统常见的数据排布格式有NCHW和NHWC等。 @@ -19,17 +19,17 @@ :width:`800px` :label:`matmuldatalayout` -在机器学习系统中我们常见的数据格式一般有两种,分别为NCHW类型和NHWC类型。其中N代表了数据输入的BatchSize大小,C代表了图像的通道,H和W分别代表图像输入的长和宽。 :numref:`data_format`展示了BatchSize为2,通道数16和长度为5\*4大小的数据逻辑示意图。 +在机器学习系统中常见的数据格式一般有两种,分别为NCHW类型和NHWC类型。其中N代表了数据输入的批大小,C代表了图像的通道,H和W分别代表图像输入的高和宽。:numref:`data_format`展示了BatchSize为2,通道数16和大小为5\*4的数据逻辑示意图。 ![常见数据格式](../img/ch05/data_format.png) :width:`800px` :label:`data_format` -但是计算机的存储并不能够直接将这样的矩阵放到内存中,需要将其展平成1维后存储,这样就涉及我们逻辑上的索引如何映射成为内存中的索引,即我们如何根据逻辑数据索引来映射到内存中的1维数据索引。 +但是计算机的存储并不能够直接将这样的矩阵放到内存中,需要将其展平成1维后存储,这样就涉及逻辑上的索引如何映射成为内存中的索引,即如何根据逻辑数据索引来映射到内存中的1维数据索引。 对于NCHW的数据是先取W轴方向数据,再取H轴方向数据,再取C轴方向,最后取N轴方向。其中物理存储与逻辑存储的之间的映射关系为 $$offsetnchw(n,c,h,w) = n*CHW + c*HW + h*W +w$$ -如 :numref:`nchw`所示,这种格式中,是按照最低维度W轴方向进行展开,W轴相邻的元素在内存排布中同样是相邻的。如果需要取下一个图片上的相同位置的元素,就必须跳过整个图像的尺寸($C*H*W$)。比如我有8张32\*32的RGB图像,此时$N=8,C=3,H=32,W=32$。在内存中存储它们需要先按照W轴方向进行展开,然后按照H轴排列,这样之后便完成了一个通道的处理,之后按照同样的方式处理下一个通道。处理完全部通道后,处理下一张图片。PyTorch和MindSpore框架默认使用NCHW格式。 +如 :numref:`nchw`所示,这种格式中,是按照最低维度W轴方向进行展开,W轴相邻的元素在内存排布中同样是相邻的。如果需要取下一个图片上的相同位置的元素,就必须跳过整个图像的尺寸($C*H*W$)。比如有8张32\*32的RGB图像,此时$N=8,C=3,H=32,W=32$。在内存中存储它们需要先按照W轴方向进行展开,然后按照H轴排列,这样之后便完成了一个通道的处理,之后按照同样的方式处理下一个通道。处理完全部通道后,处理下一张图片。PyTorch和MindSpore框架默认使用NCHW格式。 ![RGB图片下的NHWC数据格式](../img/ch05/nchw.png) :width:`800px` @@ -49,38 +49,35 @@ $$offsetnhwc(n,h,w,c) = n*HWC + h*WC + w*C +c$$ **数据精度** -通常深度学习的系统,一般使用的是单精度float(Single -Precision)浮点表示。这种数据类型占用32位内存。还有一种精度较低的数据类型float16,其内部占用了16位的内存。由于很多硬件会对float16数据类型进行优化,float16半精度的计算吞吐量可以是float32的$2\sim 8$倍,且float16可以占用的数据更小,这样可以输入更大的BatchSize,进而减少总体训练时间。接下来我们详细看一下半精度浮点数与单精度浮点数的区别。 +通常深度学习的系统,使用的是单精度(float32)表示。这种数据类型占用32位内存。还有一种精度较低的数据类型为半精度(float16),其内部占用了16位的内存。由于很多硬件会对半精度数据类型进行优化,半精度的计算吞吐量可以是单精度的$2\sim 8$倍,且半精度占用的内存更小,这样可以输入更大的批大小(BatchSize),进而减少总体训练时间。接下来详细看一下半精度浮点数与精度浮点数的区别。 ![浮点数的二进制表示](../img/ch05/floatdtype.png) :width:`800px` :label:`floatdtype` -如 :numref:`floatdtype`其中sign代表符号位,占1位,表示了机器数的正负,exponent表示指数位,Mantissa为尾数位。其中float16类型的数据采用二进制的科学计数法转换为十进制的计算方式如下: -$$(-1)^{sign}\times 2^{exponent-15}\times (\frac{mantissa}{1024}+1)$$ +如 :numref:`floatdtype`中Sig代表符号位,占1位,表示了机器数的正负,Exponent表示指数位,Mantissa为尾数位。其中float16类型的数据采用二进制的科学计数法转换为十进制的计算方式如式$$(-1)^{sign}\times 2^{exponent-15}\times (\frac{mantissa}{1024}+1)$$所示。 其中如果指数位全为0时,且尾数位全为0时表示数字0。 如果指数位全为0,尾数位不全为0则表示一个非常小的数值。 当指数全为1,尾数位全为0表示根据符号位正无穷大,或者负无穷大。 若指数全为1,但是尾数位不为0,则表示NAN。 -其中bfloat16并不属于一个通用的数据类型,是google提出的一种特殊的类型,现在一般只在一些TPU上训练使用,其指数位数与float32位数保持一致,可以较快的与float32进行数据转换。由于并不是一种通用类型。IEEE中也并没有提出该类型的标准。 +其中bfloat16并不属于一个通用的数据类型,是Google提出的一种特殊的类型,现在一般只在一些TPU上训练使用,其指数位数与float32位数保持一致,可以较快的与float32进行数据转换。由于bfloat16并不是一种通用类型,IEEE中也并没有提出该类型的标准。 **算子信息库** -前面我们讲述了数据格式和数据精度的概念,基于这两个概念,在不同硬件下会有不同的算子支持,此时需要有一个硬件上支持的所有算子的集合,该集合我们称之为算子信息库。算子选择过程就是从算子信息库中选择最合适的算子的过程。 +前面讲述了数据格式和数据精度的概念,基于这两个概念,在不同硬件下会有不同的算子支持,一个硬件上支持的所有算子的集合定义为该硬件的算子信息库。算子选择过程就是从算子信息库中选择最合适算子的过程。 ### 算子选择的过程 前文介绍了算子选择主要是针对IR图中的每一个操作节点选择出最为合适的算子。其中算子信息主要包括了支持设备类型、数据类型和数据排布格式三个方面。经过编译器前端类型推导与静态分析的阶段后,IR图中已经推导出了用户代码侧的数据类型。下面介绍算子选择的基本过程。 -首先,选择算子执行的硬件设备。不同的硬件设备上,算子的实现、支持数据类型、执行效率通常会有所差别。这一步往往是用户自己指定的,若用户未指定,则编译器后端会为用户匹配一个默认的设备。 +如图 :numref:`select_kernel`所示,展示了算子选择过程。首先,选择算子执行的硬件设备。不同的硬件设备上,算子的实现、支持数据类型、执行效率通常会有所差别。这一步往往是用户自己指定的,若用户未指定,则编译器后端会为用户匹配一个默认的设备。 然后,后端会根据IR图中推导出的数据类型和内存排布格式选择对应的算子。 ![算子选择过程](../img/ch05/select_kernel.png) :width:`800px` :label:`select_kernel` -理想情况下算子选择所选择出的算子类型,应该与用户预期的类型保持一致。但是由于软硬件的限制,很可能算子的数据类型不能满足用户所期待的数据类型,此时需要对该节点进行升精度或者降精度处理才能匹配到合适的算子。比如在MindSpore -的Ascend后端由于硬件限制导致Conv2D算子只存在float16一种数据类型。如果用户设置的整网使用的数据类型为float32数据,那么只能对Conv2D算子的输入数据进行降精度处理,即将输入数据类型从float32转换成float16。 +理想情况下算子选择所选择出的算子类型,应该与用户预期的类型保持一致。但是由于软硬件的限制,很可能算子的数据类型不能满足用户所期待的数据类型,此时需要对该节点进行升精度或者降精度处理才能匹配到合适的算子。比如在MindSpore 的Ascend后端由于硬件限制导致Conv2D算子只存在float16一种数据类型。如果用户设置的整网使用的数据类型为float32数据,那么只能对Conv2D算子的输入数据进行降精度处理,即将输入数据类型从float32转换成float16。 算子的数据排布格式转换是一个比较耗时的操作,为了避免频繁的格式转换所带来的内存搬运开销,数据应该尽可能地以同样的格式在算子之间传递,算子和算子的衔接要尽可能少的出现数据排布格式不一致的现象。另外,数据类型不同导致的降精度可能会使得误差变大,收敛速度变慢甚至不收敛,所以数据类型的选择也要结合具体算子分析。 diff --git a/chapter_backend_and_runtime/memory_allocator.md b/chapter_backend_and_runtime/memory_allocator.md index d07e8c0..4e80054 100644 --- a/chapter_backend_and_runtime/memory_allocator.md +++ b/chapter_backend_and_runtime/memory_allocator.md @@ -1,12 +1,11 @@ ## 内存分配 :label:`ch05-sec-memory_pool` -内存在传统计算机存储器层次结构中有着重要的地位,它是连接高速缓存和磁盘之间的桥梁,有着比高速缓存更大的空间,比磁盘更快的访问速度。随着深度学习的发展,深度神经网络的模型越来越复杂,AI芯片上的内存很可能无法容纳一个大型网络模型。因此,对内存进行复用是一个重要的优化手段。此外,通过连续内存分配和 -In-Place内存分配还可以提高某些算子的执行效率。 +内存在传统计算机存储器层次结构中有着重要的地位,它是连接高速缓存和磁盘之间的桥梁,有着比高速缓存更大的空间,比磁盘更快的访问速度。随着深度学习的发展,深度神经网络的模型越来越复杂,AI芯片\footnote{与前文中的硬件加速器指意相同,业内习惯称为AI芯片}上的内存很可能无法容纳一个大型网络模型。因此,对内存进行复用是一个重要的优化手段。此外,通过连续内存分配和 In-Place内存分配还可以提高某些算子的执行效率。 ### Device内存概念 -在深度学习体系结构中,我们通常将与硬件加速器(如GPU、AI芯片等)相邻的内存称之为设备(Device)内存,而与CPU相邻的内存称之为主机(Host)内存。如 :numref:`host-device-memory`所示,CPU可以合法地访问主机上的内存,而无法直接访问设备上的内存;同理,AI芯片可以访问设备上的内存,却无法访问主机上的内存。因此,在网络训练过程中,我们往往需要从磁盘加载数据到主机内存中,然后在主机内存中做数据处理,再从主机内存拷贝到设备内存中,最后设备才能合法地访问数据。算子全部计算完成后,用户要获取训练结果,又需要把数据从设备内存拷贝到主机内存中。 +在深度学习体系结构中,通常将与硬件加速器(如GPU、AI芯片等)相邻的内存称之为设备(Device)内存,而与CPU相邻的内存称之为主机(Host)内存。如:numref:`host-device-memory`所示,CPU可以合法地访问主机上的内存,而无法直接访问设备上的内存;同理,AI芯片可以访问设备上的内存,却无法访问主机上的内存。因此,在网络训练过程中,往往需要从磁盘加载数据到主机内存中,然后在主机内存中做数据处理,再从主机内存拷贝到设备内存中,最后设备才能合法地访问数据。算子全部计算完成后,用户要获取训练结果,又需要把数据从设备内存拷贝到主机内存中。 ![主机内存和设备内存](../img/ch05/host-device-memory.png) :width:`800px` @@ -14,20 +13,17 @@ In-Place内存分配还可以提高某些算子的执行效率。 ### 内存分配 {#内存分配-1} -内存分配模块主要负责给图中算子的输入、输出分配Device内存。用户的前端脚本经过编译器前端处理后得到中间表达,后端根据中间表达进行算子选择和相关优化,可以得到算子最终的输入输出Tensor的形状、数据类型(Data -Type)、格式(Format)等信息,根据这些信息我们可以计算出算子输入、输出Tensor的尺寸大小。基本的计算方法为: -$$size=\left (\prod_{i=0}^{dimension}shape_i\right ) * sizeof\left ( data type \right )$$ -得到Tensor的尺寸大小后,往往还需要对内存大小进行对齐操作。内存通常以4字节、8字节或16字节为一组进行访问,如果被搬运的内存大小不是这些值的倍数,内存后面会填充相应数量的空数据以使得内存长度达到这些值的倍数。因此,访问非对齐的内存可能会更加耗时。 +内存分配模块主要负责给图中算子的输入、输出分配Device内存。用户的前端脚本经过编译器前端处理后得到中间表达,后端根据中间表达进行算子选择和相关优化,可以得到算子最终的输入输出张量的形状、数据类型(Data Type)、格式(Format)等信息,根据这些信息可以计算出算子输入、输出张量的尺寸大小。基本的计算方法如式$$size=\left (\prod_{i=0}^{dimension}shape_i\right ) * sizeof\left ( data type \right )$$所示。得到张量的尺寸大小后,往往还需要对内存大小进行对齐操作。内存通常以4字节、8字节或16字节为一组进行访问,如果被搬运的内存大小不是这些值的倍数,内存后面会填充相应数量的空数据以使得内存长度达到这些值的倍数。因此,访问非对齐的内存可能会更加耗时。 ![内存分配示例](../img/ch05/memory_allocate.png) :width:`800px` :label:`memory_allocate` -下面以 :numref:`memory_allocate`为例介绍内存分配的大致流程。首先我们会给Input -Tensor、Conv2D的权重和Conv2D的输出分配内存地址。然后为BatchNorm的输入分配地址时,我们发现BatchNorm的输入就是Conv2D算子的输出,而该Tensor的地址已经在之前分配过了,因此只需要将Conv2D算子的输出地址共享给BatchNorm的输入,就可以避免内存的重复申请以及内存的冗余拷贝。以此类推,可以发现整个过程中可以将待分配的内存分成三种类型:一是整张图的输入Tensor,二是算子的权重或者属性,三是算子的输出Tensor,三种类型在训练过程中的生命周期有所不同。 +下面以 :numref:`memory_allocate`为例介绍内存分配的大致流程。首先给输入张量、Conv2D的权重和Conv2D的输出分配内存地址。然后为BatchNorm的输入分配地址时,发现BatchNorm的输入就是Conv2D算子的输出,而该张量的地址已经在之前分配过了,因此只需要将Conv2D算子的输出地址共享给BatchNorm的输入,就可以避免内存的重复申请以及内存的冗余拷贝。以此类推,可以发现整个过程中可以将待分配的内存分成三种类型:一是整张图的输入张量,二是算子的权重或者属性,三是算子的输出张量,三种类型在训练过程中的生命周期有所不同。 + +在CPU上常常使用malloc函数直接申请内存,这种方式申请内存好处是随时申请随时释放,简单易用。然而在许多对性能要求严苛的计算场景中,由于所申请内存块的大小不定,频繁申请释放会降低性能。通常会使用内存池的方式去管理内存,先申请一定数量的内存块留作备用,当程序有内存申请需求时,直接从内存池中的内存块中申请。当程序释放该内存块时,内存池会进行回收并用作后续程序内存申请时使用。 +在深度学习框架中,设备内存的申请也是非常频繁的,往往也是通过内存池的方式去管理设备内存,并让设备内存的生命周期与张量的生命周期保持一致。不同的深度学习框架在内存池的设计上大同小异,以:numref:`device_malloc`的MindSpore框架内存申请为例,进程会从设备上申请足够大的内存,然后通过双游标从两端偏移为张量分配内存。首先从申请的首地址开始进行偏移,为算子权重的张量分配内存,这部分张量生命周期较长,往往持续整个训练过程。然后从申请设备地址的末尾开始偏移,为算子的输出张量分配内存,这部分内存的生命周期较短,往往在该算子计算结束并且后续计算过程中无需再次使用该算子的输出的情况下,其生命周期就可以结束。通过这种方式,只需要从设备上申请一次足够大的内存,后续算子的内存分配都是通过指针偏移进行分配,减少了直接从设备申请内存的耗时。 -在CPU上我们常常使用malloc函数直接申请内存,这种方式申请内存好处是随时申请随时释放,简单易用。然而在许多对性能要求严苛的计算场景中,由于所申请内存块的大小不定,频繁申请释放会降低性能。通常我们会使用内存池的方式去管理内存,先申请一定数量的内存块留作备用,当程序有内存申请需求时,直接从内存池中的内存块中申请。当程序释放该内存块时,内存池会进行回收并用作后续程序内存申请时使用。 -在深度学习框架中,Device内存的申请也是非常频繁的,往往也是通过内存池的方式去管理Device内存,并让Device内存的生命周期与Tensor的生命周期保持一致。不同的深度学习框架在内存池的设计上大同小异,我们以 :numref:`device_malloc`的MindSpore框架内存申请为例,进程会从Device上申请足够大的内存,然后通过双游标从两端偏移为Tensor分配内存。首先从申请的首地址开始进行偏移,为算子权重的Tensor分配内存,这部分Tensor生命周期较长,往往持续整个训练过程。然后从申请Device地址的末尾开始偏移,为算子的输出Tensor分配内存,这部分内存的生命周期较短,往往在该算子计算结束并且后续计算过程中无需再次使用该算子的输出的情况下,其生命周期就可以结束。通过这种方式,我们只需要从Device上申请一次足够大的内存,后续算子的内存分配都是通过指针偏移进行分配,减少了直接从设备申请内存的耗时。 ![双游标法分配内存](../img/ch05/device_malloc.png) :width:`800px` @@ -35,18 +31,17 @@ Tensor、Conv2D的权重和Conv2D的输出分配内存地址。然后为BatchNor ### 内存复用 -在机器学习系统中,内存复用是指分析Tensor的生命周期,将生命周期结束的Tensor的Device内存释放回内存池并用于后续Tensor的内存分配。内存复用的目的是提高内存的利用率,让有限的设备内存容纳更大的模型。 -以 :numref:`memory_allocate`为例,当BatchNorm算子计算结束后,output1不再被任何算子使用,则该Tensor的Device内存可以被回收,并且如果output1的内存尺寸大于等于output3的内存尺寸,则从output1回收的地址可以用于output3的内存分配,从而达到复用output1地址的目的。 +在机器学习系统中,内存复用是指分析张量的生命周期,将生命周期结束的张量的设备内存释放回内存池并用于后续张量的内存分配。内存复用的目的是提高内存的利用率,让有限的设备内存容纳更大的模型。 +以 :numref:`memory_allocate`为例,当BatchNorm算子计算结束后,输出1不再被任何算子使用,则该张量的设备内存可以被回收,并且如果输出1的内存尺寸大于等于输出3的内存尺寸,则从输出1回收的地址可以用于输出3的内存分配,从而达到复用输出1地址的目的。 ![内存生命周期图](../img/ch05/combine_memory_reuse_and_no_reuse.png) :width:`800px` :label:`combine_memory_reuse_and_no_reuse` -为了更好地描述内存复用问题,我们通过内存生命周期图来辅助理解。如 :numref:`combine_memory_reuse_and_no_reuse`所示,图中横坐标表示Tensor的生命周期,图中纵坐标表示内存大小。在生命周期内,某一个Tensor将一直占用某块Device内存,直至生命周期结束才会释放相应内存块。通过Tensor生命周期和内存大小可以构造出矩形块,而内存分配要求解的目标是在内存生命周期图中容纳更多的矩形块,问题的约束是矩形块之间无碰撞。 :numref:`combine_memory_reuse_and_no_reuse`左边是在未使用任何内存复用策略的情况下的内存生命周期图,此时内存同时只能容纳T0、T1、T2、T3四个Tensor。 +为了更好地描述内存复用问题,通过内存生命周期图来辅助理解。如 :numref:`combine_memory_reuse_and_no_reuse`所示,图中横坐标表示张量的生命周期,图中纵坐标表示内存大小。在生命周期内,某一个张量将一直占用某块设备内存,直至生命周期结束才会释放相应内存块。通过张量生命周期和内存大小可以构造出矩形块,而内存分配要求解的目标是在内存生命周期图中容纳更多的矩形块,问题的约束是矩形块之间无碰撞。 :numref:`combine_memory_reuse_and_no_reuse`左边是在未使用任何内存复用策略的情况下的内存生命周期图,此时内存同时只能容纳T0、T1、T2、T3四个张量。 内存复用策略的求解是一个NP完全的问题。许多深度学习框架通常采用贪心的策略去分配内存,例如采用BestFit算法,每次直接从内存池中选取可以满足条件的最小内存块,然而这种贪心的策略往往会陷入局部最优解,而无法求得全局最优解。为了更好地逼近内存分配策略全局最优解,MindSpore框架提出了一种新的内存分配算法 -SOMAS(Safe Optimized Memory Allocation -Solver)。SOMAS将计算图并行流与数据依赖进行聚合分析,得到算子间祖先关系,构建张量全局生命周期互斥约束,使用多种启发式算法求解最优的内存静态规划,实现逼近理论极限的内存复用,从而提升支持的内存大小。 +SOMAS(Safe Optimized Memory Allocation Solver,安全优化的内存分配求解器)。SOMAS将计算图并行流与数据依赖进行聚合分析,得到算子间祖先关系,构建张量全局生命周期互斥约束,使用多种启发式算法求解最优的内存静态规划,实现逼近理论极限的内存复用,从而提升支持的内存大小。 由 :numref:`combine_memory_reuse_and_no_reuse`右边所示,经过SOMAS求解之后,同样的内存大小,可支持的Tensor数量达到了7个。 @@ -54,7 +49,7 @@ Solver)。SOMAS将计算图并行流与数据依赖进行聚合分析,得到 #### 内存融合 -上述内存分配的方式,都是以单个Tensor的维度去分配的,每个Tensor分配到的Device地址往往是离散的。但是对于某些特殊的算子,如AllReduce通信算子,我们需要为它们分配连续的内存。通信算子的执行包含通信等待、数据搬移、计算等步骤,而在大规模分布式集群的场景下,通信的耗时往往是性能瓶颈。针对这种场景,如 :numref:`memory_fusion`所示,我们可以将多个通信算子融合成一个,为通信算子的输入分配连续的内存,从而减少通信的次数。 +上述内存分配的方式,都是以单个张量的维度去分配的,每个张量分配到的设备地址往往是离散的。但是对于某些特殊的算子,如AllReduce通信算子,需要为它们分配连续的内存。通信算子的执行包含通信等待、数据搬移、计算等步骤,而在大规模分布式集群的场景下,通信的耗时往往是性能瓶颈。针对这种场景,如 :numref:`memory_fusion`所示,可以将多个通信算子融合成一个,为通信算子的输入分配连续的内存,从而减少通信的次数。 又比如分布式训练中的神经网络权重初始化,通常将一个训练进程中的权重初始化,然后将该权重广播到其他进程中。当一个网络有较多权重的时候,需要多次进行广播。通常可以为所有权重分配连续的内存地址,然后广播一次,节省大量通信的耗时。 ![通信算子内存融合](../img/ch05/memory_fusion.png) @@ -63,17 +58,11 @@ Solver)。SOMAS将计算图并行流与数据依赖进行聚合分析,得到 #### In-Place算子 -在前面的内存分配流程中,我们会为每个算子的输入和输出都分配不同的内存。然而对很多算子而言,为其分配不同的输入和输出地址,会浪费内存并且影响计算性能。例如优化器算子,其计算的目的就是更新神经网络的权重;例如Python语法中的'+='和'\*='操作符,将计算结果更新到符号左边的变量中;例如'a\[0\]=b'语法,将'a\[0\]'的值更新为'b'。诸如此类计算有一个特点,都是为了更新输入的值。下面以Tensor的'a\[0\]=b'操作为例介绍In-Place的优点。 - :numref:`inplace-op`左边是非In-Place操作的实现,step1将Tensor -a拷贝到Tensor a',step2将Tensor b赋值给Tensor a',step3将Tensor -a'拷贝到Tensor -a。 :numref:`inplace-op`右边是算子In-Place操作的实现,仅用一个步骤将Tensor -b拷贝到Tensor -a对于的位置上。对比两种实现,可以发现In-Place操作节省了两次拷贝的耗时,并且省去了Tensor -a'内存的申请。 +在内存分配流程中,会为每个算子的输入和输出都分配不同的内存。然而对很多算子而言,为其分配不同的输入和输出地址,会浪费内存并且影响计算性能。例如优化器算子,其计算的目的就是更新神经网络的权重;例如Python语法中的 += 和 *= 操作符,将计算结果更新到符号左边的变量中;例如 a[0]=b 语法,将 a[0] 的值更新为 b。诸如此类计算有一个特点,都是为了更新输入的值。下面以张量的 a[0]=b 操作为例介绍In-Place的优点。 + :numref:`inplace-op`左边是非In-Place操作的实现,step1将张量a拷贝到张量a',step2将张量b赋值给张量a',step3将张量a'拷贝到张量a。 :numref:`inplace-op`右边是算子In-Place操作的实现,仅用一个步骤将张量b拷贝到张量a对应的位置上。对比两种实现,可以发现In-Place操作节省了两次拷贝的耗时,并且省去了张量a'内存的申请。 ![In-Place算子内存分配](../img/ch05/inplace-op.png) :width:`800px` :label:`inplace-op` -这节我们简单介绍了Device内存的概念,内存分配的流程,和一些优化内存分配的方法。内存分配是编译器后端的最重要部分之一,内存的合理分配,不仅关系到相同芯片上能否支持更大的网络模型,也关系到模型在硬件上的执行效率。 +这节简单介绍了设备内存的概念,内存分配的流程,和一些优化内存分配的方法。内存分配是编译器后端的最重要部分之一,内存的合理分配,不仅关系到相同内存容量下能否支持更大的网络模型,也关系到模型在硬件上的执行效率。 diff --git a/chapter_backend_and_runtime/op_compiler.md b/chapter_backend_and_runtime/op_compiler.md new file mode 100644 index 0000000..3c2243f --- /dev/null +++ b/chapter_backend_and_runtime/op_compiler.md @@ -0,0 +1,193 @@ +## 算子编译器 + +算子编译器,顾名思义,即对算子进行编译优化的工具。这里所谓的"算子"可以来自于整个神经网络中的一部分,也可以来自于通过领域特定语言(Domain +Specific Language, +DSL)实现的代码。而所谓编译,通俗来说起到的是针对目标语言进行**表达**和**转换**。 + +从目的上来说,算子编译器致力于提高算子的**执行性能**。从工程实现上来说,算子编译器的输入一般为Python等**动态语言**描述的张量计算,而输出一般为**特定AI芯片**上的可执行文件。 + +### 算子调度策略 + +算子编译器为了实现较好地优化加速,会根据现代计算机体系结构特点,将程序运行中的每个细小操作抽象为"调度策略"。 + +如果不考虑优化和实际中芯片的体系结构特点,只需要按照算子表达式的**计算逻辑**,把输入进来的张量全部加载进计算核心里完成计算,之后再把计算结果从计算核心里面取出并保存下来即可。这里的**计算逻辑**指的就是基本数学运算(如加、减、乘、除)以及其他函数表达式(如卷积、转置、损失函数)等。 + +但是 :numref:`fig:ch05/ch05-memory_architecture`向我们展示的现代计算机存储结构表明:越靠近金字塔顶尖的存储器造价越高但是访问速度越快。 + +![代计算机存储层次图](../img/ch05/memory_architecture.png) +:width:`800px` +:label:`fig:ch05/ch05-memory_architecture` + + +基于这一硬件设计的事实,有局部性(Locality)概念: + +(1)时间局部性,相对较短时间内重复访问特定内存位置。如多次访问L1高速缓存的同一位置的效率会高于多次访问L1中不同位置的效率。 + +(2)空间局部性,在相对较近的存储位置进行访问。如,多次访问L1中相邻位置的效率会高于来回在L1和主存跳跃访问的效率。 + +满足这两者任一都会有较好的性能提升。基于局部性概念,希望尽量把需要重复处理的数据放在固定的内存位置,且这一内存位置离处理器越近越好,以通过提升访存速度而进行性能提升。 + +另外,把传统的串行计算任务按逻辑和数据依赖关系进行分割后,有机会得到多组互不相关的数据,并把他们同时计算,如 :numref:`fig:ch05/ch05-parallel_computing`所示。 + +![串行计算和并行计算区别图](../img/ch05/parallel_computing.png) +:width:`800px` +:label:`fig:ch05/ch05-parallel_computing` + +以上种种在程序实际运行的时候针对数据做出的特殊操作,统称为**调度(Schedule)**。调度定义了: + +(1)应该在何时何处计算函数中的每个值? + +(2)数据应该储存在哪里? + +(3)每个值在多个消费者(Consumer)之间访存需要花费多长时间?另外在何时由每个消费者独立重新计算?这里的消费者指使用前序结构进行计算的值。 + +通俗理解,调度策略指的是:在编译阶段根据目标硬件体系结构的特点而设计出的一整套通过提升局部性和并行性而使得编译出的可执行文件在运行时性能最优的算法。这些算法并不会影响计算结果,只是干预计算过程,以达到提升运算速度的效果。 + +### 子策略组合优化 + +算子编译器的一种优化思路是:将抽象出来的调度策略进行组合,拼接排布出一个复杂而高效的调度集合。子策略组合优化,本质上还是基于人工手动模板匹配的优化方式,依赖于开发人员对于硬件架构有较深的理解。这种方式较为直接,但组合出的优化策略无法调优,同时对各类算子精细化的优化也带来较多的人力耗费。本文以TVM为例,通过在CPU上加速优化一段实际代码,简要介绍其中几种基本调度策略组成的优化算法。 + +我们以形式为乘累加计算的代码[\[lst:before_tvm\]](#lst:before_tvm){reference-type="ref" +reference="lst:before_tvm"}为例简要分析描述这一算法。该代码的核心计算逻辑为:首先对张量C进行初始化,之后将张量A与张量B相乘后,结果累加到张量C中。 + +``` {#lst:before_tvm caption="乘累加计算代码" label="lst:before_tvm"} +for (m: int32, 0, 1024) { + for (n: int32, 0, 1024) { + C[((m*1024) + n)] = 0f32 + for (k: int32, 0, 1024) { + let cse_var_2: int32 = (m*1024) + let cse_var_1: int32 = (cse_var_2 + n) + C[cse_var_1] = (C[cse_var_1] + (A[(cse_var_2 + k)]*B[((k*1024) + n)])) + } + } +} +``` + +假定数据类型为浮点型(Float),此时张量A、B、C的大小均为1024 $\times$ +1024,三者占用的空间共为1024 $\times$ 1024 $\times$ 3 $\times$ +sizeof(float) = 12MB。这远远超出了常见缓存的大小(如L1 +Cache为32KB)。因此按照此代码形式,要将整块张量A、B、C一起计算,只能放入离计算核更远的内存进行计算。其访存效率远低于缓存。 + +为了提升性能,提出使用平铺(Tile),循环移序(Reorder)和切分(Split)的调度策略。由于L1缓存大小为32KB,为了保证每次计算都能够放入缓存中,我们选取因子(Factor)为32进行平铺,使得平铺后的每次计算时只需要关注m.inner +$\times$ +n.inner构成的小块(Block)即可,而其他的外层循环不会影响最内层小块的访存。其占用内存大小为32 +$\times$ 32 $\times$ 3 $\times$ sizeof(float) = +12KB,足够放入缓存中。代码[\[lst:after_tvm\]](#lst:after_tvm){reference-type="ref" +reference="lst:after_tvm"}展示了经过该策略优化优化后的变化。 + +``` {#lst:after_tvm caption="子策略组合优化后的代码" label="lst:after_tvm"} +// 由for (m: int32, 0, 1024)以32为因子平铺得到外层循环 +for (m.outer: int32, 0, 32) { + // 由for (n: int32, 0, 1024)以32为因子平铺得到外层循环 + for (n.outer: int32, 0, 32) { + // 由for (m: int32, 0, 1024)以32为因子平铺得到内层循环 + for (m.inner.init: int32, 0, 32) { + // 由for (n: int32, 0, 1024)以32为因子平铺得到内层循环 + for (n.inner.init: int32, 0, 32) { + // 对应地得到相应系数 + C[((((m.outer*32768) + (m.inner.init*1024)) + (n.outer*32)) + n.inner.init)] = 0f32 + } + } + // 由for (k: int32, 0, 1024)以4为因子切分得到外层循环,并进行了循环移序 + for (k.outer: int32, 0, 256) { + // 由for (k: int32, 0, 1024)以4为因子切分得到外层循环,并进行了循环移序 + for (k.inner: int32, 0, 4) { + // 由for (m: int32, 0, 1024)以32为因子平铺得到内层循环 + for (m.inner: int32, 0, 32) { + // 由for (n: int32, 0, 1024)以32为因子平铺得到内层循环 + for (n.inner: int32, 0, 32) { + // 由n轴平铺得到的外轴系数 + let cse_var_3: int32 = (n.outer*32) + // 由m轴平铺得到的外轴和内轴系数 + let cse_var_2: int32 = ((m.outer*32768) + (m.inner*1024)) + // 由m轴和n轴得到的外轴和内轴系数 + let cse_var_1: int32 = ((cse_var_2 + cse_var_3) + n.inner) + // 这里是核心计算逻辑,划分成不同层次使得每次循环计算的数据能够放入cache中 + C[cse_var_1] = (C[cse_var_1] + (A[((cse_var_2 + (k.outer*4)) + n.inner)] * B[((((k.outer*4096) + (k.inner*1024)) + cse_var_3) + n.inner)])) + } + } + } + } + } +} +``` + +本示例参照TVM提供的"在CPU上优化矩阵乘运算的实例教程"[^1]中的第一项优化,读者可深入阅读后续优化内容。 + +### 调度空间算法优化 + +算子编译器的另外一种优化思路是:通过对调度空间搜索/求解,自动生成对应算子调度。此类方案包括多面体模型编译(Polyhedral +Compilation)(基于约束对调度空间求解)和Ansor(调度空间搜索)等。这类方法的好处是提升了算子编译的泛化能力,缺点是搜索空间过程会导致编译时间过长。 +以多面体模型编译技术将代码的多层循环抽象为多维空间,将每个计算实例抽象为空间中的点,实例间的依赖关系抽象为空间中的线,主要对循环进行优化。该算法的主要思想是针对输入代码的访存特点进行建模,调整循环语句中的每一个实例的执行顺序,使得新调度下的循环代码有更好的局部性和并行性。 + +我们以代码[\[lst:before_poly\]](#lst:before_poly){reference-type="ref" +reference="lst:before_poly"}为例介绍该算法。 + +``` {#lst:before_poly caption="待优化代码" label="lst:before_poly"} +for (int i = 0; i < N; i++) + for (int j = 1; j < N; j++) + a[i+1][j] = a[i][j+1] - a[i][j] + a[i][j-1]; +``` + +如 :numref:`fig:ch05/ch05-poly_test`所示,通过多面体模型算法先对此代码的访存结构进行建模,然后分析实例(即 :numref:`fig:ch05/ch05-poly_test`中节点)间的依赖关系(即 :numref:`fig:ch05/ch05-poly_test`中箭头)。 + +![示例代码的多面体模型](../img/ch05/poly_test.png) +:width:`800px` +:label:`fig:ch05/ch05-poly_test` + + +再进行复杂的依赖分析和调度变换之后得到一个符合内存模型的最优解。代码[\[lst:after_poly\]](#lst:after_poly){reference-type="ref" +reference="lst:after_poly"}显示了经过多面体模型优化后得到的结果。 + +``` {#lst:after_poly caption="多面体模型算法优化后的代码" label="lst:after_poly"} +for (int i_new = 0; i_new < N; i_new++) + for (int j_new = i+1; j_new < i+N; j_new++) + a[i_new+1][j_new-i_new] = a[i_new][j_new-i_new+1] - a[i_new][j_new-i_new] + a[i_new][j_new-i_new-1]; +``` + +观察得到的代码,发现优化后的代码较为复杂。但是仅凭肉眼很难发现其性能优势之处。仍需对此优化后的代码进行如算法描述那样建模,并分析依赖关系后得出结论,如 :numref:`fig:ch05/ch05-poly`所示:经过算法优化后解除了原代码中的循环间的依赖关系,从而提高了并行计算的机会。即沿着 :numref:`fig:ch05/ch05-poly`中虚线方向分割并以绿色块划分后,可以实现并行计算。 +该算法较为复杂,限于篇幅,在这里不再详细展开。读者可移步到笔者专门为此例写的文章-《深度学习编译之多面体模型编译------以优化简单的两层循环代码为例》详读。 + + +![多面体模型优化结果](../img/ch05/poly.png) +:width:`800px` +:label:`fig:ch05/ch05-poly` + + +### 芯片指令集适配 + +前文讲述了算子编译器的优化方法,本小节将阐述算子编译器适配不同芯片上指令集的情况。一般意义上来说,通用编译器的设计会尽量适配多种后端。如此一来,在面临不同体系结构特点和不同编程模型的多种后端时,算子编译器承受了相当大的压力。 + +当下的AI芯片中,常见的编程模型分为:单指令多数据(Single Instruction, +Multiple Data, +SIMD),即单条指令一次性处理大量数据,如 :numref:`fig:ch05/ch05-SIMD`所示;单指令多线程(Single Instruction, +Multiple Threads, +SIMT),即单条指令一次性处理多个线程的数据,如 :numref:`fig:ch05/ch05-SIMT`所示。前者对应的是带有向量计算指令的芯片;后者对应的是带有明显的线程分级的芯片。另外,也有一些芯片开始结合这两种编程模型的特点,既有类似线程并行计算的概念,又有向量指令的支持。针对不同的编程模型,算子编译器在进行优化(如向量化等)时的策略也会有所不同。 + + +![单指令多数据流示意图](../img/ch05/SIMD.png) +:width:`800px` +:label:`fig:ch05/ch05-SIMD` + +![单指令多线程示意图](../img/ch05/SIMT.png) +:width:`800px` +:label:`fig:ch05/ch05-SIMT` + +一般来说,算子编译器在具体的代码中会按照:前端、中端、后端,逐渐差异化的思路进行实现。即在前端设计中兼容多种不同芯片后端的指令集,以帮助编译器用户(即AI程序员)不需要在乎芯片差异,而只需要专注在AI算法逻辑上即可;在中间表示(IR)设计中对不同芯片的体系结构进行区分,从而可以实现对不同芯片进行不同的优化方法;在后端的目标代码生成部分对各个芯片的不同指令集详细区分,以保证生成出的目标代码能够顺利运行在目标芯片上。 + +### 算子表达能力 + +算子表达能力指的是:算子编译器的前端识别输入代码,并在不损失语义信息的情况下转换为中间表示的能力。算子编译器承接的前端输入往往是PyTorch等的Python形式的代码,而Python中各种灵活的表达方式(包括而不限于索引、View语义等)对算子编译器的前端表达能力提出了较高要求。另外在检测网络中,输入算子往往还有大量的控制流语句。此外,还经常可以看到神经网络中存在许多的动态形状问题,即网络中的算子形状会受网络迭代次数和控制流等条件的影响。这些都对算子编译器前端的表达能力提出了很高的要求。 + +在实际工程实践中,发现大量的长尾分布般不常见但性能很差的算子(后文简称为长尾算子)往往是整体网络训练或推理的瓶颈点。而这些长尾算子大都是由于其出现频次低而不至于实现在计算库中。同时其语法过于灵活或存在大量的控制流语句以及动态形状问题而难以被目前的算子编译器前端充分表达出来,因此也难以通过算子编译器进行优化加速。于是,这些长尾算子只好以运行速度较慢的Python解释器或者虚拟机的方式执行,从而成为整个网络中的性能瓶颈。此时,提高算子编译器前端的表达能力就成为了重中之重。 + +### 相关编译优化技术 + +算子编译器与传统编译器在优化技术方面根出同源,但由于面对的问题不同,所以在优化思路上也有差别。两者都以前中后端的思路进行设计,都是以增强局部性和并行性为优化的理论依据。 +但是前者面向的问题是AI领域中的计算问题,往往在优化过程中会大量参考和借鉴高性能计算(High-Performance +Computing, +HPC)的优化思路,这种情况称为借助专家经验进行优化。另外算子编译器面对的后端AI芯片的体系结构的不同,如重点的单指令多数据和单指令多线程为代表的两种后端体系结构,决定了优化过程中更多偏向于生成对单指令多数据友好的加速指令,或者生成对单指令多线程友好的多线程并行计算模型。 +而后者面向的问题是更加通用的标量计算行为和计算机控制命令,往往在优化中围绕寄存器的使用和分支预测准确性等进行优化。 +总之,由于需要解决的问题不同,算子编译器和传统编译器在优化算法的具体实现上有着一定的区别,但是在算法设计时也有互相借鉴的机会。 + +[^1]: 在CPU上优化矩阵乘运算的实例教程: \ No newline at end of file diff --git a/chapter_backend_and_runtime/overview.md b/chapter_backend_and_runtime/overview.md index 77a0dc8..0ab166a 100644 --- a/chapter_backend_and_runtime/overview.md +++ b/chapter_backend_and_runtime/overview.md @@ -1,8 +1,8 @@ ## 概述 -编译器前端主要将用户代码进行解析翻译得到计算图IR并对其进行设备信息无关的优化,此时我们并不考虑程序执行的底层硬件信息。编译器后端的主要职责对前端下发的IR做进一步的计算图优化,让其更加贴合硬件,并为IR中的计算节点选择适合在硬件上执行的算子,然后为每个算子的输入输出分配硬件内存,最终生成一个可以在硬件上执行的任务序列。 +编译器前端主要将用户代码进行解析翻译得到计算图IR,并对其进行设备信息无关的优化,此时的优化并不考虑程序执行的底层硬件信息。编译器后端的主要职责是对前端下发的IR做进一步的计算图优化,让其更加贴合硬件,并为IR中的计算节点选择在硬件上执行的算子,然后为每个算子的输入输出分配硬件内存,最终生成一个可以在硬件上执行的任务序列。 -如 :numref:`compiler-backend-architecture`所示,编译器后端处于前端和硬件驱动层中间,主要负责计算图优化、算子选择和内存分配的任务。首先,需要根据硬件设备的特性将IR图进行等价图变换,以便在硬件上能够找到对应的执行算子,该过程是计算图优化的重要步骤之一。前端IR生成是解析用户代码,属于一个较高的抽象层次,隐藏一些底层运行的细节信息,此时无法直接对应硬件上的算子(算子是设备上的基本计算序列,例如MatMul、Convolution和ReLU等),需要将细节信息进行展开后,才能映射到目标硬件上的算子。对于某些前端IR的子集来说,一个算子便能够执行对应的功能,此时可以将这些IR节点合并成为一个计算节点,该过程称之为算子融合;对于一些复杂计算,后端并没有直接与之对应的算子,但是可以通过几个基本运算的算子组合达到同样的计算效果,此时可以将前端IR节点拆分成多个小算子。然后,我们需要进行算子选择。算子选择是在得到优化的IR图后,需要选取最合适的目标设备算子。针对用户代码所产生的IR往往可以映射成多种不同的硬件算子,但是生成不同的算子执行效率往往有很大的差别,如何根据前端IR选择出最高效的算子,是算子选择的核心问题。算子选择本质上是一个模式匹配问题。其最简单的方法就是每一个IR节点对应一个目标硬件的算子,但是这种方法往往对目标硬件的资源利用比较差。目前来说对于现有的编译器一般都对每一个IR节点提供了多个候选的算子,算子选择目标就是从中选择最优的一个算子作为最终执行在设备上的算子。总的来说,在机器学习系统中,对前端生成的IR图上的各个节点进行拆分和融合,让前端所表示的高层次IR逐步转换为可以在硬件设备上执行的低层次IR。得到了这种更加贴合硬件的IR后,对于每个单节点的IR可能仍然有很多种不同的选择,例如可以选择不同的输入输出格式和数据类型,我们需要对IR图上每个节点选择出最为合适的算子,算子选择过程可以认为是针对IR图的细粒度优化过程,最终生成完整的算子序列。最后,遍历算子序列,为每个算子分配相应的输入输出内存,然后将算子加载到设备上执行计算。 +如 :numref:`compiler-backend-architecture`所示,编译器后端处于前端和硬件驱动层中间,主要负责计算图优化、算子选择和内存分配的任务。首先,需要根据硬件设备的特性将IR图进行等价图变换,以便在硬件上能够找到对应的执行算子,该过程是计算图优化的重要步骤之一。前端IR是通过解析用户代码生成的,属于一个较高的抽象层次,隐藏一些底层运行的细节信息,此时无法直接对应硬件上的算子(算子是设备上的基本计算序列,例如MatMul、Convolution、ReLU等),需要将细节信息进行展开后,才能映射到目标硬件上的算子。对于某些前端IR的子集来说,一个算子便能够执行对应的功能,此时可以将这些IR节点合并成为一个计算节点,该过程称之为算子融合;对于一些复杂计算,后端并没有直接与之对应的算子,但是可以通过几个基本运算的算子组合达到同样的计算效果,此时可以将前端IR节点拆分成多个小算子。在完成计算图优化之后,就要进行算子选择过程,为每个计算节点选择执行算子。算子选择是在得到优化的IR图后选取最合适的目标设备算子的过程。针对用户代码所产生的IR往往可以映射成多种不同的硬件算子,但是这些不同硬件算子的执行效率往往有很大差别,如何根据前端IR选择出最高效的算子,是算子选择的核心问题。算子选择本质上是一个模式匹配问题。其最简单的方法就是每一个IR节点对应一个目标硬件的算子,但是这种方法往往对目标硬件的资源利用比较差。现有的编译器一般都对每一个IR节点提供了多个候选的算子,算子选择目标就是从中选择最优的一个算子作为最终执行在设备上的算子。总的来说,在机器学习系统中,对前端生成的IR图上的各个节点进行拆分和融合,让前端所表示的高层次IR逐步转换为可以在硬件设备上执行的低层次IR。得到了这种更加贴合硬件的IR后,对于每个单节点的IR可能仍然有很多种不同的选择,例如可以选择不同的输入输出格式和数据类型,需要对IR图上每个节点选择出最为合适的算子,算子选择过程可以认为是针对IR图的细粒度优化过程,最终生成完整的算子序列。最后,遍历算子序列,为每个算子分配相应的输入输出内存,然后将算子加载到设备上执行计算。 ![编译器后端总体架构简图](../img/ch05/compiler-backend-architecture.png) :width:`800px` @@ -23,3 +23,7 @@ ### 计算调度与执行 经过算子选择与内存分配之后,计算任务可以通过运行时完成计算的调度与在硬件上的执行。根据是否将算子编译为计算图,计算的调度可以分为单算子调度与计算图调度两种方式。而根据硬件提供的能力差异,计算图的执行方式又可以分为逐算子下发执行的交互式执行以及将整个计算图或者部分子图一次性下发到硬件的下沉式执行两种模式。 + +### 算子编译器 + +作为AI编译器中一个重要组成部分,算子编译器把单个简单或复杂的算子经过表达和优化后编译为一个单独的可执行文件。目前业界面对算子编译器仍有许多有趣的问题尚未得出明确结论,相关的处理逻辑与方法也尚未收敛。本小节希望将这些问题简单抛出,并给出业界比较典型的几种处理方式。若能对业界朋友们和同学们有所启发甚至若能对这些问题起到促进收敛的作用,那真是再好不过!目前尚待收敛的问题包括而不限于:如何通过算子编译器进行性能优化?算子编译器如何兼容不同体系结构特点的芯片?面对输入Python代码的灵活性以及神经网络训练时动态性的情况,该如何充分将这些完美表达出来? diff --git a/chapter_backend_and_runtime/summary.md b/chapter_backend_and_runtime/summary.md index 054a7e3..b91662e 100644 --- a/chapter_backend_and_runtime/summary.md +++ b/chapter_backend_and_runtime/summary.md @@ -18,7 +18,10 @@ - 运行时对于算子的执行可以分为单算子调度和计算图调度两种模式,而在计算图调度模式中,根据具体硬件的能力又可以分为交互式执行和下沉式执行两种方式,交互式执行具备更多的灵活性,下沉执行可以获得更好的计算性能。 +- 算子编译器是优化硬件性能的关键组件。其中,调度策略的优化和基于多面体模型算法的优化是两个关键技术。 + ## 扩展阅读 - 内存分配作为机器学习后端的重要部分,建议阅读 [Sublinear Memory Cost](https://arxiv.org/abs/1604.06174)、 [Dynamic Tensor Rematerialization](https://arxiv.org/abs/2006.09616)。 -- 对于运行时的调度以及执行,建议阅读 [A Lightweight Parallel and Heterogeneous Task Graph Computing System](https://arxiv.org/abs/2004.10908)、 [Dynamic Control Flow in Large-Scale Machine Learning](https://arxiv.org/abs/1805.01772)、[DEEP LEARNING WITH DYNAMIC COMPUTATION GRAPHS](https://arxiv.org/abs/1702.02181)。 \ No newline at end of file +- 对于运行时的调度以及执行,建议阅读 [A Lightweight Parallel and Heterogeneous Task Graph Computing System](https://arxiv.org/abs/2004.10908)、 [Dynamic Control Flow in Large-Scale Machine Learning](https://arxiv.org/abs/1805.01772)、[DEEP LEARNING WITH DYNAMIC COMPUTATION GRAPHS](https://arxiv.org/abs/1702.02181)。 +- 算子编译器是本书的扩展部分,建议阅读提出计算与调度分离的论文: [Halide: A Language and Compiler for Optimizing Parallelism, Locality, and Recomputation in Image Processing Pipelines](https://dl.acm.org/doi/abs/10.1145/2499370.2462176),以及介绍调度空间优化的论文 [Ansor: Generating High-Performance Tensor Programs for Deep Learning](https://arxiv.org/abs/2006.06762)和 [olly - Polyhedral optimization in LLVM](https://arxiv.org/abs/2105.04555) diff --git a/chapter_reinforcement_learning/distributed_node_rl.md b/chapter_reinforcement_learning/distributed_node_rl.md index 9b55a6d..2b6cf89 100644 --- a/chapter_reinforcement_learning/distributed_node_rl.md +++ b/chapter_reinforcement_learning/distributed_node_rl.md @@ -2,7 +2,7 @@ 分布式强化学习系统是比上面介绍的单节点强化学习系统更强大的一种。它能支持多环境多模型并行处理,主要是能同时在多个实际计算机系统上对多个模型进行更新,将大大提高强化学习系统的学习速度和整体表现。我们这里介绍分布式强化学习常见的算法和系统。 -异步优势行动-批判者(Asynchronous Advantage Actor-Critic,A3C)是由DeepMind研究人员 :cite:`mnih2016asynchronous`于2016年提出的可以在多个计算设备上并行更新网络的学习算法。相比于 :numref:`ch12/ch12-rlzoo`中的单节点强化学习系统,A3C通过创建一组工作者(Worker),并将每个工作者分配到不同的计算设备上且为他们各自创建可以交互的环境来实现并行采样和模型更新,同时用一个主(Master)节点维护这些行动者(Actor)和批判者(Critic)网络的更新。行动者是策略网络,批判者是价值网络, 分别对应强化学习中的策略和价值函数。通过这样的设计,整个算法的各个工作者可以实时将所采集到样本计算出的梯度回传到主节点,来更新主节点的模型参数,并在主节点模型更新后随时下发到各个工作者进行模型更新。每个工作者可以单独在一个GPU上进行运算,从而整个算法可以在一个GPU集群上并行更新模型。算法结构由 :numref:`ch12/ch12-a3c`所示。研究表明,分布式强化学习训练除加速模型学习之外,由于其更新梯度是由多个计算节点各自对环境采样计算得到的,还有利于稳定学习表现。 +异步优势行动-批判者(Asynchronous Advantage Actor-Critic,A3C)是由DeepMind研究人员 :cite:`mnih2016asynchronous`于2016年提出的可以在多个计算设备上并行更新网络的学习算法。相比于 :numref:`ch12/ch12-rlzoo`中的单节点强化学习系统,A3C通过创建一组工作者(Worker),并将每个工作者分配到不同的计算设备上且为他们各自创建可以交互的环境来实现并行采样和模型更新,同时用一个主(Master)节点维护这些行动者(Actor)和批判者(Critic)网络的更新。行动者是策略网络,批判者是价值网络,分别对应强化学习中的策略和价值函数。通过这样的设计,整个算法的各个工作者可以实时将所采集到样本计算出的梯度回传到主节点,来更新主节点的模型参数,并在主节点模型更新后即时下发到各个工作者进行模型更新。每个工作者可以单独在一个 GPU 上进行运算,从而整个算法可以在一个 GPU 集群上并行更新模型,算法结构由 :numref:`ch12/ch12-a3c`所示。研究表明,分布式强化学习训练除加速模型学习之外,由于其更新梯度是由多个计算节点各自对环境采样计算得到的,还有利于稳定学习表现。 ![A3C分布式算法架构](../img/ch12/ch12-a3c.png) @@ -10,7 +10,7 @@ :label:`ch12/ch12-a3c` -重要性加权行动-学习者架构(Importance Weighted Actor-Learner Architecture,IMPALA) 是由Lasse Espeholt等人于2018年 :cite:`espeholt2018impala`提出的能够实现多机集群训练的强化学习框架。与A3C算法类似,IMPALA能够在多个GPU上并行进行梯度计算。具体地,IMPALA并行多个行动者(Actor)和学习者(Learner),每个行动者包含一个策略网络,并用它来和一个环境交互收集样本。所收集到的样本轨迹由行动者发送到各自的学习者,进行梯度计算。所有的学习者中有一个称为主学习者,它可以和其他所有学习者通信获取他们计算的梯度,从而在主学习者内部对模型进行更新,随后下发到各个学习者及行动者,做新一轮的采样和梯度计算。IMPALA被证明是比A3C更高效的分布式计算架构,它同时得益于一个特殊设计的学习者内的梯度计算函数,称为V-轨迹目标(V-trace Target),通过重要性加权来稳定训练。我们这里侧重对分布式强化学习结构的介绍,对此不再赘述。感兴趣的读者可以参考原论文。 +重要性加权行动-学习者架构(Importance Weighted Actor-Learner Architecture,IMPALA) 是由Lasse Espeholt等人于2018年 :cite:`espeholt2018impala`提出的能够实现多机集群训练的强化学习框架,如:numref:`ch12/ch12-impala`所示。与 A3C 算法类似,IMPALA 能够在多个 GPU 上并行进行梯度计算。具体地,IMPALA 并行多个行动者(Actor)和学习者(Learner),每个行动者包含一个策略网络,并用这个策略网络与一个环境进行交互,以收集样本。所收集到的样本轨迹由行动者发送到各自的学习者,进行梯度计算。所有的学习者中有一个称为主学习者,它可以和其他所有学习者通信获取他们计算的梯度,从而在主学习者内部对模型进行更新,随后下发到各个学习者及行动者,做新一轮的采样和梯度计算。IMPALA 被证明是比 A3C 更高效的分布式计算架构,它同时得益于一个特殊设计的学习者内的梯度计算函数,称为 V-轨迹目标(V-trace Target),通过重要性加权来稳定训练。我们这里侧重对分布式强化学习结构的介绍,对此不再赘述。感兴趣的读者可以参考原论文 ![IMPALA分布式算法架构](../img/ch12/ch12-impala.png) @@ -26,7 +26,8 @@ :label:`ch12/ch12-rllib` -Ray :cite:`moritz2018ray`是由伯克利大学几名研究人员发起的一个分布式计算框架,基于Ray之上构建了一个专门针对强化学习的系统RLlib :cite:`liang2017ray`。RLlib是一个面向工业级应用的开源强化学习框架,同时包含了强化学习的算法库,它对非强化学习专家使用也很方便。 +Ray :cite:`moritz2018ray`是由伯克利大学几名研究人员发起的一个分布式计算框架,基于Ray之上构建了一个专门针对强化学习的系统RLlib :cite:`liang2017ray`。RLlib 是一个面向工业级应用的开源强化学习框架,同时 +包含了强化学习的算法库,没有太多强化学习经验的人也可以很方便地使用 RLlib。 ![RLlib分布式训练](../img/ch12/ch12-rllib-distributed.svg) @@ -34,8 +35,9 @@ Ray :cite:`moritz2018ray`是由伯克利大学几名研究人员发起的一个 :label:`ch12/ch12-rllib_dist` -RLlib的系统架构如 :numref:`ch12/ch12-rllib`所示,系统底层是构建在Ray的分布式计算和通信的基础组建之上,面向强化学习的领域概念,在Python层抽象了Trainer, Environment, Policy等基础组件,并为各个抽象组件提供了一些常用的内置实现,同时用户可以根据自己的算法场景对组件进行扩展,通过这些内置以及自定义的算法组件,研究人员可以方便快速地实现具体的强化学习算法。RLlib支持多种范式的分布式强化学习训练,如 :numref:`ch12/ch12-rllib_dist`所示为基于同步采样的强化学习算法的分布式训练架构。其中每一个Rollout Worker为一个独立进程,负责和对应的环境进行交互以完成经验采集,多个Rollout Worker可以并行地完成环境交互;Trainer负责Rollout Worker之间的协调,策略优化,以及将更新后的策略同步到Rollout Worker中。 +RLlib的系统架构如 :numref:`ch12/ch12-rllib`所示,系统底层是构建在 Ray 的分布式计算和通信的基础组建之 +上,面向强化学习的领域概念,在 Python 层抽象了 Trainer, Environment, Policy 等基础组件,并为各个抽象组件提供了一些常用的内置实现,同时用户可以根据自己的算法场景对组件进行扩展,通过这些内置以及自定义的算法组件,研究人员可以方便快速地实现具体的强化学习算法。RLlib支持多种范式的分布式强化学习训练,如 :numref:`ch12/ch12-rllib_dist`所示为基于同步采样的强化学习算法的分布式训练架构。其中每一个 Rollout Worker 为一个独立进程,负责和对应的环境进行交互以完成经验采集,多个 Rollout Worker 可以并行地完成环境交互;Trainer 负责 Rollout Worker之间的协调,策略优化,以及将更新后的策略同步到 Rollout Worker 中。 -强化学习中的策略通常可以采用深度神经网络,而基于深度神经网络的分布式强化学习训练,可以采用RLlib结合PyTorch或者TensorFlow等深度学习框架协同完成,深度学习框架负责策略网络的训练和更新,RLlib负责强化学习的算法计算。此外RLlib支持对环境交互使用向量化(Vectorized)的并行方式,允许外接模拟器,以及可以进行离线(Offline)强化学习。 +强化学习中的策略通常可以采用深度神经网络,而基于深度神经网络的分布式强化学习训练,可以采用 RLlib 结合 PyTorch 或者 TensorFlow 等深度学习框架协同完成,深度学习框架负责策略网络的训练和更新,RLlib 负责强化学习的算法计算。此外 RLlib 支持与并行的向量化(Vectorized)环境交互,允许外接模拟器,以及可以进行离线(Offline)强化学习。 -对于分布式系统中样本回放缓冲池的管理,我们会提到另一个工作Reverb :cite:`cassirer2021reverb`。回忆本章开头,我们介绍了强化学习中的状态、动作、奖励等概念,实际强化学习算法进行训练所使用的数据正是存放在经验缓冲池中的这些数据元组,而每种数据自身的格式可能又有不同,实际使用时也需要对不同的数据做不同类型的操作。常见的数据操作类型如拼接、截取、乘积、转置、部分乘积、取均值、取极值等,而每种操作都可能需要对特定数据的特定维度进行,这常常给现有的强化学习框架在实践中产生一定的困难。为了方便强化学习过程中灵活使用不同的数据形式,Reverb设计了数据块的概念(Chunks),所有使用的训练数据在缓冲池中都使用数据块的格式进行管理和调用,这一设计基于数据是多维张量的特点,增大了数据使用的灵活性和访问速度。Acme :cite:`hoffman2020acme`是近年来由DeepMind提出的一个分布式强化学习框架,同样是针对学术界的研究和工业界的应用,它基于Reverb对样本缓冲池的数据管理,结合分布式采样的结构,给出了一个更快的分布式强化学习解决方案。Reverb帮助解决了数据管理和传输的效率问题,使得Acme得以将分布式计算的效力充分发挥,研究人员用Acme在大量强化学习基准测试中取得了显著的速度提升。 \ No newline at end of file +对于分布式系统中样本回放缓冲池的管理,我们会提到另一个工作Reverb :cite:`cassirer2021reverb`。回忆本章开头,我们介绍了强化学习中的状态、动作、奖励等概念,实际强化学习算法进行训练所使用的数据正是存放在经验缓冲池中的这些数据元组,而每种数据自身的格式可能又有不同,实际使用时也需要对不同的数据做不同类型的操作。常见的数据操作类型如拼接、截取、乘积、转置、部分乘积、取均值、取极值等,而每种操作都可能需要对特定数据的特定维度进行,这常常给现有的强化学习框架在实践中产生一定的困难。为了方便强化学习过程中灵活使用不同的数据形式,Reverb 设计了数据块的概念(Chunks),所有使用的训练数据在缓冲池中都使用数据块的格式进行管理和调用,这一设计基于数据是多维张量的特点,增大了数据使用的灵活性和访问速度。Acme :cite:`hoffman2020acme`是近年来由DeepMind提出的一个分布式强化学习框架,同样是针对学术界的研究和工业界的应用,它基于 Reverb 对样本缓冲池的数据管理,结合分布式采样的结构,给出了一个更快的分布式强化学习解决方案。Reverb 帮助解决了数据管理和传输的效率问题,使得 Acme得以将分布式计算的效力充分发挥,研究人员用 Acme 在大量强化学习基准测试中取得了显著的速度提升。 \ No newline at end of file diff --git a/chapter_reinforcement_learning/marl.md b/chapter_reinforcement_learning/marl.md index 227cade..d230b5a 100644 --- a/chapter_reinforcement_learning/marl.md +++ b/chapter_reinforcement_learning/marl.md @@ -1,8 +1,6 @@ ## 多智能体强化学习 -以上所讲述的强化学习内容都为单智能体强化学习,而在近来的强化学习研究中,多智能体强化学习越来越受到研究人员关注。回想在本小节初介绍的单智能体强化学习框架 :numref:`ch12/ch12-rl-framework`,其中我们只有单个智能体产生的单个动作对环境产生影响,环境也返回单个奖励值给智能体。这里我们把单智能体强化学习扩展到多智能体强化学习,可以得到至少两种可能的多智能体强化学习框架,如 :numref:`ch12/ch12-marl`所示。 :numref:`ch12/ch12-marl`(a)为多智能体同时执行动作的情况,他们相互之间观察不到彼此的动作,他们的动作一同对环境产生影响,并各自接受自己动作所产生的奖励。 :numref:`ch12/ch12-marl`(b)为多智能体顺序执行动作的情况,后续智能体可能观察到前序智能体的动作,他们的动作一同对环境产生影响,并接受到各自的奖励值或共同的奖励值。除此之外,还有许多其他可能的多智能体框架,如更复杂的智能体间观察机制、智能体间通讯机制、多智能体合作与竞争等等。同时,这里假设多个智能体对环境的观察量都为环境的状态,这是最简单的一种,也是现实中最不可能出现的一种,实际情况下的多智能体往往对环境有各自不同的观察量。 - -![两种可能的多智能体强化学习框架:(a)同步式多智能体决策;(b)异步式多智能体决策。](../img/ch12/ch12-marl.png) +以上所讲述的强化学习内容都为单智能体强化学习,而在近来的强化学习研究中,多智能体强化学习越来越受到研究人员关注。回想在本小节初介绍的单智能体强化学习框架 :numref:`ch12/ch12-rl-framework`,其中我们只有单个智能体产生的单个动作对环境产生影响,环境也返回单个奖励值给智能体。这里我们把单智能体强化学习扩展到多智能体强化学习,可以得到至少两种可能的多智能体强化学习框架,如 :numref:`ch12/ch12-marl`所示。 :numref:`ch12/ch12-marl`(a)为多智能体同时执行动作的情况,他们相互之间观察不到彼此的动作,他们的动作一同对环境产生影响,并各自接受自己动作所产生的奖励。 :numref:`ch12/ch12-marl`(b)为多智能体顺序执行动作的情况,后续智能体可能观察到前序智能体的动作,他们的动作一同对环境产生影响,并接受到各自的奖励值或共同的奖励值。除此之外,还有许多其他可能的多智能体框架,如更复杂的智能体间观察机制、智能体间通讯机制、多智能体合作与竞争等等。同时,这里假设多个智能体对环境的观察量都为环境的状态,这是最简单的一种,也是现实中最不可能出现的一种,实际情况下的多智能体往往对环境有各自不同的观察量。![两种可能的多智能体强化学习框架:(a)同步式多智能体决策;(b)异步式多智能体决策。](../img/ch12/ch12-marl.png) :width:`800px` @@ -12,16 +10,18 @@ 由上述介绍和定义可以发现,多智能体强化学习是一个比单智能体强化学习更加复杂的问题。而实际上,多个智能体的存在,对于每个智能体的决策而言,绝对不是简单的把每个单智能体决策累加的难度,实际情况要比单智能体决策问题复杂很多。多智能体系统的研究实际上是门古老的学科,它与博弈论(Game Theory)密切相关,在深度强化学习盛行以前早已有大量研究和许多理论上未解的难题。其中一个典型的问题是纳什均衡在双人非零和博弈下没有多项式时间内可解的方法(实际上,这是一个PPAD(Polynomial Parity Argument, Directed version)类的问题。(见论文Settling the Complexity of Computing Two-Player Nash Equilibria. Xi Chen, et al.)由于篇幅限制,我们这里无法对多智能体问题做深入探讨,我们可以用一个简单例子来介绍为什么多智能体强化学习问题无法简单地用单智能体强化学习算法来解。 -:剪刀-石头-布奖励值 +:剪刀-石头-布的奖励值表 -| | 剪刀 | 石头 | 布 | +| 奖励值 | 剪刀 | 石头 | 布 | | --- | ------- | ------- | ------- | -| 剪刀 | (0,0) | (-1,+1) | (+1,-1) | -| 石头 | (+1,-1) | (0,0) | (-1,+1) | -| 布 | (-1,+1) | (+1,-1) | (0,0) | -:label:`tab_ch12_ch12_marl` +| **剪刀** | (0,0) | (-1,+1) | (+1,-1) | +| **石头** | (+1,-1) | (0,0) | (-1,+1) | +| **布** | (-1,+1) | (+1,-1) | (0,0) | +|:label:`tab_ch12_ch12_marl`|||| -我们考虑一个大家都熟悉的游戏, 剪刀-石头-布,考虑两个玩家玩这个游戏的输赢情况,我们知道有这样的输赢关系:剪刀<石头<布<剪刀...这里的“<”即前一个纯策略被后一个纯策略完全压制,我们给予奖励值-1、+1到这两个玩家,当他们选择相同的纯策略时,奖励值均为0。于是我们得到一个奖励值表如 :numref:`tab_ch12_ch12_marl`所示,横轴为玩家1,纵轴为玩家2,表内的数组为玩家1和玩家2各自在相应动作下得到的奖励值。由于这个矩阵的反对称性,这个问题的纳什均衡策略对两个玩家相同,均为$(\frac{1}{3}, \frac{1}{3}, \frac{1}{3})$的策略分布,即有各$\frac{1}{3}$的概率出剪刀、石头或布。如果我们把得到这个纳什均衡策略作为多智能体学习的目标,那么我们可以简单分析得到这个均衡策略无法通过简单的单智能体算法得到。考虑我们随机初始化两个玩家为任意两个纯策略,比如玩家1出剪刀,玩家2出石头。这时假设玩家2策略固定,可以把玩家2看做固定环境的一部分,于是可以使用任意单智能体强化学习算法对玩家1进行训练,使其最大化自己的奖励值。于是,玩家1会收敛到布的纯策略。这时再把玩家1固定,训练玩家2,玩家2又收敛到剪刀的纯策略。于是循环往复,整个训练过程始终无法收敛,玩家1和2各自在3个策略中循环却无法得到正确的纳什均衡策略。 +我们考虑一个大家都熟悉的游戏, 剪刀-石头-布,考虑两个玩家玩这个游戏的输赢情况,我们知道有这样的输赢关系:剪刀<石头<布<剪刀...这里的“<”即前一个纯策略被后一个纯策略完全压制,我们给予奖励值-1、+1到这两个玩家,当他们选择相同的纯策略时,奖励值均为0。于是我们得到一个奖励值表如 :numref:`tab_ch12_ch12_marl`所示,横轴为玩家1,纵轴为玩家2,表内的数组为玩家1和玩家2各自在相应动作下得到的奖励值。 + +由于这个矩阵的反对称性,这个问题的纳什均衡策略对两个玩家相同,均为$(\frac{1}{3}, \frac{1}{3}, \frac{1}{3})$的策略分布,即有各$\frac{1}{3}$的概率出剪刀、石头或布。如果我们把得到这个纳什均衡策略作为多智能体学习的目标,那么我们可以简单分析得到这个均衡策略无法通过简单的单智能体算法得到。考虑我们随机初始化两个玩家为任意两个纯策略,比如玩家1出剪刀,玩家2出石头。这时假设玩家2策略固定,可以把玩家2看做固定环境的一部分,于是可以使用任意单智能体强化学习算法对玩家1进行训练,使其最大化自己的奖励值。于是,玩家1会收敛到布的纯策略。这时再把玩家1固定,训练玩家2,玩家2又收敛到剪刀的纯策略。于是循环往复,整个训练过程始终无法收敛,玩家1和2各自在3个策略中循环却无法得到正确的纳什均衡策略。 ![自学习算法示意图。](../img/ch12/ch12-marl-sp.png) @@ -30,7 +30,9 @@ :label:`ch12/ch12-marl-sp` -我们在上面这个例子中采用的学习方法其实是多智能体强化学习中最基础的一种,叫自学习(Selfplay),如 :numref:`ch12/ch12-marl-sp`所示。自学习的方法即固定当前对首次策略,按照单智能体优化的方法最大化一侧智能体的表现,这一过程称为最佳反应策略(Best Response Strategy)。之后再将这一最佳反应策略作为该智能体的固定策略,再来优化另一边的智能体策略,如此循环。我们可以看到自学习在特定的任务设置下可能无法收敛到我们想要的最终目标。正是由于多智能体学习过程中有类似循环结构的出现,我们需要更复杂的训练方法,和专门针对多智能体的学习方式来达到我们想要的目标。一般来讲,多智能体强化学习是比单智能体强化学习更复杂的一类,对于自学习的方法而言,单智能体强化学习的过程可以看做一个多智能体强化学习的子任务。从前面这一小游戏的角度来理解,当玩家1策略固定时,玩家1加游戏环境构成玩家2的实际学习环境,由于这个环境是固定的,玩家2可以通过单智能体强化学习来达到自身奖励值最大化;这时再固定玩家2的策略,玩家1又可以进行单智能体强化学习......这样,单智能体强化学习是多智能体任务的子任务。其他算法如虚构自学习(Fictitious Self-play) :numref:`ch12/ch12-marl-fsp`,需要在每个单智能体强化学习的步骤中,对对手历史策略的平均策略求得最优应对策略,而对手的训练也是如此,进行循环,能够在上面剪刀-石头-布一类的游戏中保证收敛到纳什均衡策略。 +我们在上面这个例子中采用的学习方法其实是多智能体强化学习中最基础的一种,叫自学习(Selfplay),如 :numref:`ch12/ch12-marl-sp`所示。自学习的方法即固定当前玩家 1 的策略,按照单智能体优化的方法最大化一侧智能体的表现,所得策略称为最佳反应策略(Best Response Strategy)。之后再将这一最佳反应策略作为玩家 2 的固定策略,再来优化另一边的智能体策略,如此循环。我们可以看到自学习在特定的任务设置下可能无法收敛到我们想要的最终目标。正是由于多智能体学习过程中有类似循环结构的出现,我们需要更复杂的训练方法,和专门针对多智能体的学习方式来达到我们想要的目标。 + +一般来讲,多智能体强化学习是比单智能体强化学习更复杂的一类,对于自学习的方法而言,单智能体强化学习的过程可以看做一个多智能体强化学习的子任务。从前面这一小游戏的角度来理解,当玩家 1 策略固定时,玩家 1 加游戏环境构成玩家 2 的实际学习环境,由于这个环境是固定的,玩家 2 可以通过单智能体强化学习来达到自身奖励值最大化;这时再固定玩家 2 的策略,玩家 1 又可以进行单智能体强化学习...... 这样,单智能体强化学习是多智能体任务的子任务。如 :numref:`ch12/ch12-marl-fsp`,其他算法如虚构自学习(Fictitious Self-play),需要在每个单智能体强化学习的步骤中,对对手历史策略的平均策略求得最优应对策略,而对手的训练也是如此,进行循环,能够在上面剪刀-石头-布一类的游戏中保证收敛到纳什均衡策略。 ![虚构自学习算法示意图。](../img/ch12/ch12-marl-fsp.png) diff --git a/chapter_reinforcement_learning/marl_sys.md b/chapter_reinforcement_learning/marl_sys.md index 963c736..aceb638 100644 --- a/chapter_reinforcement_learning/marl_sys.md +++ b/chapter_reinforcement_learning/marl_sys.md @@ -1,6 +1,7 @@ ## 多智能体强化学习系统 -上述的简单例子只是为了帮助读者理解强化学习在多智能体问题里的角色,而如今前沿的多智能体强化学习算法已经能够解决相当大规模的复杂多智能体问题,如星际争霸(StarCraft II)、Dota 2等游戏,已相继被DeepMind、OpenAI等公司所研究的智能体AlphaStar :cite:`vinyals2019grandmaster`和OpenAI Five :cite:`berner2019dota`攻克,达到超越人类顶级玩家的水平。国内公司如腾讯、启元世界等也提出了星际争霸游戏的多智能体强化学习解决方案TStarBot-X :cite:`han2020tstarbot`和SCC :cite:`wang2021scc`。对于这类高度复杂的游戏环境,整个训练过程对分布式计算系统的要求更高,而整个训练过程可能需要分为多个阶段。以AlphaStar为例,它训练的智能体采用了监督学习与强化学习结合的方式。在训练早期,往往先采用大量的人类专业玩家标定数据进行有监督的学习,从而使智能体快速获得较好的能力,随后,训练会切换到强化学习过程,使用前面介绍的虚构自学习的算法进行训练,即自我博弈。为了得到一个表现最好的智能体,算法需要充分探索整个策略空间,从而在训练中不止对一个策略进行训练,而是对一个策略集群(League)进行训练,并通过类似演化算法的方式对策略集群进行筛选,得到大量策略中表现最好的策略。如 :numref:`ch12/ch12-marl_train`所示,在训练过程中每个智能体往往需要和其他智能体以及剥削者(Exploiter)进行博弈,剥削者是专门针对某一个智能体策略的最佳对手策略,与之对抗可以提高策略自身的防剥削能力。通过对大量智能体策略进行训练并筛选的这类方法称为集群式训练(Population-based Training/League Training),是一种通过分布式训练提高策略种群多样性进而提升模型表现的方式。可见,在实践中这类方法自然需要分布式系统支持,来实现多个智能体的训练和相互博弈,这很好地体现了多智能体强化学习对分布式计算的依赖性。 +上述的简单例子只是为了帮助读者理解强化学习在多智能体问题里的角色,而如今前沿的多智能体强化学习算法已经能够解决相当大规模的复杂多智能体问题,如星际争霸(StarCraft II)、Dota 2等游戏,已相继被DeepMind、OpenAI等公司所研究的智能体AlphaStar :cite:`vinyals2019grandmaster`和OpenAI Five :cite:`berner2019dota`攻克,达到超越人类顶级玩家的水平。国内公司如腾讯、启元世界等也提出了星际争霸游戏的多智能体强化学习解决方案TStarBot-X :cite:`han2020tstarbot`和SCC :cite:`wang2021scc`。对于这类高度复杂的游戏环境,整个训练过程对分布式计算系统的要求更高,而整个训练过程可能需要分为多个阶段。以 AlphaStar 为例,它训练的智能体采用了监督学习与强化学习结合的方式。在训练早期,往往先采用大量的人类专业玩家标定数据进行有监督的学习,从而使智能体快速获得较好的能力,随后,训练会切换到强化学习过程,使用前面介绍的虚构自学习的算法进行训练,即自我博弈。为了得到一个表现最好的智能体,算法需要充分探索整个策略空间,从而在训练中不止对一个策略进行训练,而是对一个策略集群(League)进行训练,并通过类似演化算法的方式对策略集群进行筛选,得到大量策略中表现最好的策略。如 :numref:`ch12/ch12-marl_train`所示,在训练过程中每个智能体往往需要 +和其他智能体以及剥削者(Exploiter)进行博弈,剥削者是专门针对某一个智能体策略的最佳对手策略,与之对抗可以提高策略自身的防剥削能力。通过对大量智能体策略进行训练并筛选的这类方法称为集群式训练(Population-based Training/League Training),是一种通过分布式训练提高策略种群多样性进而提升模型表现的方式。可见,在实践中这类方法自然需要分布式系统支持,来实现多个智能体的训练和相互博弈,这很好地体现了多智能体强化学习对分布式计算的依赖性。 ![集群式多智能体强化学习训练示意图](../img/ch12/ch12-marl-train.svg) @@ -12,22 +13,26 @@ * **智能体个数带来的复杂度**:从单智能体系统到多智能体系统最直接的变化,就是智能体个数从1变为大于1个。对于一个各个智能体独立的$N$智能体系统而言,这种变化带来的策略空间表示复杂度是指数增加的,即$\tilde{O}(e^N)$。举个简单的例子,对于一个离散空间的单智能体系统,假设其状态空间大小为$S$, 动作空间大小为$A$,游戏步长为$H$,那么这个离散策略空间的大小为$O(HSA)$;而直接将该游戏扩展为$N$玩家游戏后,在最一般的情况下,即所有玩家有对称的动作空间动作空间大小为$A$且不共享任何结构信息,所有玩家策略的联合分布空间大小为$O(HSA^N)$。这是因为每个独立玩家的策略空间构成联合策略空间是乘积关系$\mathcal{A}=\mathcal{A}_1\times\dots\mathcal{A}_N$。而这将直接导致算法搜索复杂度提升。 -* **游戏类型带来的复杂度**:从博弈论的角度,多智能系统所产生的游戏类型是复杂的。从最直接的分类角度,有竞争型、合作型、混合型。在竞争型游戏中,最典型的研究模型是二人零和博弈,如前一小结中提到的剪刀-石头-布的游戏。这类游戏中的纳什均衡策略一般为混合型策略,即无法通过单一纯策略达到均衡条件。纯策略纳什均衡存在于少数零和游戏中。合作型游戏即多个智能体需要通过合作来提升整体奖励。在这类问题研究中一般采用基于值分解的思路,将所有智能体得到的奖励值分配到单个智能体作为其奖励值。这一类的算法有VDN :cite:`sunehag2017value`, COMA :cite:`foerster2018counterfactual`, QMIX :cite:`rashid2018qmix`等。在混合型游戏中,部分智能体之间为合作关系,部分智能体或智能体的集合间为竞争关系。一般的非零和博弈且非纯合作型游戏为混合型游戏,举个简单的例子如囚徒困境(Prisoner's Dilemma), 其奖励值表如 :numref:`tab_ch12_ch12_marl_prison`所示。囚徒困境的两个玩家各有两个动作,沉默和背叛。可以用警察审查两名罪犯来理解,奖励值的绝对值即他们将被判处的年数。纯所有玩家的奖励值之和非常数,故其为非零和博弈型游戏。因此这一游戏不能被认为是纯竞争型或纯合作型游戏,因为当他们中的一方选择沉默一方选择背叛时,二者没有有效合作,而一方拿到了0的奖励,另一方为-3。而两者都选择沉默时是一种合作策略,各自拿到-1的奖励值。尽管这一策略看起来优于其他策略,但是这并不是这个游戏的纳什均衡策略,因为纳什均衡策略假设玩家间策略需要单独制定,无法形成联合策略分布。这实际上切断了玩家间的信息沟通和潜在合作的可能。因此,囚徒困境的纳什均衡策略是两个玩家都选择背叛对方。诸如此类的博弈论游戏类型,导致单智能体强化学习不能被直接用来优化多智能体系统中的各个智能体的策略。单智能体强化学习一般是找极值的过程,而多智能体系统求解纳什均衡策略往往是找极大-极小值即鞍点的过程,从优化的角度看这也是不同的。复杂的关系需要更普适的系统进行表达,这也对多智能体系统的构建提出了挑战。多智能体游戏类型也有许多其他的分类角度,如单轮进行的游戏、多轮进行的游戏、多智能体同时决策的、多智能体序贯决策等等,每一类不同的游戏都有相应不同的算法。而现有的多智能体系统往往针对单一类型游戏或者单一算法,缺少普适性多智能体强化学习系统,尤其是分布式的系统。 + 在这种情况下,原先的单智能体系统,需要扩展为对多智能体策略进行优化的系统,这意味着单智能体分布式系统内的每个并行化的模块现在需要相应扩展到多智能体系统中的每个智能体上。而在复杂的情况下,还需要考虑智能体之间通信过程、智能体之间的异质性等,甚至不同智能体可能需要采用不完全对称模型进行表示,以及采用不同的算法进行优化等等。 + +* **游戏类型带来的复杂度**:从博弈论的角度,多智能系统所产生的游戏类型是复杂的。从最直接的分类角度,有竞争型、合作型、混合型。在竞争型游戏中,最典型的研究模型是二人零和博弈,如前一小节中提到的剪刀-石头-布的游戏。这类游戏中的纳什均衡策略一般为混合型策略,即无法通过单一纯策略达到均衡条件。纯策略纳什均衡存在于少数零和游戏中。合作型游戏即多个智能体需要通过合作来提升整体奖励。在这类问题研究中一般采用基于值分解的思路,将所有智能体得到的奖励值分配到单个智能体作为其奖励值。这一类的算法有VDN :cite:`sunehag2017value`, COMA :cite:`foerster2018counterfactual`, QMIX :cite:`rashid2018qmix`等。 + + 在混合型游戏中,部分智能体之间为合作关系,部分智能体或智能体的集合间为竞争关系。一般的非零和博弈且非纯合作型游戏为混合型游戏,举个简单的例子如囚徒困境(Prisoner's Dilemma), 其奖励值表如 :numref:`tab_ch12_ch12_marl_prison`所示。囚徒困境的两个玩家各有两个动作,沉默和背叛。可以用警察审查两名罪犯来理解,奖励值的绝对值即他们将被判处的年数。纯所有玩家的奖励值之和非常数,故其为非零和博弈型游戏。因此这一游戏不能被认为是纯竞争型或纯合作型游戏,因为当他们中的一方选择沉默一方选择背叛时,二者没有有效合作,而一方拿到了 0 的奖励,另一方为-3。而两者都选择沉默时是一种合作策略,各自拿到-1 的奖励值。尽管这一策略看起来优于其他策略,但是这并不是这个游戏的纳什均衡策略,因为纳什均衡策略假设玩家间策略需要单独制定,无法形成联合策略分布。这实际上切断了玩家间的信息沟通和潜在合作的可能。因此,囚徒困境的纳什均衡策略是两个玩家都选择背叛对方。 + 诸如此类的博弈论游戏类型,导致单智能体强化学习不能被直接用来优化多智能体系统中的各个智能体的策略。单智能体强化学习一般是找极值的过程,而多智能体系统求解纳什均衡策略往往是找极大-极小值即鞍点的过程,从优化的角度看这也是不同的。复杂的关系需要更普适的系统进行表达,这也对多智能体系统的构建提出了挑战。多智能体游戏类型也有许多其他的分类角度,如单轮进行的游戏、多轮进行的游戏、多智能体同时决策的、多智能体序贯决策等等,每一类不同的游戏都有相应不同的算法。而现有的多智能体系统往往针对单一类型游戏或者单一算法,缺少普适性多智能体强化学习系统,尤其是分布式的系统。 :囚徒困境奖励值 -| | 沉默 | 背叛 | -| --- | ------- | ------- | -| 沉默 | (-1,-1) | (-3,0) | -| 背叛 | (0,-3) | (-2,-2) | -:label:`tab_ch12_ch12_marl_prison` +| 奖励值 | 沉默 | 背叛 | +| --- | ------- | ------- | +| **沉默** | (-1,-1) | (-3,0) | +| **背叛** | (0,-3) | (-2,-2) | +|:label:`tab_ch12_ch12_marl_prison`||| -* **算法的异构**:从前面介绍的几个简单的多智能体算法,如自学习、虚构自学习等可以看出,多智能体算法有时由许多轮单智能体强化学习过程组成。而对不同的游戏类型,算法的类型也不相同。比如,对合作型游戏,许多算法是基于奖励分配(Credit Assignment)的思想,如何将多个智能体获得的共同奖励合理分配给单个智能体是这类算法的核心。而这里面按照具体算法执行方式,也可以分为集成训练统一执行的(Centralized Training Centralized Execution)、集成训练分别执行的(Centralized Training Decentralized Execution)、分别训练并分别执行(Decentralized Training Decentralized Execution)的几类,来描述不同智能体训练过程和执行过程的统一性。对于竞争型游戏,往往采用各种计算纳什均衡的近似方法,如前面提到的虚构自学习、Double Oracle、Mirror Descent等等,将获取单个最优策略的单智能体强化学习过程看做一个“动作”,而对这些“动作”组成的元问题上进行纳什均衡近似。现有的算法在类似问题上有很大的差异性,使得构建一个统一的多智能体强化学习系统比较困难。 - +* **算法的异构**:从前面介绍的几个简单的多智能体算法,如自学习、虚构自学习等可以看出,多智能体算法有时由许多轮单智能体强化学习过程组成。而对不同的游戏类型,算法的类型也不相同。比如,对合作型游戏,许多算法是基于功劳分配(Credit Assignment)的思想,如何将多个智能体获得的共同奖励合理分配给单个智能体是这类算法的核心。而这里面按照具体算法执行方式,也可以分为集成训练统一执行的(Centralized Training Centralized Execution)、集成训练分别执行的(Centralized Training Decentralized Execution)、分别训练并分别执行(Decentralized Training Decentralized Execution)的几类,来描述不同智能体训练过程和执行过程的统一性。对于竞争型游戏,往往采用各种计算纳什均衡的近似方法,如前面提到的虚构自学习、Double Oracle、Mirror Descent 等等,将获取单个最优策略的单智能体强化学习过程看做一个“动作”,而对这些“动作”组成的元问题上进行纳什均衡近似。现有的算法在类似问题上有很大的差异性,使得构建一个统一的多智能体强化学习系统比较困难。 * **学习方法组合**:在前面提到的AlphaStar :cite:`vinyals2019grandmaster`等工作中,多智能体系统中优化得到一个好的策略往往不只需要强化学习算法,还需要其他学习方法如模仿学习等的辅助。比如从一些顶级人类玩家的游戏记录中形成有标签的训练样本,来预训练智能体。由于这些大规模游戏的复杂性,这往往是一个在训练前期快速提升智能体表现的有效方式。而对于整个学习系统而言,这就需要对不同学习范式进行结合,如合理地在模仿学习和强化学习之间进行切换等。这也使得大规模多智能体系统不单一是构建强化学习系统的问题,而需要许多其他学习机制和协调机制的配合实现。 -如 :numref:`ch12/ch12_marl_sys`所示,为一个分布式多智能体强化学习系统。图中的两个智能体可以类似扩展到多个智能体。每个智能体包含多个行动者(Actor)用于采样和学习者(Learner)用于更新模型,这些行动者和学习者可以并行处理来加速训练过程,具体方法可以参考单智能体分布式系统章节介绍的A3C和IMPALA架构。训练好的模型被统一存储和管理在模型存储器中,是否对各个智能体的模型分别存储取决于各个智能体是否对称。存储器中的模型可以被模型评估器用来打分,从而为下一步模型选择器做准备。模型选择器根据模型评估器或者元学习者(如PSRO算法 :cite:`lanctot2017unified`)以及均衡求解器等进行模型选择,并将选出的模型分发到各个智能体的行动者上。这一处理过程,我们称为联盟型管理(League-based Management)。对于与环境交互的部分,分布式系统可以通过一个推理服务器(Inference Server)对各个并行进程中的模型进行集中推理,将基于观察量(Observation)的动作(Action)发送给环境。环境部分也可以是并行的。推理服务器将采集到的交互轨迹发送给各个智能体进行模型训练。以上为一个分布式多智能体系统的例子,实际中根据不同的游戏类型和算法结构可能会有不同的设计。 +如 :numref:`ch12/ch12_marl_sys`所示,为一个分布式多智能体强化学习系统。图中的两个智能体可以类似扩展到多个智能体。每个智能体包含多个行动者(Actor)用于采样和学习者(Learner)用于更新模型,这些行动者和学习者可以并行处理来加速训练过程,具体方法可以参考单智能体分布式系统章节介绍的A3C和IMPALA架构。训练好的模型被统一存储和管理在模型存储器中,是否对各个智能体的模型统一存储取决于各个智能体是否对称——如果不对称,需要将模型分别存储。存储器中的模型可以被模型评估器用来打分,从而为下一步模型选择器做准备。模型选择器根据模型评估器或者元学习者(如PSRO算法 :cite:`lanctot2017unified`)以及均衡求解器等进行模型选择,并将选出的模型分发到各个智能体的行动者上。这一处理过程,我们称为联盟型管理(League-based Management)。对于与环境交互的部分,分布式系统可以通过一个推理服务器(Inference Server)对各个并行进程中的模型进行集中推理,将基于观察量(Observation)的动作(Action)发送给环境。环境部分也可以是并行的,对推理服务器传递来的动作进行并行处理后,返回观察量。推理服务器将采集到的交互轨迹发送给各个智能体进行模型训练。以上为一个分布式多智能体系统的例子,实际中根据不同的游戏类型和算法结构可能会有不同的设计。 ![分布式多智能体强化学习系统](../img/ch12/ch12-marl-sys.png) diff --git a/chapter_reinforcement_learning/rl_introduction.md b/chapter_reinforcement_learning/rl_introduction.md index 6ec0401..cf8b446 100644 --- a/chapter_reinforcement_learning/rl_introduction.md +++ b/chapter_reinforcement_learning/rl_introduction.md @@ -1,13 +1,17 @@ ## 强化学习介绍 -近年来,强化学习作为机器学习的一个分支受到越来越多的关注。从2013年起,DeepMind公司的研究人员就提出深度Q学习 :cite:`mnih2013playing`(Deep Q-learning)用于学习7个不同的电子游戏中对象的操作。自此以后,以DeepMind为首的科研机构推出了像AlphaGo下围棋这类的引人瞩目的强化学习成果,并在2016年与世界顶级围棋高手李世石的对战中取得胜利。自那以后,强化学习领域连续取得了一系列成就,如星际争霸游戏智能体AlphaStar、Dota 2游戏智能体OpenAI Five、多人零和博弈德州扑克的Pluribus、机器狗运动控制算法等。在这一系列科研成就的背后,是整个强化学习领域算法在这些年内快速迭代进步的结果,基于模拟器产生的大量数据使得对数据“饥饿”(Data Hungry)的深度神经网络能够表现出很好的拟合效果,从而将强化学习算法的能力充分发挥出来,在以上领域中达到或者超过人类专家的学习表现。目前,强化学习已经从电子游戏逐步走向更广阔的应用场景,如机器人控制、机械手灵巧操作、能源系统调度、网络负载分配、股票期货交易等一系列更加现实和富有意义的领域,对传统控制方法和启发式决策理论发起冲击。 +近年来,强化学习作为机器学习的一个分支受到越来越多的关注。2013 年 DeepMind 公司的研究人员提出了深度 Q 学习 :cite:`mnih2013playing`(Deep Q-learning),成功让 AI 从图像中学习玩电子游戏。自此以后,以 DeepMind 为首的科研机构推出了像 AlphaGo 围棋 AI 这类的引人瞩目的强化学习成果,并在 2016 年与世界顶级围棋高手李世石的对战中取得了胜利。自那以后,强化学习领域连续取得了一系列成就,如星际争霸游戏智能体 AlphaStar、Dota 2 游戏智能体 OpenAI Five、多人零和博弈德州扑克的 Pluribus、机器狗运动控制算法等。在这一系列科研成就的背后,是整个强化学习领域算法在这些年内快速迭代进步的结果,基于模拟器产生的大量数据使得对数据“饥饿”(Data Hungry)的深度神经网络能够表现出很好的拟合效果,从而将强化学习算法的能力充分发挥出来,在以上领域中达到或者超过人类专家的学习表现。目前,强化学习已经从电子游戏逐步走向更广阔的应用场景,如机器人控制、机械手灵巧操作、能源系统调度、网络负载分配、股票期货交易等一系列更加现实和富有意义的领域,对传统控制方法和启发式决策理论发起冲击。 ![强化学习框架](../img/ch12/ch12-rl.png) :width:`400px` :label:`ch12/ch12-rl-framework` -强化学习的核心是决策,即基于某个**状态**(State)下的**动作**(Action)的选择。进行这一决策的对象我们常称为**智能体**(Agent),而这一决策的影响将在**环境**(Environment)中体现。更具体地,不同的决策会影响环境的**状态转移**(State Transition),以及**奖励**(Reward)。以上过程可以抽象为 :numref:`ch12/ch12-rl-framework`所示,这是文献中最常见的强化学习的模型描述。举例来说,当人在玩某个电子游戏的时候,需要逐渐熟悉游戏的操作以取得更好的游戏结果,那么人从刚接触到这个游戏到逐步掌握游戏技巧的这个过程为一个类似于强化学习的过程。该游戏从开始后的任一时刻,会处于一个特定的状态,而人通过观察这个状态(如观察游戏机显示屏的图像)会获得一个**观察量**(Observation),并基于这个观察量做出一个操作动作,这一动作将改变这个游戏状态,使其转移到下一个状态,并且玩家可以知道当前动作的效果(如产生了一个正或负的分数)。这时玩家再基于下一个状态的观察量做出新的动作选择,周而复始,直到游戏结束。通过反复的操作和观察,人能够逐步掌握这个游戏的技巧,一个强化学习智能体也是如此。这里注意,有几个比较关键的问题:(1)观察量未必等于状态,而通常观察量是状态的函数,从状态到观察量的映射可能有一定的信息损失。对于观察量等于状态或者根据观察量能够完全恢复环境状态的情况,我们称为**完全可观测**(Fully Observable),否则我们称为**部分可观测**(Partially Observable)环境;(2)玩家的每个动作未必会产生立即反馈,某个动作可能在许多步之后才产生效果,强化学习模型允许这种延迟反馈的存在;(3)这种反馈对人的学习过程而言未必是个数字,但是我们对强化学习智能体所得到的反馈进行数学抽象,将其转变为一个数字,称为奖励值。奖励值可以是状态的函数,也可以是状态和动作的函数,依具体问题而定。奖励值的存在是强化学习问题的一个基本假设,也是现有强化学习算法训练智能体与监督式深度学习的一个主要区别。 +强化学习的核心是不断地与环境交互来优化策略从而提升奖励的过程,主要表现为基于某个**状态**(State)下的**动作**(Action)的选择。进行这一决策的对象我们常称为**智能体**(Agent),而这一决策的影响将在**环境**(Environment)中体现。更具体地,不同的决策会影响环境的**状态转移**(State Transition)和**奖励**(Reward)。以上状态转移是环境从当前状态转移到下一状态的函数,它可以是确定性也可以是随机性的。奖励是环境对智能体动作的反馈,通常是一个标量。以上过程可以抽象为 :numref:`ch12/ch12-rl-framework`所示,这是文献中最常见的强化学习的模型描述。 + +举例来说,当人在玩某个电子游戏的时候,需要逐渐熟悉游戏的操作以取得更好的游戏结果,那么人从刚接触到这个游戏到逐步掌握游戏技巧的这个过程为一个类似于强化学习的过程。该游戏从开始后的任一时刻,会处于一个特定的状态,而人通过观察这个状态会获得一个**观察量**(Observation)(如观察游戏机显示屏的图像),并基于这个观察量做出一个操作动作(如发射子弹),这一动作将改变这个游戏下一时刻的状态,使其转移到下一个状态(如把怪物打败了),并且玩家可以知道当前动作的效果(如产生了一个正或负的分数,怪物打败了则获得正分数)。这时玩家再基于下一个状态的观察量做出新的动作选择,周而复始,直到游戏结束。通过反复的操作和观察,人能够逐步掌握这个游戏的技巧,一个强化学习智能体也是如此。 + +这里注意,有几个比较关键的问题:一是观察量未必等于状态,而通常观察量是状态的函数,从状态到观察量的映射可能有一定的信息损失。对于观察量等于状态或者根据观察量能够完全恢复环境状态的情况,我们称为**完全可观测**(Fully Observable),否则我们称为**部分可观测**(Partially Observable)环境;二是玩家的每个动作未必会产生立即反馈,某个动作可能在许多步之后才产生效果,强化学习模型允许这种延迟反馈的存在;三是这种反馈对人的学习过程而言未必是个数字,但是我们对强化学习智能体所得到的反馈进行数学抽象,将其转变为一个数字,称为奖励值。奖励值可以是状态的函数,也可以是状态和动作的函数,依具体问题而定。奖励值的存在是强化学习问题的一个基本假设,也是现有强化学习与监督式学习的一个主要区别 强化学习的决策过程通常由一个马尔可夫决策过程(Markov Decision Process,MDP)(马尔可夫决策过程即一个后续状态只依赖当前状态和动作而不依赖于历史状态的函数)描述,可以用一个数组$(\mathcal{S}, \mathcal{A}, R, \mathcal{T}, \gamma)$来表示。$\mathcal{S}$和$\mathcal{A}$分别是状态空间和动作空间,$R$是奖励函数,$R(s,a)$: $\mathcal{S}\times \mathcal{A}\rightarrow \mathbb{R}$为对于当前状态$s\in\mathcal{S}$和当前动作$a\in\mathcal{A}$的奖励值。从当前状态和动作到下一个状态的状态转移概率定义为$\mathcal{T}(s^\prime|s,a)$: $\mathcal{S}\times\mathcal{A}\times\mathcal{S}\rightarrow \mathbb{R}_+$。$\gamma\in(0,1)$是奖励折扣因子(折扣因子可以乘到每个后续奖励值上,从而使无穷长序列有有限的奖励值之和)。强化学习的目标是最大化智能体的期望累计奖励值$\mathbb{E}[\sum_t \gamma^t r_t]$。 @@ -17,6 +21,7 @@ $$ \mathcal{T}(s_{t+1}|s_t) = \mathcal{T}(s_{t+1}|s_0, s_1, s_2, \dots, s_t) $$ -即当前状态转移只依赖于上一时刻状态,而不依赖于整个历史。这里的状态转移函数$\mathcal{T}$中省略了动作$a$,因为马尔可夫性质是独立于决策过程的环境转移过程的属性。 +即当前状态转移只依赖于上一时刻状态,而不依赖于整个历史。这里的状态转移函数$\mathcal{T}$中省略了动作$a$,马尔可夫性质是环境转移过程的属性,其独立于产生动作的决策过程。 基于马尔可夫性质,可以进一步推导出在某一时刻最优策略不依赖于整个决策历史,而只依赖于当前最新状态的结论。这一结论在强化学习算法设计中有着重要意义,它简化了最优策略的求解过程。 + diff --git a/chapter_reinforcement_learning/single_node_rl.md b/chapter_reinforcement_learning/single_node_rl.md index 30c9079..3d2a207 100644 --- a/chapter_reinforcement_learning/single_node_rl.md +++ b/chapter_reinforcement_learning/single_node_rl.md @@ -1,15 +1,21 @@ ## 单节点强化学习系统 -前面介绍了强化学习的基本知识和在系统层面的一般需求,这里我们介绍常见的单智能体强化学习系统中较为简单的一类,即单节点强化学习系统。这里,我们按照是否对模型训练和更新进行并行处理,将强化学习系统分为单节点和分布式强化学习系统。其中,单节点强化学习系统可以理解为只实例化一个类对象作为智能体,与环境交互进行采样和利用所采得的样本进行更新的过程分别视为这个类内的不同函数。除此之外的更为复杂的强化学习框架都可视为分布式强化学习系统。分布式强化学习系统的具体形式有很多,这也往往依赖于所实现的算法。从最简单的情况考虑,假设我们仍在同一个计算单元上实现算法,但是将强化学习的采样过程和更新过程实现为两个并行的进程,甚至各自实现为多个进程,以满足不同计算资源间的平衡。这时就需要进程间通信来协调采样和更新过程,这是一个最基础的分布式强化学习框架。更为复杂的情况是,整个算法的运行在多个计算设备上进行(如一个多机的计算集群),智能体的函数可能需要跨机跨进程间的通信来实现。对于多智能体系统,还需要同时对多个智能体的模型进行更新,则需要更为复杂的计算系统设计。我们将逐步介绍这些不同的系统内的实现机制。 +前面介绍了强化学习的基本知识,这里我们介绍常见的单智能体强化学习系统中较为简单的一类,即单节点强化学习系统,这里的节点是指一个用于模型更新的计算单元。我们按照是否对模型更新的过程做并行化处理,将强化学习系统分为单节点和分布式强化学习系统。其中,单节点强化学习系统可以理解为只实例化一个类对象作为智能体,与环境交互进行采样和利用所采得的样本进行更新的过程分别视为这个类内的不同函数。除此之外的更为复杂的强化学习框架都可视为分布式强化学习系统。 -我们先对单节点强化学习系统进行介绍。 -在这里,我们以RLzoo :cite:`ding2020efficient`为例,讲解一个单节点强化学习系统构建所需要的基本模块。如 :numref:`ch12/ch12-rlzoo`所示,是RLzoo中采用的一个典型的单节点强化学习系统,它包括几个基本的组成部分:神经网络、适配器、策略网络和价值网络、环境实例、模型学习器、经验回放缓存(Experience Replay Buffer)等。 +分布式强化学习系统的具体形式有很多,系统的形式也往往依赖于所实现的算法。从最简单的情况考虑,假设我们仍在同一个计算单元上实现算法,但是将强化学习的采样过程和更新过程实现为两个并行的进程,甚至各自实现为多个进程,以满足不同计算资源间的平衡。这时就需要进程间通信来协调采样和更新过程,这是一个最基础的分布式强化学习框架。更为复杂的情况是,整个算法的运行在多个计算设备上进行(如一个多机的计算集群),智能体的函数可能需要跨机跨进程间的通信来实现。对于多智能体系统,还需要同时对多个智能体的模型进行更新,则需要更为复杂的计算系统设计。我们将逐步介绍这些不同的系统内的实现机制。 -我们先对前三个,神经网络、适配器、策略网络和价值网络进行介绍。神经网络即一般深度学习中使用的神经网络,用于实现基于数据的函数拟合,特点是可以用梯度下降的方法更新。我们在图中简单列出常见的三类神经网络:全连接网络,卷积网络和循环网络。策略网络和价值网络是一般深度强化学习的常见组成部分,分别是对策略函数和价值函数的近似表示。策略网络即一个由参数化深度神经网络表示的动作策略,而价值网络为神经网络表示的状态价值(State-Value)或状态-动作价值(State-Action Value)函数。这里我们不妨称全连接网络,卷积网络和循环网络为一般神经网络,它们常作为基本构建模块而被用来搭建强化学习中的策略网络和价值网络。在RLzoo中,适配器则是为实现强化学习特定函数近似而选配一般神经网络的功能模块,每个适配器是一个根据网络输入输出格式决定的网络格式选择函数。如:numref:`ch12/ch12-rlzoo`所示,RLzoo在实现中使用了三个不类型的适配器来使得强化学习算法构建过程有自适应能力。首先,根据不同的观察量类型,强化学习智能体所用的神经网络头部会有不同的结构,这一选择可以由一个基于观察量的适配器来实现;其次,根据所采用的强化学习算法类型,相应的策略网络尾部需要有不同的输出类型,包括确定性策略和随机性策略,RLzoo中使用一个策略适配器来进行选择;最后,根据不同的动作输出,如离散型、连续型、类别型等,需要使用一个动作适配器来选择。介绍完这些,我们已经有了可用的策略网络和价值网络,这构成了强化学习智能体核心学习模块。除此之外,还需要一个学习器(Learner)来更新这些学习模块,更新的规则就是强化学习算法给出的损失函数。而要想实现学习模块的更新,最重要的是输入的学习数据,即智能体跟环境交互过程中所采集的样本。对于**离线**(Off-Policy)强化学习,这些样本通常被存储于一个称为经验回放缓存的地方,学习器在需要更新模型时从该缓存中采得一些样本来进行更新。这里说到的离线强化学习是强化学习算法中的一类,强化学习算法可以分为**在线**(On-Policy)强化学习和离线强化学习两类,按照某个特定判据。这个判据是,用于更新的模型和用于采样的模型是否为同一个,如果是,则称在线强化学习算法,否则为离线强化学习算法。因而,离线强化学习通常允许与环境交互的策略采集的样本被存储于一个较大的缓存内,从而允许在许久之后再从这个缓存中抽取样本对模型进行更新。而对于在线强化学习,这个“缓存”有时其实也是存在的,只不过它所存储的是非常近期内采集的数据,从而被更新模型和用于采样的模型可以近似认为是同一个。从而,这里我们简单表示RLzoo的强化学习系统统一包括这个经验回放缓存模块。有了以上策略和价值网络、经验回放缓存、适配器、学习器,我们就得到了RLzoo中一个单节点的强化学习智能体,将这个智能体与环境实例交互,并采集数据进行模型更新,我们就得到了一个完整的单节点强化学习系统。这里的环境实例化我们允许多个环境并行采样。 +我们先对单节点强化学习系统进行介绍。在这里,我们以RLzoo :cite:`ding2020efficient`为例,讲解一个单节点强化学习系统构建所需要的基本模块。如 :numref:`ch12/ch12-rlzoo`所示,是RLzoo算法库中采用的一个典型的单节点强化学习系统,它包括几个基本的组成部分:神经网络、适配器、策略网络和价值网络、环境实例、模型学习器、经验回放缓存(Experience Replay Buffer)等。 + +我们先对前三个,神经网络、适配器、策略网络和价值网络进行介绍。神经网络即一般深度学习中的神经网络,用于实现基于数据的函数拟合,我们在图中简单列出常见的三类神经网络:全连接网络,卷积网络和循环网络。策略网络和价值网络是一般深度强化学习的常见组成部分,策略网络即一个由深度神经网络参数化的策略表示,而价值网络为神经网络表示的状态价值(State-Value)或状态-动作价值(State-Action Value)函数。这里我们不妨称前三类神经网络为一般神经网络,策略网络和价值网络为强化学习特定网络,前者往往是后者的重要组成部分。在RLzoo中,适配器则是为实现强化学习特定网络而选配一般神经网络的功能模块。首先,根据不同的观察量类型,强化学习智能体所用的神经网络头部会有不同的结构,这一选择可以由一个基于观察量的适配器来实现;其次,根据所采用的强化学习算法类型,相应的策略网络尾部需要有不同的输出类型,包括确定性策略和随机性策略,RLzoo 中使用一个策略适配器来进行选择;最后,根据不同的动作输出,如离散型、连续型、类别型等,需要使用一个动作适配器来选择。:numref:`ch12/ch12-rlzoo`中我们统称这三个不类型的适配器为适配器。 + +介绍完这些,我们已经有了可用的策略网络和价值网络,这构成了强化学习智能体核心学习模块。除此之外,还需要一个学习器(Learner)来更新这些学习模块,更新的规则就是强化学习算法给出的损失函数。而要想实现学习模块的更新,最重要的是输入的学习数据,即智能体跟环境交互过程中所采集的样本。对于**离线**(Off-Policy)强化学习,这些样本通常被存储于一个称为经验回放缓存的地方,学习器在需要更新模型时从该缓存中采得一些样本来进行更新。这里说到的离线强化学习是强化学习算法中的一类,强化学习算法可以分为在线(On-Policy)强化学习和离线(Off-Policy)强化学习两类,按照某个特定判据。这个判据是,用于更新的模型和用于采样的模型是否为同一个,如果是,则称在线强化学习算法,否则为离线强化学习算法。因而,离线强化学习通常允许与环境交互所采集的样本被存储于一个较大的缓存内,从而允许在许久之后再从这个缓存中抽取样本对模型进行更新。而对于在线强化学习,这个“缓存”有时其实也是存在的,只不过它所存储的是非常近期内采集的数据,从而被更新模型和用于采样的模型可以近似认为是同一个。从而,这里我们简单表示 RLzoo 的强化学习系统统一包括这个经验回放缓存模块。有了以上策略和价值网络、经验回放缓存、适配器、学习器,我们就得到了 RLzoo 中一个单节点的强化学习智能体,将这个智能体与环境实例交互,并采集数据进行模型更新,我们就得到了一个完整的单节点强化学习系统。这里的环境实例化我们允许多个环境并行采样。 ![RLzoo算法库中使用的强化学习系统](../img/ch12/ch12-rlzoo.png) :width:`800px` :label:`ch12/ch12-rlzoo` -近来研究人员发现,强化学习算法领域的发展瓶颈,可能不仅在于算法本身,而在于智能体采集数据的模拟器的模拟速度。Isaac Gym :cite:`makoviychuk2021isaac`是Nvidia公司于2021年推出的基于GPU(Graphics Processing Unit)的模拟引擎,在单GPU上实现2-3倍于之前基于CPU(Central Processing Unit)的模拟器的运行速度。关于GPU上运行加速我们已经在章节5中有所介绍。之所以GPU模拟能够对强化学习任务实现显著的加速效果,除了GPU本身多核心的并行运算能力之外,还在于这省却了CPU与GPU之间的数据传输和通信时间。传统的强化学习环境,如OpenAI Gym(这是一个常用的强化学习基准测试环境)等,都是基于CPU进行的模拟计算,而深度学习方法的神经网络训练通常是在GPU(或TPU,Tensor Processing Unit)上进行的。从智能体与CPU上实例化的模拟环境交互过程所收集的数据样本,通常先暂时以CPU的数据格式存储,在使用的时候被转移到GPU上成为具有GPU数据类型的数据(如使用PyTorch时可通过tensor.to(device)的函数实现,只需将device设为“cuda”即可将一个类型为torch.Tensor的tensor转移到GPU上),然后来进行模型训练。同时,由于模型参数是以GPU上数据的类型存储的,调用模型进行前向传递的过程中也需要先将输入数据从CPU转移到GPU上,并且可能需要将模型输出的GPU数据再转移回CPU类型。这一系列冗余的数据转换操作都会显著增长模型学习的时间,并且也增加了算法实际使用过程中的工程量。Isaac Gym模拟器的设计从底层上解决了这一困难,由于模拟器和模型双双实现在GPU上,他们之间的数据通信不再需要通过CPU来实现,从而绕过了CPU与GPU数据双向传输这一问题,实现了对强化学习任务中模拟过程的特定加速。 \ No newline at end of file +近来研究人员发现,强化学习算法领域的发展瓶颈,可能不仅在于算法本身,而在于让智能体在其中采集数据的模拟器的模拟速度。Isaac Gym :cite:`makoviychuk2021isaac`是Nvidia公司于2021年推出的基于GPU(Graphics Processing Unit)的模拟引擎,在单GPU上实现2-3倍于之前基于CPU(Central Processing Unit)的模拟器的运行速度。关于 GPU上运行加速我们已经在章节 5 中有所介绍。之所以 GPU 模拟能够对强化学习任务实现显著的加 +速效果,除了 GPU 本身多核心的并行运算能力之外,还在于这省却了 CPU 与 GPU 之间的数据传输和通信时间。传统的强化学习环境,如 OpenAI Gym(这是一个常用的强化学习基准测试环境)等,都是基于 CPU 进行的模拟计算,而深度学习方法的神经网络训练通常是在 GPU 或TPU(Tensor Processing Unit) 上进行的。 + +从智能体与 CPU 上实例化的模拟环境交互过程所收集的数据样本,通常先暂时以 CPU 的数据格式存储,在使用的时候被转移到 GPU 上成为具有 GPU 数据类型的数据(如使用 PyTorch 时可通过tensor.to(device)的函数实现,只需将device设为“cuda”即可将一个类型为torch.Tensor的tensor转移到GPU上),然后来进行模型训练。同时,由于模型参数是以 GPU 上数据的类型存储的,调用模型进行前向传递的过程中也需要先将输入数据从 CPU 转移到 GPU 上,并且可能需要将模型输出的 GPU 数据再转移回 CPU 类型。这一系列冗余的数据转换操作都会显著增长模型学习的时间,并且也增加了算法实际使用过程中的工程量。Isaac Gym 模拟器的设计从模拟器下层运行硬件上解决了这一困难,由于模拟器和模型双双实现在 GPU 上,他们之间的数据通信不再需要通过 CPU 来实现,从而绕过了 CPU 与 GPU 数据双向传输这一问题,实现了对强化学习任务中模拟过程的特定加速。 \ No newline at end of file diff --git a/chapter_reinforcement_learning/summary.md b/chapter_reinforcement_learning/summary.md index da4e074..7973d43 100644 --- a/chapter_reinforcement_learning/summary.md +++ b/chapter_reinforcement_learning/summary.md @@ -1,6 +1,6 @@ ## 小结 -在这一章,我们简单介绍了强化学习的基本概念,包括单智能体和多智能体强化学习算法、单节点和分布式强化学习系统等,给读者对强化学习问题的基本认识。当前,强化学习是一个快速发展的深度学习分支,许多实际问题都有可能通过强化学习算法的进一步发展得到解决。另一方面,由于强化学习问题设置的特殊性(如需要与环境交互进行采样等),也使得相应算法对计算系统的要求更高:如何更好地平衡样本采集和策略训练过程?如何均衡CPU和GPU等不同计算硬件的能力?如何在大规模分布式系统上有效部署强化学习智能体?等等,都需要对计算机系统的设计和使用有更好的理解。 +在这一章,我们简单介绍了强化学习的基本概念,包括单智能体和多智能体强化学习算法、单节点和分布式强化学习系统等,给读者对强化学习问题的基本认识。当前,强化学习是一个快速发展的深度学习分支,许多实际问题都有可能通过强化学习算法的进一步发展得到解决。另一方面,由于强化学习问题设置的特殊性(如需要与环境交互进行采样等),也使得相应算法对计算系统的要求更高:如何更好地平衡样本采集和策略训练过程?如何均衡 CPU 和 GPU 等不同计算硬件的能力?如何在大规模分布式系统上有效部署强化学习智能体?都需要对计算机系统的设计和使用有更好的理解。 ## 参考文献 diff --git a/img/ch05/SIMD.png b/img/ch05/SIMD.png new file mode 100644 index 0000000..1d69f54 Binary files /dev/null and b/img/ch05/SIMD.png differ diff --git a/img/ch05/SIMT.png b/img/ch05/SIMT.png new file mode 100644 index 0000000..2fb95bd Binary files /dev/null and b/img/ch05/SIMT.png differ diff --git a/img/ch05/memory_architecture.png b/img/ch05/memory_architecture.png new file mode 100644 index 0000000..07fe2ad Binary files /dev/null and b/img/ch05/memory_architecture.png differ diff --git a/img/ch05/parallel_computing.png b/img/ch05/parallel_computing.png new file mode 100644 index 0000000..502f94f Binary files /dev/null and b/img/ch05/parallel_computing.png differ diff --git a/img/ch05/poly.png b/img/ch05/poly.png new file mode 100644 index 0000000..fe3357c Binary files /dev/null and b/img/ch05/poly.png differ diff --git a/img/ch05/poly_test.png b/img/ch05/poly_test.png new file mode 100644 index 0000000..9db3773 Binary files /dev/null and b/img/ch05/poly_test.png differ