三. TensorRT基础入门-onnx-graph-surgeon

发布于:2024-05-20 ⋅ 阅读:(307) ⋅ 点赞:(0)

前言

自动驾驶之心推出的 《CUDA与TensorRT部署实战课程》,链接。记录下个人学习笔记,仅供自己参考

本次课程我们来学习课程第三章—TensorRT 基础入门,一起来学习 onnx-graph-surgeon

课程大纲可以看下面的思维导图

在这里插入图片描述

0. 简述

本小节目标:学习使用 onnx-surgeon,比较与 onnx.helper 的区别,学习快速修改 onnx 以及替换算子/创建算子的技巧

这节我们学习第三章节第七小节—onnx-graph-surgeon,之前我们是利用 onnx.helper 模块来创建或者修改 onnx,在 TensorRT/tools 下面其实提供了一个更方便的 onnx 工具包 onnx_graphsurgeon

这个小节我们就来学习 onnx_graphsurgeon,并学习它与 onnx.helper 的区别,学会将复杂的 onnx 拆分成子图

1. 执行一下我们的python程序

源代码获取地址https://github.com/kalfazed/tensorrt_starter

这个小节的案例主要是 3.5-onnxsurgeon,如下所示:

在这里插入图片描述

3.5-onnxsurgeon

我们会结合开源的 swin-tiny.onnx 来学习使用 onnx_graphsurgeon,学会修改 LayerNormalization、Multi-Head-Attention 的子图

2. onnx-graph-surgeon

我们先来看下 onnx-graph-surgeon 是什么,onnx-graph-surgeon 是一个创建和修改 onnx 的工具,它可以在 TensorRT/tools 中安装,它的主要特性:

  • 更加方便地添加/修改 onnx 节点
  • 更加方便地修改子图
  • 更加方便地替换算子
  • 底层一般是用的 onnx.helper,但是给做了一些封装

我们可以使用如下指令安装:

python3 -m pip install onnx_graphsurgeon --index-url https://pypi.ngc.nvidia.com

比如我们可以利用它将 min-max 替换成 clip 算子:

在这里插入图片描述

在这里插入图片描述

min-max->clip

也可以用它将一系列复杂的 LayerNorm 计算替换成一个 LN 算子:

在这里插入图片描述

在这里插入图片描述

LayerNorm->LN

Note:LayerNorm 计算替换成 LN 是 TensorRT-8.6 以前的做法,TensorRT-8.6 之后就直接开始支持 LayerNorm 了

下面是 ChatGPT 给出的关于 onnx_graphsurgeon 模块的用途:

onnx-graphsurgeon 是 NVIDIA 的 TensorRT 工具包中一个用于操作 ONNX 图的模块。它允许用户以更高层次的抽象来修改和优化 ONNX 模型的计算图。以下是 onnx-graphsurgeon 的主要功能和用途:

  • 图结构修改:可以添加、删除或替换图中的节点和边,从而对计算图进行各种修改。
  • 图优化:通过合并节点、删除冗余节点等方式来优化计算图,从而提升模型的推理性能。
  • 节点和张量操作:能够轻松地操作节点和张量,包括改变节点类型、修改节点属性以及调整张量形状等。
  • 子图提取与合并:支持从图中提取子图或将多个子图合并,从而灵活地管理复杂的模型。
  • 模型转换:可以将修改后的计算图转换回 ONNX 格式,从而便于与其他工具和框架进行集成。
  • 调试与验证:提供工具来调试和验证图的结构和节点,确保修改后的图仍然有效并且能够正确运行。

onnx-graphsurgeon 适用于需要对 ONNX 模型进行深度修改和优化的场景,特别是在使用 TensorRT 进行模型加速时非常有用。

3. onnx-surgeon vs onnx.helper

我们在进入案例代码之前先看下 onnx_graphsurgeon 和 onnx.helper 有什么不同:

onnx 中的 IR 表示:

  • ModelProto:描述的是整个模型的信息
    • GraphProto:描述的是整个网络的信息
      • NodeProto:描述的是各个计算节点,比如 conv,linear
      • TensorProto:描述的是 tensor 的信息,主要包括权重
      • ValueInfoProto:描述的是 input/output 信息

onnx_graphsurgeon(gs)中的 IR 表示:

  • Tensor:有两种类型
    • Variable:主要就是那些不到推理不知道的变量
    • Constant:不用推理时,而在推理前就知道的变量
  • Node:跟 onnx 中的 NodeProto 差不多
  • Graph:跟 onnx 中的 GraphProto 差不多

