当前位置:网站首页 > PyTorch框架 > 正文

pytorch模型部署onnx(pytorch模型部署到orin)





bert模型进行gpu上推理




 
  
 
  


// bert输入向量化过程




 
  
 
  

简单回顾一下,这个模型是对新闻标题进行分类,共有10类,编号0 ~ 9,模型预测的过程分位如下几步:

1. 首先将新闻标题的文本,在前面拼上字符串[CLS],然后经过切词得到token,再把token转数字得到token_ids。这里还需要再做一下padding。因为模型输入是定长的,本模型中 pad_size 是32,即token_id个数不足32的时候,要通过0补全到32。

2. 模型的第二个输入是mask,它和token_ids拥有相同的shape(形状)。比如token_ids是1行32列,那么mask也是1行32列。mask内部元素的取值只有0和1。它表征的是token_ids哪些位置是token,哪些是padding。如果是token则为1,是padding则为0。

3. 将token_ids和mask转成torch.Tensor,这里需要的是二维结构,所以套上了一层 [] 。

4. 使用加载好的模型,传入构造好的Tensor,做预测,这个过程其实就是跑一遍模型的前向(forward)传播的过程。seq_len其实不需要,只不过模型训练的forward函数,接收了它,所以也一并传过去。其实forward函数内部又把seq_len丢掉了。

5. 拿到模型的输出,它输出的是这个新闻标题对应10个分类的概率。所以它返回的是一个浮点型的数组。此时使用argmax函数,也就是找到这个数组中值最大的元素的下标。再拿下标去10个分类中找到该分类的名称(代码中是变量key),就是最终的预测结果了。

pytorch自带函数可以把pytorch模型导出成onnx模型。官网API资料:  针对我们的得模型,我们可以这样写出大致的导出脚本 to_onnx.py:

对export函数的参数进行一下解读:

参数 解读 model 加载的pytorch模型的变量 args tuple类型参数,表示模型输入的shape(形状) 'model.onnx' 导出的onnx模型的文件名 export_params 是否导出参数 opset_version ONNX的op版本,这里用的是11 input_names 模型输入的参数名 output_names 模型输出的参数名 dynamic_axes 动态维度设置,不设置即只支持固定维度的参数。本例子其实可以不设置,因为我们传入的参数都是自己调整好维度的。

args用于标识模型输入参数的shape。这个可以好好谈谈一下。

在pytorch模型预测脚本中,build_predict_text()函数会把一段文本处理成模型的三个输入参数,所以它返回的对象肯定是符合模型输入shape的。:

torch.onnx.export()调用的时候,其实只关心形状,而不关心内容。所以我们可以去掉token化的过程。直接用0:

但如果把这个args传入export()函数,运行会报错:

报错显示,forward()函数预期传入两个参数,但是实际传入了4个。 看一下模型的forward函数的定义(models/bert.py中)

函数定义确实是两个参数,一个self,一个x,x存储的(ids, seql_len, mask)这一tuple。 模型训练的时候,训练过程调用forward()函数,传入这个tuple,不会出问题,export()内部调用forward()的时候为何会报错呢?其实很多人都遇到过这个问题,比如:

源码差异我也没有去深究,对我的意图而言有点舍本逐末,这里其实无需纠结。 解决方法如下:

可以给它再套上一层。

如果你实在不想给args再套一层,可以让build_args1返回list而非,实测也能解决问题。

如果你不想用0,也可以转换成另外一种写法:

torch.randint()函数是用来构造随机整形Tensor的,这里不展开介绍它的参数释义了。

好了,经过前面的步骤,顺利的话,你已经得到一个onnx的模型文件model.onnx了,现在我们可以用ONNX模型执行预测任务了。但我们不能一口气吃成一个胖子,在真正使用C++将ONNX模型服务化之前,我们还是需要先使用Python完成ONNX模型的预测,一方面是验证我们转换出来的ONNX确实可用,另一方面对后续转换成C++代码也有参考意义!

这个过程我们就需要用到ONNX Runtime了。库名是:onnxruntime了,通常简称ort。

