计算图优化架构
本文将会介绍推理引擎转换中的图优化模块,该模块负责实现计算图中的各种优化操作,包括算子融合、布局转换、算子替换和内存优化等,以提高模型的推理效果。计算图是一种表示和执行数学运算的数据结构,在机器学习和深度学习中,模型的训练和推理过程通常会被表示成一个复杂的计算图,其中节点代表运算操作,边代表数据(通常是张量)在操作之间的流动。
计算图优化是一种重要的技术,主要目标是提高计算效率和减少内存占用,通常由 AI 框架的编译器自动完成,通过优化,可以降低模型的运行成本,加快运行速度,提高模型的运行效率,尤其在资源有限的设备上,优化能显著提高模型的运行效率和性能.
挑战与架构
离线模块的挑战
首先整体看下在离线优化模块中的挑战和架构,在最开始第一节内容的时候其实已经跟大家详细的普及过,优化模块的挑战主要由以下几部分组成:
-
结构冗余:神经网络模型结构中的无效计算节点、重复的计算子图、相同的结构模块,可以在保留相同计算图语义情况下无损去除的冗余类型;
-
精度冗余:推理引擎数据单元是张量,一般为 FP32 浮点数,FP32 表示的特征范围在某些场景存在冗余,可压缩到 FP16/INT8 甚至更低;数据中可能存大量 0 或者重复数据。
-
算法冗余:算子或者 Kernel 层面的实现算法本身存在计算冗余,比如均值模糊的滑窗与拉普拉斯的滑窗实现方式相同。
-
读写冗余:在一些计算场景重复读写内存,或者内存访问不连续导致不能充分利用硬件缓存,产生多余的内存传输。
离线优化的方案
针对每一种冗余,我们在离线优化模块都是有对应的方式处理的:
- 针对于结构冗余:一般会对计算图进行优化,例如算子融合、算子替换、常量折叠等。
算子融合(Operator Fusion):算子融合是指在计算图中,将多个相邻的算子(operations)融合成一个新的算子。这样可以减少运算过程中的数据传输和临时存储,从而提高计算效率。例如,如果有两个连续的矩阵乘法操作,可以将它们融合为一个新的操作,从而减少一次数据读写。这在 GPU 等并行计算设备上特别有用,因为它们的数据传输成本相对较高。
算子替换(Operator Substitution):算子替换是指在计算图中,用一个效率更高的算子替换原有的算子。例如,如果一个算子是通过多个基础操作组成的,那么可能存在一个复杂但效率更高的算子可以替换它。这样可以减少计算的复杂性,提高计算效率。
常量折叠(Constant Folding):常量折叠是指在计算图的优化过程中,预先计算出所有可以确定的常量表达式的结果,然后用这个结果替换原有的表达式。这样可以减少运行时的计算量。例如,如果计算图中有一个操作是3*4
,那么在优化过程中,可以将这个操作替换为12
。
- 针对于精度冗余:一般会对算子进行优化,例如量化、稀疏化、低秩近似等。
量化(Quantization):量化是一种将浮点数转换为定点数或更低比特宽度的整数的方法,从而减少模型的存储和计算需求。量化可以分为静态量化和动态量化。静态量化是在模型训练后进行的,需要额外的校准步骤来确定量化范围;动态量化则是在模型运行时进行的,不需要额外的校准步骤。量化能够显著减小模型的大小,并提高推理速度,但可能会带来一些精度损失。
稀疏化(Sparsity):稀疏化是一种将模型中的一部分权重设为零的方法,从而减少模型的有效参数数量。稀疏化可以通过在训练过程中添加 L1 正则化或使用专门的稀疏训练算法来实现。稀疏化后的模型可以通过专门的稀疏矩阵运算库进行高效的推理。
低秩近似(Low-rank Approximation):低秩近似是一种将大的权重矩阵近似为两个小的矩阵乘积的方法,从而减少模型的参数数量。这种方法通常使用奇异值分解(SVD)或其他矩阵分解方法来实现。低秩近似能够显著减小模型的大小,并提高推理速度,但可能会带来一些精度损失。
-
针对于算法冗余:一般会统一算子/计算图的表达,例如 kernel 提升泛化性等。
Kernel 提升泛化性是指通过设计和优化 Kernel 函数,使得它能够适应更多类型的数据和任务,从而提高算子或计算图的泛化能力。例如多尺度 kernel、深度可分离卷积等方法。 -
针对于读写冗余:一般会通过数据排布的优化和内存分配的优化进行解决。
数据排布的优化:数据排布的优化主要是根据计算的访问模式和硬件的内存层次结构,来选择一个合适的数据排布方式。例如,在 CPU 上,为了利用缓存的局部性,可以将经常一起访问的数据放在一起;在 GPU 上,为了避免内存访问的冲突,可以将数据按照一定的模式分布在不同的内存通道上。此外,数据的排布方式也可以影响向量化(vectorization)和并行化(parallelization)的效果。
- 内存分配的优化:内存分配的优化主要是通过合理的内存管理策略,来减少内存的分配和回收开销。例如,可以使用内存池(memory pool)来管理内存,将经常使用的内存块预先分配好,然后在需要时直接从内存池中获取,避免频繁的内存分配和回收操作。此外,也可以使用一些高级的内存管理技术,如垃圾回收(garbage collection)和引用计数(reference counting)等。