gs 帮助我们隐藏了很多信息,例如 node 的属性以前使用 AttributeProto 保存,但是 gs 中统一用 dict 来保存

onnx-graphsurgeononnx.helper 模块在功能和使用场景上有明显的区别:(from ChatGPT)

onnx-graphsurgeon

  • 高层次抽象:提供更高层次的 API,便于用户进行复杂的图结构修改和优化。
  • 节点和图的高级操作:支持更灵活的节点操作、图重构和优化。
  • 图优化和调试:内置了许多用于图优化和调试的功能,特别适合与 TensorRT 集成。
  • 设计目标:主要设计用于深度修改和优化 ONNX 图,以提高模型的性能和可操作性。

onnx.helper

  • 基本功能:提供了创建和操作 ONNX 图基本元素(如节点、模型、图、张量)的辅助函数。
  • 低层次操作:API 更基础,主要用于构建和操作基本图结构。
  • 模型构建:适合用来创建和初始化 ONNX 模型,定义节点和图的基本元素。
  • 设计目标:旨在简化 ONNX 模型的创建和初始化过程,帮助用户快速构建基本的 ONNX 模型。

具体区别

  • 抽象层次

    • onnx-graphsurgeon 提供更高层次的操作接口,更适合于复杂的图优化和修改。
    • onnx.helper 更侧重于基础的创建和操作,适合于构建和初始化模型。
  • 功能侧重

    • onnx-graphsurgeon 侧重于图的优化和高级操作,适合需要深度修改和优化的场景。
    • onnx.helper 主要用于定义节点、创建图和模型,侧重于基础功能。

使用原生的 onnx.helper 创建 onnx 的代码如下:

在这里插入图片描述

使用原生的 gs 创建 onnx 的代码如下:

在这里插入图片描述

可以看到使用 gs 来创建 onnx 比 onnx.helper 要简单不少

gs 还可以自定义一些函数去创建 onnx 使整个 onnx 的创建更加方便,如下图所示:

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

这个类似于 onnx 中 symbolic 符号函数来注册算子一样,我们完全可以自己创建一些算子在这里使用

此外 gs 还可以方便我们把整个网络中的一些子图给“挖”出来,以此来分析细节,一般配合 polygraphy 使用,去寻找量化掉精度严重的子图

Notepolygraphy 是 TensorRT用来分析模型的一个非常非常重要的一个软件,会在后面的章节详细展开讲

比如现在有一个 swin-transformer.onnx 如下图所示:

在这里插入图片描述

我们可以使用如下代码将其中的 LayerNorm 部分给挖出来:

在这里插入图片描述

在这里插入图片描述

也可以使用如下代码将其中的 MHSA 部分给挖出来:

在这里插入图片描述

在这里插入图片描述

最后 gs 中最重要的一个特点在于我们可以使用 gs 来替换算子或者创建算子,这个会直接跟后面的 TensorRT plugin 绑定,实现算子的加速或者不兼容算子的实现

比如使用如下代码替换 LayerNormal:

在这里插入图片描述

在这里插入图片描述

修改前的 onnx 如下图所示:

在这里插入图片描述

修改后的 onnx 如下图所示:

在这里插入图片描述

4. 案例代码分析

下面我们进入代码中看下,我们先看 gs_create_conv.py 案例,代码如下:

import onnx_graphsurgeon as gs
import numpy as np
import onnx

# onnx_graph_surgeon(gs)中的IR会有以下三种结构
# Tensor
#    -- 有两种类型
#       -- Variable:  主要就是那些不到推理不知道的变量
#       -- Constant:  不用推理时,而在推理前就知道的变量
# Node
#    -- 跟onnx中的NodeProto差不多
# Graph
#    -- 跟onnx中的GraphProto差不多

def main() -> None:
    input = gs.Variable(
            name  = "input0",
            dtype = np.float32,
            shape = (1, 3, 224, 224))

    weight = gs.Constant(
            name  = "conv1.weight",
            values = np.random.randn(5, 3, 3, 3))

    bias   = gs.Constant(
            name  = "conv1.bias",
            values = np.random.randn(5))
    
    output = gs.Variable(
            name  = "output0",
            dtype = np.float32,
            shape = (1, 5, 224, 224))

    node = gs.Node(
            op      = "Conv",
            inputs  = [input, weight, bias],
            outputs = [output],
            attrs   = {"pads":[1, 1, 1, 1]})

    graph = gs.Graph(
            nodes   = [node],
            inputs  = [input],
            outputs = [output])

    model = gs.export_onnx(graph)

    onnx.save(model, "../models/sample-conv.onnx")