onnxruntime不会随onnx一起安装,需要单独安装。因为我们整个实际都是基于GPU展开的,这里推荐用pip安装,因为conda似乎没有onnxruntime的gpu的包,conda默认安装的是CPU版本。pip安装命令如下:

如果去掉-gpu,则安装的也是CPU版本。

执行一下下面脚本,检查一下是否有安装成功:

在我的环境上会输出:

onnxruntime的python API手册在这:

onnxruntime中执行预测的主体是通过类型的对象进行的。 的构造参数不少,但常用的构造参数只有2个,示例:

第一个参数就是模型文件的路径,第二个参数指定provider,它的取值可以是:

  • CUDAExecutionProvider
  • CPUExecutionProvider
  • TensorrtExecutionProvider

顾名思义,就是用GPU的CUDA执行,就是用CPU执行,是用TensorRT执行。没有安装TensorRT环境的话,即使指定它也不会生效,会退化成CPUExecutionProvider·。

预测过程就是InferenceSession对象调用run()方法,它的参数声明如下:

  • output_names – 输出的名字,可以为None
  • input_feed – 字典类型
  • run_options – 有默认值,可以忽略

它的返回值是一个list,list里面的值是这段预测文本对于每种分类的概率。和pytorch的预测脚本一样,再经过argmax()等处理就是最终结果了。

好了,现在唯一的问题就是构造第二个参数了。这是一个字典数据结构,key是参数的名称。我们可以通过InferenceSession的get_inputs()函数来获取。get_inputs()返回一个list,list中NodeArg类型的对象,这个对象有一个name变量表示参数的名称。 写个小代码测试一下:

输出:

可以看到两个输入参数的名称ids和mask,其实就是我们导出ONNX模型的时候指定的输入参数名,这我们不适用get_inputs()来获取参数名,直接硬编码,其实也可以。

input_feed 的 key解决了,接下来我们来获取input_feed的value。

pytorch的预测脚本中,build_predict_text()把一段文本转换成了三个torch.Tensor。onnx模型的输入肯定不是torch中的Tensor。它只需要numpy数组即可。 偷懒的做法是,我们直接引入build_predict_text(),然后把Tensor类型转换成numpy数组。可以在网上找的转换的代码:

然后就可以:

其实这有点绕弯子了,我们并不需要通过Tensor转numpy,因为Tensor是通过list转出来的,我们直接用list转numpy就可以了。

先改一下pred.py将原先的build_predict_text拆成两部分:

接着我们onnx的预测脚本(onnx_pred.py)中就可以直接调用,再把它的结果转numpy数组就可以了。注意,我们训练得到的Bert模型需要的是一个二维结构,所以和Tensor的构造方式一样,还需要再套上一层 。 好了,完整的onnx预测脚本可以这么写:

最终输出:

模型转换为ONNX模型后,性能表现如何呢?接下来可以验证一下。 先写一些辅助函数:

新建一个文件news_title.txt,里面存放多条新闻标题,我们来让pytorch模型和onnx模型分别做一下预测,然后对比耗时,main函数如下:

最终结果:

可以看到ONNX比Pytorch的性能提高了一倍。当然TensorRT的性能会更好,不过这是后话了,本文暂且不表。

所谓“他山之石,可以攻玉”。把中文文本向量化,这一步基本是琐细且模式化的操作,网上肯定有现成的代码,比如这个Github gist:

这是一个完整的可运行的代码片段,编译并运行它需要两个依赖:boost和utf8proc

boost对于C++程序员来说应该都不陌生,utf8proc是一个处理UTF-8字符的C语言库,在Github上:

这是一个可以循环从标准输入获取文本,然后转换成向量输出的程序。我们其实只需要关注如下API即可。

需要传入一个词汇文件(vocab)作为输入。我们那个Bert项目,也是有自己的词汇文件的。

现在我们使用我们Bert项目中词汇文件,来初始化一个FulTokenizer,看看它的向量化结果是否符合预期:

输出:

我们使用之前文章中提到的python脚本中的向量化函数,来对同一段文本进行一下向量化。看一下结果:

可以看出python版本第一位多一个101,后面的数字基本相同。这个101就是Bert模型中[CLS]标记对应的向量化后的数字。别忘了,python版本,都是要拼接这个前缀的:

标记在Bert模型中表示的是这是一个分类(Classify)任务。因为Bert模型除了分类,还能执行其他任务。

当然如果你把传入我们C++版本的向量化函数,结果是不符合预期,比如输入改成:

结果是前面多了4个数字,并不会只是添加一个101,因为是在Bert模型中的tokenizer会特殊处理的字符串,和普通文本的向量化方式不同。

解决方案是我们只需要可以把C++向量化结果,手工拼上一个101就可以了。

强烈不推荐从Github源码编译产出ONNX Runtime(以下简称ORT)库,直接下载安装它的cuda的release包,里面有现成的so:

ORT的C++版本API文档:

Ort::Session对应ORT的python API中 InferenceSession。 Ort::Session的构造函数有多个重载版本,最常用的是:

比如可以这构造Session:

session_options是用于设置一些额外的配置参数,比如上面设置成CUDA执行。它还有其他的设置,这里不展开。我们只需要实现一个最简单代码即可。

Ort::Value是模型输入的类型,也就是ORT C++ API中表示Tensor(张量)的类型。它通过CreateTensor函数构造,CreateTensor函数也有多个重载,这里介绍其中一个。

CreateTensor() API

函数参数 描述 info p_data指向数据存储的内存类型,CPU或者GPU p_data 核心数据(的地址) p_data_element_count p_data指向数据的字节数 shape p_data形状(的地址) shape_len shape参数的维度

模板参数T

模板参数T表示Tensor中数据类型。对于我们这里就是。

p_data 与 p_data_element_count

表示的就是核心的数据,是一段连续存储,可以使用vector来存储,通过data()函数获取其数据的指针。 p_data_element_count 表示的就是这段连续存储中有多少个元素了。

shape 与 shape_len

shape参数用来表示Tensor的形状。因为不管数学意义上的Tensor的形状如何,在ORT C++ API中p_data都是使用一度连续存储的空间表示,不会像python中一样套上层层的括号表达维度。比如数学意义上的一个2维矩阵:,在这里只需要传入 然后通过shape参数:表示这是的矩阵。

通过上一篇文章,我们知道我们模型的输入是ids和mask两个Tensor,每个形状都是一个。所以可以这样表示这个shape:

shape.data()即可以获得一个的指针,因为我们这里维度是固定的,所以直接用int64的数组也以。

shape_len表示的就是shape中有几个元素(shape的维度),即2。

info

表示的是p_data是存储在CPU还是GPU(CUDA)上。这里我们用CPU来存储输入的Tensor数据即可,因为代码会比较简练:

如果是GPU存储,则需要调用CUDA的API,稍微繁琐一点。 即使这里是CPU也不影响我们模型在GPU上跑推理的。

合并Tensor

假设我们已经得到了存储模型输入参数ids和mask向量的两个vector对象:input_tensor_values和mask_tensor_values,我们可以先这样获得表示各自Tensor的Ort::Value对象:

接下来,将两个Tensor合并成一个。

Session的Run函数就是执行模型推理的过程。

参数梗概

参数如下:

参数 描述 run_options 可忽略 input_names 模型输入的名称 input_values 模型输入 input_count 输入的个数 output_names 输出的名称 output_count 输出的个数

调用示例

Run()的返回值是std::vector<Ort::Value>类型,因为模型可能有多输出,所以是vector表示,但是对于我们的模型来说它的输出只有一个Tensor,所以返回值outout_tensors的size必为1。不放心的话,也可以额外检查一下。 所以outout_tensors[0]就是输出向量了,和python一样,表示的是输入Tensor对于每种分类下的概率,我们选取概率最高的那个,就表示最终预测的分类结果了。

C++本身没有argmax的函数,但是利用STL,很容易写出一个:

最终结果

