简介
这是一个用于神经网络模型推理的跨平台库,支持各种硬件加速器。它通过图优化技术,如操作融合、常量折叠和分布式计算,显著提高了大语言模型的推理效率。
模型推理速度测试
以ResNet50模型为例
Nvidia GPU:
PyTorch 0.11262585956603288 VS ONNX 0.025557354614138602
CPU:
PyTorch 0.04904477499992936 VS ONNX 0.015337402000004658
onnx-simplifier工具使用:python -m onnxsim model_ori.onnx model_sim.onnx
onnx-simplifier算法解读
基本流程
simplify的基本流程如下:
- 配置优化器,判断是否跳过某些优化器,选择适当的优化传递机制
- 利用onnxruntime推理计算图,得到各个节点的输入输出的张量形状(确保每个节点的输入输出形状已知,避免简化过程中出现形状不匹配。为模型优化提供形状信息,帮助识别哪些节点是冗余的或可优化的。)
- 基于参数指定的优化方法调用onnxoptimizer进行ONNX模型的优化(如fuse_bn_into_conv)
- 基于get_constant_nodes函数获取常量OP,对ONNX模型的常量OP进行折叠
- 将得到的常量OP从图中移除(断开连线),同时将其节点参数构建为其他节点的输入参数,并清理图中的孤立节点
- 重复2-5步,反复应用形状推断、优化器和常量折叠,直到简化模型不再变化或达到最大迭代次数
- 简化完成后,检查简化后的模型,确保其结构有效
函数接口定义
def simplify(
model: Union[str, onnx.ModelProto],
check_n: int = 0,
perform_optimization: bool = True,
skip_fuse_bn: bool = False,
overwrite_input_shapes=None,
test_input_shapes=None,
skipped_optimizers: Optional[List[str]] = None,
skip_constant_folding=False,
skip_shape_inference=False,
input_data=None,
dynamic_input_shape: bool = False,
custom_lib: Optional[str] = None,
include_subgraph: bool = False,
unused_output: Optional[Sequence[str]] = None,
tensor_size_threshold: str = DEFAULT_TENSOR_SIZE_THRESHOLDHOLD,
mutable_initializer: bool = False,
*,
input_shapes=None,
) -> Tuple[onnx.ModelProto, bool]:
”“”
:param model: onnx ModelProto对象或文件路径
:param check_n:简化模型将通过随机输入检查' check_n '次
:param perform_optimization:是否在模型上运行onnx优化器
:param skip_fuse_bn:跳过fuse_bn_into_conv onnx优化器
:param overwrite_input_shapes:如果模型有动态输入形状,用户必须传递一个固定的输入形状
用于生成随机输入和检查等式。
:param test_input_shapes:如果模型有动态输入形状,用户必须传递一个固定的输入形状
用于生成随机输入和检查等式。
:param skipped_optimizers:跳过一些特定的onnx优化器
:param skip_constant_folding:跳过常量折叠
:param skip_shape_inference:跳过形状推断(有时形状推断会崩溃)
:param input_data:提供自定义输入数据,以便在需要时进行检查
:param dynamic_input_shape:已弃用。不再需要了。
:param custom_lib: onnxruntime自定义ops的共享库
:param include_subgraph:简化子图而不仅仅是主图
:param unused_output:将从模型中消除的未使用输出的名称
:param input_shapes:已弃用。请使用' overwrite_input_shapes '和/或' test_input_shapes '代替。
:return:一个元组(简化模型,success(True)或failed(False))
”“”
具体功能分析
OnnxSimplifier是一个用于简化onnx模型的工具,主要是拥有折叠常量(FoldConstant)的功能、自动调度OnnxOptimizer,最为重要而且核心的是FixedPointFn这个简化调度算法。
OnnxOptimizer是一个onnx官方的一个onnx模型优化库,内部包含很多模型简化/优化的功能。用户也可直接通过python/c++/c api执行调用。
FixedPointFn迭代优化函数基本原理:通过两个优化函数,反复迭代优化中得到了最终无法继续优化的最终模型。函数逻辑如下:
while (_max_iters-- > 0) {
if (google::protobuf::util::MessageDifferencer::Equals(y1, y2)){return y2}
y1 = f1(y2);
if (google::protobuf::util::MessageDifferencer::Equals(y1, y2)){return y1}
y2 = f2(y1);
}
直到y1=y2或达到最大迭代次数max_iter跳出循环
优化函数有二:
OptAndShape:FixedPointFn(std::function{InferShapes}, std::function{Optimize})
OptAndShape.f1:_InferShapes形状推导(可选,不使用的时候为Identity)
OptAndShape.f2:OptimizeFixed优化,调用了OnnxOptimizer函数库进行优化
OptAndShape.max_iters:默认是50(可通过ONNXSIM_FIXED_POINT_ITERS设置)
OptAndShapeAndFold:FixedPointFn(std::function{OptAndShape}, std::function{FoldConstant})
OptAndShapeAndFold.f1:OptAndShape
OptAndShapeAndFold.f2: FoldConstant函数(可选,不使用的时候为Identity)
FoldConstant具体来说:
- 基于GetConstantNodes函数得到所有的常量节点和非常量节点;(常量节点需同时判断是官方算子、是确定性的、不是QuantizeLinear(将浮点数张量转换为量化的整数张量)或DequantizeLinear(将量化的张量(即整数张量)还原为浮点数张量)、不含有子图、不会输出超过指定大小的张量、输入为常量)
- 使用onnxruntime进行一次推理(RunOp:在给定的 ONNX 模型上执行单个操作节点(`NodeProto`),并返回该操作节点的输出结果。),得到常量节点的值,并将常量节点的值及节点名构造成initializer,存入model中
- 清空model中所有的node
- 加入非常量节点
这样得到一个新的model,可以表示为常量的节点都转换为initializer
OptAndShapeAndFold.max_iters:默认是50
sim_model = OptAndShapeAndFold(model)得到最终的优化模型
onnxsim 本身只提供 constant folding/propagation(即消除结果恒为常量的算子)的能力,而图变换(即合并 conv 和 bn 等等)的能力是由 onnxsim 调用 onnx optimizer 的各种 pass 实现的。constant folding 和图变换同时使用时,很多隐藏的优化机会会被挖掘出来,这也是 onnxsim 优化效果出色的原因之一。
onnx-optimizer算法解读
Pass
含义:所有优化项的基类,每个优化项必须拥有唯一的名字,用来注册和管理。
优化类型(PassType):
- Fuse: 会融合算子的优化项
- Nop: 会移除无用的算子的优化项
- Separate:会分开某种形式的算子
- Immutable:不可变优化
- Replace: 会进行节点替换的优化项
- Other: 其他
返回类型(PassAnalysisType):
- Empty:空分析结果
- CountBased:基于计数的分析结果
效率(PassEfficiency):
- Partial:部分有效,不能保证连续两次运行的结果与运行一次相同
- Complete:完全有效,连续两次运行的结果等于一次运行的结果
优化目标(PassOptimizationType):
- None:不优化
- Compute: 针对计算优化
- Memory: 针对内存优化
- ComputeMemory: 针对计算和内存优化
- Stability: 优化稳定性(如log-sum-exp)
节点销毁类型(NodeDestroyType):
- DestroyZero:不销毁节点。
- DestroyOne:等同于调用 destroyCurrent() 一次。
- DestroyTwo:等同于调用 destroyCurrent() 两次。
具体pass实现(共39个):
- adjust_add:交换add第一个常量输入到第二位置上
- adjust_slice_and_matmul:调整slice和matmul之间的顺序,以便优化:Y = Matmul(Slice(data, start, end, axes) ,rhs) ==> Y = Slice(Matmul(data, rhs), start, end, axes)
- eliminate_common_subexpression:消除掉那些属性/输入一致的节点,也就是公共子表达式
- eliminate_deadend:移除掉output没有连接到其他节点的node
- eliminate_duplicate_initializer:移除掉重复的initializer(可能导致意外)
- eliminate_identity:移除掉Identity node
- eliminate_if_with_const_cond:消除输入条件cond为常量的if算子
- eliminate_nop_cast:消除那些目标输出数据类型等于输入数据类型的cast算子
- eliminate_nop_concat:消除输入个数为1的Concat算子
- eliminate_nop_dropout:消除那些ratio=0.0的dropout算子
- eliminate_nop_expand:消除expand_dim可以广播到input_dim的Expand算子
- eliminate_nop_flatten:消除那些对shape无影响的Flatten算子
- eliminate_nop_monotone_argmax:消除那些正相关的激活函数到argmax函数上,减少计算
- eliminate_nop_pad:消除pads=0的Pad算子
- eliminate_nop_reshape:消除掉reshape dim == input_dim的reshape算子
- eliminate_nop_split:消除输出个数为1、input_dim[axis]=split[0]的Split算子
- eliminate_nop_transpose:消除掉不起作用的transpose算子
- eliminate_nop_with_unit:消除掉同0并的And、同1乘的Mul、同0或的Or、同0加的Add、减0的Sub、除1的Div、方1的Pow、 无效的Concat
- eliminate_shape_gather:融合掉indices=[indices_val,]、前面的节点是Shape的Gather算子
- eliminate_shape_op:融合掉可以直接获取input_shape的Shape算子
- eliminate_slice_after_shape:融合掉前面的节点是Shape的Slice算子
- eliminate_unused_initializer:移除掉不被使用的initializer
- extract_constant_to_initializer:移除Constant算子,放置到Initializer内
- fuse_add_bias_into_conv:融合掉前面的节点是Conv2d、且input[1]是常量可配合dim的Add算子
- fuse_bn_into_conv:融合BN算子到Conv2d上,同时修改Conv2d的权重
- fuse_concat_into_reshape:融合reshape的shape输入的concat/cast算子,变成constant shape输入
- fuse_consecutive_concats:融合前面的axis相同的Concat到该Concat节点上
- fuse_consecutive_log_softmax:融合softmax+log成为LogSoftmax算子
- fuse_consecutive_reduce_unsqueeze:当前面的Reduce算子的axes=Unsqueeze_axes、keepdims=0时,融合Unsqueeze算子到Reduce算子上
- fuse_consecutive_slices:融合掉axes没有交集的连续的slice算子,合并为一个Slice
- fuse_consecutive_squeezes:合并多个连续的Squeeze算子成为一个Squeeze算子
- fuse_consecutive_transposes:合并多个连续的Transpose算子成为一个Transpose算子
- fuse_consecutive_unsqueezes:融合连续的Unsqueezes算子
- fuse_matmul_add_bias_into_gemm:合并MatMul+Add成为一个Gemm算子
- fuse_pad_into_conv:合并Pad+Conv成为一个Conv算子,Pad操作合并到了Conv上
- fuse_pad_into_pool:合并Pad+AveragePool/MaxPool成为一个Pool算子
- fuse_qkv:合并qkv计算的三个matmul为只有一个matmul: A = matmul(X, Q), B = matmul(X, K), C = matmul(X, V) ==> A,B,C = split(matmul(X, concat(Q,K,V)))
- fuse_transpose_into_gemm:融合前面的Transpose操作反转Gemm的transA/transB参数,从而融合掉transpose算子
- replace_einsum_with_matmul:满足条件的einsum(爱因斯坦求和约定)变成matmul操作:"bhij,bhjd->bhid"变成matmul; "bhid,bhjd→bhij"变成transpose+matmul操作