# 使用onnx.helper创建一个最基本的ConvNet
#         input (ch=3, h=64, w=64)
#           |
#          Conv (in_ch=3, out_ch=32, kernel=3, pads=1)
#           |
#         output (ch=5, h=64, w=64)

if __name__ == "__main__":
    main()

这段代码展示了使用 onnx_graphsurgeon 库创建了一个简单的 ONNX 模型,包含一个卷积层,可以看到相比于之前的 onnx.helper 模块创建 ONNX 要简单不少

导出的 ONNX 如下图所示:

在这里插入图片描述

我们再看下一个案例 gs_create_complicate_graph.py,代码如下所示:

import onnx_graphsurgeon as gs
import numpy as np
import onnx

#####################在graph注册调用的函数########################
@gs.Graph.register()
def add(self, a, b):
    return self.layer(op="Add", inputs=[a, b], outputs=["add_out_gs"])

@gs.Graph.register()
def mul(self, a, b):
    return self.layer(op="Mul", inputs=[a, b], outputs=["mul_out_gs"])

@gs.Graph.register()
def gemm(self, a, b, trans_a=False, trans_b=False):
    attrs = {"transA": int(trans_a), "transB": int(trans_b)}
    return self.layer(op="Gemm", inputs=[a, b], outputs=["gemm_out_gs"], attrs=attrs)

@gs.Graph.register()
def relu(self, a):
    return self.layer(op="Relu", inputs=[a], outputs=["act_out_gs"])


#####################通过注册的函数进行创建网络########################
#          input (64, 64)
#            |
#           gemm (constant tensor A(64, 32))
#            |
#           add  (constant tensor B(64, 32))
#            |
#           relu
#            |
#           mul  (constant tensor C(64, 32))
#            |
#           add  (constant tensor D(64, 32))

# 初始化网络的opset
graph    = gs.Graph(opset=12)

# 初始化网络需要用的参数
consA    = gs.Constant(name="consA", values=np.random.randn(64, 32))
consB    = gs.Constant(name="consB", values=np.random.randn(64, 32))
consC    = gs.Constant(name="consC", values=np.random.randn(64, 32))
consD    = gs.Constant(name="consD", values=np.random.randn(64, 32))
input0   = gs.Variable(name="input0", dtype=np.float32, shape=(64, 64))

# 设计网络架构
gemm0    = graph.gemm(input0, consA, trans_b=True)
relu0    = graph.relu(*graph.add(*gemm0, consB))
mul0     = graph.mul(*relu0, consC)
output0  = graph.add(*mul0, consD)

# 设置网络的输入输出
graph.inputs = [input0]
graph.outputs = output0

for out in graph.outputs:
    out.dtype = np.float32

# 保存模型
onnx.save(gs.export_onnx(graph), "../models/sample-complicated-graph.onnx")

这段代码使用 onnx_graphsurgeon 创建了一个更复杂的 ONNX 模型,展示了如何通过自定义的注册函数简化图层操作。

导出的 ONNX 如下图所示:

在这里插入图片描述

我们再接着看下一个案例 gs_create_subgraph.py,代码如下所示:

import onnx_graphsurgeon as gs
import numpy as np
import onnx

def load_model(model : onnx.ModelProto):
    graph = gs.import_onnx(model)
    print(graph.inputs)
    print(graph.outputs)