有了前面的铺垫,我们把文本向量化和ORT的预测功能整合成一个Model类,提供一个更简单便捷的使用方式。

通过前面的例子,Ort::Env参数应该只是构造Ort::Session时的临时变量,这里为什么要弄成Model类的成员变量呢?作为Model构造函数中的局部变量不行吗?在我的1.31的ORT版本上还真不行。因为如果env是一个局部变量,在后面infer函数中执行Session::Run()的时候,会coredump。 回看Ort::Session的构造参数定义:

Env是一个非常量的引用,也就是env如果定义成一个局部变量,那么在Model构造函数结束之后,env引用就失效了,出现引用悬空(可以理解成野指针)。但是在Session::Run()执行的时候,内部还会使用到env中的数据,从而出现非法的内存访问。

其实这属于API设计上的一个BUG,最近看到ORT的Github上已经做了修复。参见这个Pull Request:

这里改成常量引用,是可以延长局部变量的声明周期的。 这个PR是2022.10.19 Merge到主干的,并没有包含到当前(2022.11)最新的版本(1.13.1)中。虽然这个版本是2022.10.25发布release的。下个版本应该能体现,到时候就不用再特殊处理Env参数了。

用来封装文本向量化,以及最终返回ids和mask两个向量的过程。中间包含补,padding的操作。

infer用来执行推理,返回文本最接近的分类。

predict()函数比infer()函数更进一步,用来返回分类的名称。 首先我们还是借用python中分类名:

然后:

终于到了bRPC服务化的环节了,其实这部分已经比较简单了。直接用官方example中的echo_server改改就可以了。关于bRPC的基础,可以参考我之前的这两篇文章:

bRPC支持单端口多协议,一个bRPC服务默认除了可以提供protobuf类型的请求外,也只支持HTTP+JSON请求。所以我们可以直接使用curl来测试:

输出:

观察server端的日志,cost的单位是us:

可以看出平均耗时是6.6ms左右。在同一台机器上,使用我上次的python版onnx脚本来测试相同文本,平均耗时是7.2ms左右。C++和python的ORT都是没有设置额外参数的,单就推理本身而言,其实C++版本的推理本身性能优势并不大。因为即使是python版ORT,它真正执行的推理后端也是C/C++编译产出的库,而非python直接进行推理。

这就有个疑惑了,既然如此,那么把模型用C++服务化呢?

一个原因在于,这个模型并不是特别复杂的模型,所以对比并不明显,另外一个重要原因在于我并没有使用高qps来压测,我们把模型用C++服务化,也并不是奢求能获得更多的推理过程的加速,而是希望达到作为一个后台服务本身的高并发、高吞吐。

我拿来对比的python代码是单次执行的脚本,而非python服务。这些测试都是单条单条进行的,如果是高qps下,用C++服务和python服务做对比,差异会更明显。

 
  
到此这篇pytorch模型部署onnx(pytorch模型部署到orin)的文章就介绍到这了,更多相关内容请继续浏览下面的相关推荐文章,希望大家都能在编程的领域有一番成就! 
  

                            

版权声明


相关文章:

  • pytorch模型部署(pytorch模型部署到web)2025-07-28 10:18:08
  • 怎么删除虚拟环境jupyter(删除pytorch虚拟环境)2025-07-28 10:18:08
  • pytorch模型部署到web(pytorch模型部署到树莓派)2025-07-28 10:18:08
  • pytorch模型部署到web(pytorch模型部署到Linux)2025-07-28 10:18:08
  • 尽情享受生活之乐趣——蒙田2025-07-28 10:18:08
  • pytorch模型部署 django(pytorch模型部署单片机)2025-07-28 10:18:08
  • pytorch模型部署到安卓(pytorch 安卓)2025-07-28 10:18:08
  • pytorch模型部署(pytorch模型部署到树莓派)2025-07-28 10:18:08
  • 删除pytorch虚拟环境(pycharm怎么删除虚拟环境)2025-07-28 10:18:08
  • 怎么删除虚拟环境中的pytorch(pytorch 虚拟环境)2025-07-28 10:18:08
  • 全屏图片