def main() -> None:
    model = onnx.load("../models/swin_tiny_patch4_window7_224.onnx")

    graph = gs.import_onnx(model)
    tensors = graph.tensors()

    # LayerNorm部分 
    print(tensors["/backbone/patch_embed/Transpose_output_0"])  # LN的输入:  1 x 3136 x 96
    print(tensors["/backbone/patch_embed/norm/Div_output_0"])   # LN的输出:  1 x 3136 x 96
    graph.inputs = [
            tensors["/backbone/patch_embed/Transpose_output_0"].to_variable(dtype=np.float32, shape=(1, 3136, 96))]
    graph.outputs = [
            tensors["/backbone/patch_embed/norm/Div_output_0"].to_variable(dtype=np.float32, shape=(1, 3136, 96))]
    graph.cleanup()
    onnx.save(gs.export_onnx(graph), "../models/swin-subgraph-LN.onnx")

    # MHSA部分
    graph = gs.import_onnx(model)
    tensors = graph.tensors()
    print(tensors["/backbone/layers.0/blocks.0/Reshape_3_output_0"])          # MHSA输入matmul:      64 x 49 x 96
    print(tensors["onnx::MatMul_2233"])                                       # MHSA输入matmul的权重: 96 x 288
    print(tensors["onnx::MatMul_2249"])                                       # MHSA输出matmul的权重: 96 x 96
    print(tensors["/backbone/layers.0/blocks.0/attn/proj/MatMul_output_0"])   # MHSA输出:             64 x 49 x 96
    graph.inputs = [
            tensors["/backbone/layers.0/blocks.0/Reshape_3_output_0"].to_variable(dtype=np.float32, shape=(64, 49, 96))]
    graph.outputs = [
            tensors["/backbone/layers.0/blocks.0/attn/proj/MatMul_output_0"].to_variable(dtype=np.float32, shape=(64, 49, 96))]
    graph.cleanup()
    onnx.save(gs.export_onnx(graph), "../models/swin-subgraph-MSHA.onnx")

# 我们想把swin中LayerNorm中的这一部分单独拿出来
if __name__ == "__main__":
    main()

这段代码使用 onnx_graphsurgeon 提取 Swin Transformer 模型中的 LayerNorm 和 MHSA 子图,具体步骤包括:加载完整模型后,通过打印和选择相关张量,设置 LayerNorm 的输入和输出,然后清理图结构并保存为 swin-subgraph-LN.onnx;同样地,为 MHSA 设置输入和输出,清理图结构后保存为 swin-subgraph-MSHA.onnx,实现了对特定子图的精准提取和导出。

swin-transformer 是一个基于 Transformer 的分类器,我们会在第六章节跟大家详细讲解,这里我们重点并不是要去理解 swin-transformer 的架构,而是要看如何把 ONNX 中的某个部分给截取出来,这个案例我们需要把 LayerNorm 和 self-attention 部分给截取出来

输出如下图所示:

在这里插入图片描述

导出来的 LayerNorm 子图 ONNX 如下图所示:

在这里插入图片描述

导出来的 MSHA 子图 ONNX 如下图所示:

在这里插入图片描述

我们再看后面几个案例,先看 gs_replace_op.py,代码如下所示:

import onnx_graphsurgeon as gs
import numpy as np
import onnx
import onnxruntime
import torch

#####################在graph注册调用的函数########################
@gs.Graph.register()
def min(self, *args):
    return self.layer(op="Min", inputs=args, outputs=["min_output"])

@gs.Graph.register()
def max(self, *args):
    return self.layer(op="Max", inputs=args, outputs=["max_output"])

@gs.Graph.register()
def identity(self, a):
    return self.layer(op="Identity", inputs=[a], outputs=["identity_output"])

@gs.Graph.register()
def clip(self, inputs, outputs):
    return self.layer(op="Clip", inputs=inputs, outputs=outputs)


#####################通过注册的函数进行创建网络########################
#          input (5, 5)
#            |
#         identity 
#            |
#           min  
#            |
#           max
#            |
#         identity  
#            |
#          output (5, 5)
def create_onnx_graph():
    # 初始化网络的opset
    graph    = gs.Graph(opset=12)

    # 初始化网络需要用的参数
    min_val  = np.array(0, dtype=np.float32)
    max_val  = np.array(1, dtype=np.float32)
    input0   = gs.Variable(name="input0", dtype=np.float32, shape=(5, 5))

    # 设计网络架构
    identity0 = graph.identity(input0)
    min0      = graph.min(*identity0, max_val)
    max0      = graph.max(*min0, min_val)
    output0   = graph.identity(*max0)

    # 设置网络的输入输出
    graph.inputs = [input0]
    graph.outputs = output0

    # 设置网络的输出的数据类型
    for out in graph.outputs:
        out.dtype = np.float32

    # 保存模型
    model = gs.export_onnx(graph)
    model.ir_version = 9  # 设置 IR 版本为 9
    onnx.save(model, "../models/sample-minmax.onnx")

#####################通过注册的clip算子替换网络节点####################
#          input (5, 5)
#            |
#         identity 
#            |
#           clip
#            |
#         identity  
#            |
#          output (5, 5)
def change_onnx_graph():
    graph = gs.import_onnx(onnx.load_model('../models/sample-minmax.onnx'))
    tensors = graph.tensors()

    inputs = [tensors["identity_output_0"], 
              tensors["onnx_graphsurgeon_constant_5"],
              tensors["onnx_graphsurgeon_constant_2"]]

    outputs = [tensors["max_output_6"]]
    
    # 因为要替换子网,所以需要把子网和周围的所有节点都断开联系
    for item in inputs:
        # print(item.outputs)
        item.outputs.clear()

    for item in outputs:
        # print(item.inputs)
        item.inputs.clear()

    # 通过注册的clip,重新把断开的联系链接起来
    graph.clip(inputs, outputs)

    # 删除所有额外的节点
    graph.cleanup()

    model = gs.export_onnx(graph)
    model.ir_version = 9  # 设置 IR 版本为 9
    onnx.save(model, "../models/sample-minmax-to-clip.onnx")

#####################验证模型##########################################
def validate_onnx_graph(input, path):
    sess   = onnxruntime.InferenceSession(path)
    output = sess.run(None, {'input0': input.numpy()})

    print("input is \n", input)
    print("output is \n", output)

def main() -> None:
    input  = torch.Tensor(5, 5).uniform_(-1, 1)

    # 创建一个minmax的网络
    create_onnx_graph()

    # 通过onnxruntime确认导出onnx是否正确生成
    print("\nBefore modification:")
    validate_onnx_graph(input, "../models/sample-minmax.onnx")

    # 将minmax网络修改成clip网络
    change_onnx_graph()

    # 确认网络修改的结构是否正确
    print("\nAfter modification:")
    validate_onnx_graph(input, "../models/sample-minmax-to-clip.onnx")


if __name__ == "__main__":
    main()

这段代码使用 onnx_graphsurgeon 库来操作和修改 ONNX 模型。它定义了一个简单的神经网络架构,其中包括一个通过最小值和最大值节点的流程,然后将这个网络转换为一个使用裁剪 (clip) 操作的网络。

值得注意的是上述代码与源码有些区别,博主进行了简单修改,将 model 的 IR 版本设置成了 9,这是因为博主在执行的过程中遇到了如下问题:

在这里插入图片描述

错误信息指出 “Unsupported model IR version: 10, max supported IR version: 9”,意味着模型的 IR 版本是 10,但是当前 onnxruntime 环境只支持到 IR 版本 9

博主尝试使用如下指令来更新 onnxruntime:

pip install --upgrade onnxruntime

更新后的 onnxruntime 版本是 1.17.3,执行后仍然出现如上的问题,因此博主将代码中导出的 model 的 IR 版本设置为了 9,一切输出正常

执行后输出如下:

在这里插入图片描述

可以看到 clip 替换 min、max 算子的前后输出基本一致,说明替换没问题

导出的 min-max.onnx 模型如下图所示:

在这里插入图片描述

替换后的 clip.onnx 模型如下图所示:

在这里插入图片描述

我们再看案例 gs_replace_LN.py,代码如下所示:

import onnx_graphsurgeon as gs
import numpy as np
import onnx
import onnxsim
import onnxruntime
import torch
import torch.nn as nn


#####################在graph注册调用的函数########################
@gs.Graph.register()
def identity(self, inputs, outputs):
    return self.layer(op="Identity", inputs=inputs, outputs=outputs)

@gs.Graph.register()
def layerNorm(self, inputs, outputs, axis, epsilon):
    attrs = {'axis': np.int64(axis), 'epsilon': float(epsilon)}
    return self.layer(op="LayerNormalization", inputs=inputs, outputs=outputs, attrs=attrs)

@gs.Graph.register()
def layerNorm_default(self, inputs, outputs):
    return self.layer(op="LayerNormalization", inputs=inputs, outputs=outputs)


class Model(torch.nn.Module):
    def __init__(self):
        super().__init__()
        self.conv1 = nn.Conv2d(in_channels=3, out_channels=3, kernel_size=3, padding=1)
        self.norm  = nn.LayerNorm(3)
        self.act   = nn.ReLU()

    def forward(self, x):
        _, _, H, W = x.shape
        L = H * W
        x = self.conv1(x)
        x = x.view(x.shape[0], x.shape[1], L).permute(0, 2, 1)
        x = self.norm(x)
        x = self.act(x)
        return x

def export_onnx_graph():
    input  = torch.Tensor(1, 3, 5, 5).uniform_(-1, 1)
    model  = Model()
    model.eval()

    file   = "../models/sample-ln-before.onnx"
    torch.onnx.export(
            model         = model,
            args          = (input,),
            f             = file,
            input_names   = ["input0"],
            output_names  = ["output0"],
            opset_version = 12)

    print("\nFinished export {}".format(file))

    model_onnx = onnx.load(file)
    onnx.checker.check_model(model_onnx)

    print(f"Simplifying with onnx-simplifier {onnxsim.__version__}...")
    model_onnx, check = onnxsim.simplify(model_onnx)
    assert check, "assert check failed"
    onnx.save(model_onnx, file)


#####################通过注册的LN算子替换网络节点####################
#          input (5, 5)
#            |
#           conv
#            |
#          reshape
#            |
#         layerNorm
#            |
#           relu
#            |
#          output (5, 5)

def change_onnx_graph():
    graph = gs.import_onnx(onnx.load_model('../models/sample-ln-before.onnx'))
    tensors = graph.tensors()

    norm_scale = gs.Constant(name="norm.weight", values=np.ones(shape=[3], dtype=np.float32))
    norm_bias  = gs.Constant(name="norm.bias", values=np.zeros(shape=[3], dtype=np.float32))

    inputs  = [tensors["/Transpose_output_0"]]
    outputs = [tensors["/norm/Div_output_0"]]
    
    # 因为要替换子网,所以需要把子网和周围的所有节点都断开联系
    for item in inputs:
        item.outputs.clear()

    for item in outputs:
        item.inputs.clear()

    # 为了迎合onnx中operator中的设计,这里把scale和bias给加上
    inputs = [tensors["/Transpose_output_0"],
              norm_scale,
              norm_bias]
    
    # 这个onnx中的epsilon,我们给加上。当然,我们也可以选择默认的值
    epsilon = [tensors["/norm/Constant_1_output_0"]]
    print(type(epsilon[0].values))

    # 通过注册的LayerNorm,重新把断开的联系链接起来
    graph.layerNorm(inputs, outputs, axis=-1, epsilon=epsilon[0].values)
    # graph.identity(inputs, outputs)
    # graph.layerNorm_default(inputs, outputs)

    # 删除所有额外的节点
    graph.cleanup()

    model = gs.export_onnx(graph)
    model.ir_version = 9
    onnx.save(model, "../models/sample-ln-after.onnx")

#####################验证模型##########################################
def validate_onnx_graph(input, origin_path, modified_path):
    sess_origin   = onnxruntime.InferenceSession(origin_path)
    output_origin = sess_origin.run(None, {'input0': input.numpy()})

    sess_modify   = onnxruntime.InferenceSession(modified_path)
    output_modify = sess_modify.run(None, {'input0': input.numpy()})

    print("input is \n", input)
    print("output_before is \n", output_origin)
    print("output_after is \n", output_modify)


def main() -> None:
    input  = torch.Tensor(1, 3, 5, 5).uniform_(-1, 1)
    
    ##从pytorch导出onnx(这里为了实验,将不支持LayerNorm的opset12为例导出)
    export_onnx_graph()

    ##手动修改LayerNorm
    change_onnx_graph()

    ##验证修改的onnx
    validate_onnx_graph(
            input, 
            "../models/sample-ln-before.onnx", 
            "../models/sample-ln-after.onnx")

if __name__ == "__main__":
    main()

这段代码首先定义了一个简单的 PyTorch 模型并导出为 ONNX 格式。然后,通过 onnx_graphsurgeon 替换模型中的 LayerNormalization 节点,并保存修改后的 ONNX 模型。最后,使用 onnxruntime 运行并验证修改前后的 ONNX 模型,确保两者输出一致。

gs_replace_LN.py 脚本中,LayerNormalization 的替换过程包括调用自定义的 LayerNormalization 函数,具体步骤如下:(from ChatGPT)

  • 1. 导入模型并获取张量:使用 onnx_graphsurgeon 导入已导出的 ONNX 模型,并提取模型中的所有张量。这些张量将用于重新连接新的节点。
  • 2. 创建 LayerNormalization 所需的常量:创建 scale 和 bias 常量,这些常量是 LayerNormalization 操作所必需的。这些常量被定义为固定值,并命名为 norm.weightnorm.bias
  • 3. 断开旧节点的连接:找到需要替换的节点的输入和输出张量,并清除它们之间的连接。这确保了旧节点不会影响新的 LayerNormalization 操作。
  • 4. 获取 epsilon 常量:提取现有网络中 LayerNormalization 操作所需的 epsilon 常量,这个值在重新连接新节点时会使用。
  • 5. 调用自定义的 LayerNormalization 函数:使用先前提取的输入张量、scale 常量、bias 常量和 epsilon 常量,调用自定义的 layerNorm 函数。该函数通过注册的 LayerNormalization 操作重新连接节点。调用时传入的参数包括输入张量、输出张量、axis 和 epsilon 值。
  • 6. 清理图并保存模型:清理图中不再需要的节点,确保模型结构的简洁和正确。最后,将修改后的图导出并保存为新的 ONNX 模型文件。

通过这些步骤,脚本实现了使用自定义的 LayerNormalization 函数替换模型中的旧节点,成功修改了模型的结构和功能。

值得注意的是上述代码与源码有所区别,博主进行了简单修改,将 17 行的 np.float 替换成了 float,这是因为博主在执行的过程中出现了如下问题:

在这里插入图片描述

这个错误是因为 np.float 是一个已经被弃用的别名,使用它会导致 AttributeError。从 NumPy 1.20 开始 np.float 被弃用,并推荐使用内置的 floatnp.float64

此外将模型导出的 IR 版本固定在 9 版本,不然会出现之前的 “Unsupported model IR version: 10, max supported IR version: 9” 错误

执行后输出如下:

在这里插入图片描述

在这里插入图片描述

可以看到 LayerNormalization 算子替换前后输出基本一致,说明替换没问题

导出的 sample-ln-before.onnx 模型如下图所示:

在这里插入图片描述

导出的 sample-ln-after.onnx 模型如下图所示:

在这里插入图片描述

其实 LayerNormalization 算子在 opset=17 的版本已经支持,因此我们完全可以在导出 ONNX 将 opset 版本设置为 17 及以上这样导出来的就是一个完整的 LayerNormalization,这里只是为了方便演示 onnx_graphsurgeon 替换算子的功能将 opset 设置成了 12

我们来看最后一个案例 gs_remove_node.py,代码如下所示:

import onnx_graphsurgeon as gs
import numpy as np
import onnx

def load_model(model : onnx.ModelProto):
    graph = gs.import_onnx(model)
    print(graph.inputs)
    print(graph.outputs)

def main() -> None:
    model = onnx.load("../models/example_two_head.onnx")

    graph = gs.import_onnx(model)
    tensors = graph.tensors()

    print(tensors)
    print(tensors["input0"])
    print(tensors["output0"])
    print(tensors["output1"])
    print(tensors["onnx::MatMul_8"])
    graph.inputs = [
            tensors["input0"].to_variable(dtype=np.float32, shape=(1, 1, 1, 4))]
    graph.outputs = [
            tensors["output0"].to_variable(dtype=np.float32, shape=(1, 1, 1, 3))]
            # tensors["onnx::MatMul_8"].to_variable(dtype=np.float32, shape=(1, 1, 1, 3))]
    graph.cleanup()
    onnx.save(gs.export_onnx(graph), "../models/example_two_head_removed.onnx")


if __name__ == "__main__":
    main()

该脚本文件使用 onnx_graphsurgeon 对 ONNX 模型进行修改。具体实现的功能是:导入一个名为 example_two_head.onnx 的模型,打印模型的输入、输出和特定张量的信息,修改模型的输入输出张量形状并清理图中的多余节点,最终保存修改后的模型为 example_two_head_removed.onnx。

执行后输出如下图所示:

在这里插入图片描述

example_two_head.onnx 如下图所示:

在这里插入图片描述

example_two_head_removed.onnx 如下图所示:

在这里插入图片描述

总结

本次课程我们主要学习了利用 onnx-graph-surgeon 来操作 ONNX,包括添加、修改节点,修改子图,替换算子,相比于 onnx.helper 更加方便,后续我们通过各种案例学习了 onnx_graphsurgeon 如何使用

OK,以上就是第 7 小节有关 onnx-graph-surgeon 的全部内容了,下节我们来学习快速分析开源代码并导出 ONNX,敬请期待😄

参考


网站公告

今日签到

点亮在社区的每一天
去签到