GPU 推理服务性能优化之路 | 得物技术
1 背景
随着 CV 算法在业务场景中使用越来越多,给我们带来了新的挑战,需要提升 Python 推理服务的性能以降低生产环境成本。为此我们深入去研究 Python GPU 推理服务的工作原理,推理模型优化的方法。最终通过两项关键的技术: 1.Python 的 GPU 与 CPU 进程分离,2.使用 TensorRT 对模型进行加速,使得线上大部分模型服务 QPS 提升 5-10 倍左右,大量节约了线上 GPU 推理服务的成本。
针对上面的两项关键技术,我们还自研了相关框架与工具进行沉淀。包括基于 Python 的 CPU 与 GPU 进程自动隔离的推理服务框架,以及对推理模型进行转 TensorRT 优化的调试工具。
此外针对不同的推理服务性能瓶颈,我们还梳理了各种实战优化技巧,比如 CPU 与 GPU 分离,TensorRT 开启半精度优化,同模型混合部署,GPU 数据传输与推理并行等。
下面从理论,框架与工具,实战优化技巧三个方面介绍下推理服务性能优化的方法。
2 理论篇
2.1 CUDA 架构
CUDA 是 NVIDIA 发明的一种并行计算平台和编程模型。它通过利用图形处理器 (GPU) 的处理能力,可大幅提升计算性能。
CUDA 的架构中引入了主机端(host, cpu)和设备(device, gpu)的概念。CUDA 的 Kernel 函数既可以运行在主机端,也可以运行在设备端。同时主机端与设备端之间可以进行数据拷贝。
CUDA Kernel 函数:是数据并行处理函数(核函数),在 GPU 上执行时,一个 Kernel 对应一个 Grid,基于 GPU 逻辑架构分发成众多 thread 去并行执行。CUDA Stream 流:Cuda stream 是指一堆异步的 cuda 操作,他们按照 host 代码调用的顺序执行在 device 上。
典型的 CUDA 代码执行流程:a.将数据从 Host 端 copy 到 Device 端。b.在 Device 上执行 kernel。c.将结果从 Device 段 copy 到 Host 端。
以上流程也是模型在 GPU 推理的过程。在执行的过程中还需要绑定 CUDA Stream,以流的形式执行。
2.2 传统 Python 推理服务瓶颈
2.2.1 传统 Python 推理服务架构
由于 Python 在神经网络训练与推理领域提供了丰富的库支持,加上 Python 语言自身的便利性,所以推理服务大多用 Python 实现。CV 算法的推理引擎大多采用 Python flask 框架或 Kserve 的框架直接实现。这种框架大致调用流程如下:
以上架构是传统推理服务的常用架构。这种架构的优势是代码写起来比较通俗易懂。但是在性能上有很大的弊端,所能承载的 QPS 比较低。我们用了几个 CV 模型去压测,极限 QPS 也一般不会超过 4。
2.2.2 瓶颈分析
由于以上架构的 CPU 逻辑(图片的前处理,后处理)与 GPU 逻辑(模型推理)在同一个线程内,所以会存在如下性能瓶颈:
如果是单线程的模式,CPU 逻辑与 GPU 逻辑相互等待,GPU Kernel 函数调度不足,导致 GPU 使用率不高。无法充分提升 QPS。这种情况下只能开启更多进程来提升 QPS,但是更多进程会带来更多显存的开销。
如果开启多线程模式,经过实测,这种方式也不能带来 QPS 的提升。主要是因为 Python 的 GIL 锁的原因,由于 Python GIL 锁的存在,Python 的多线程实际上是伪的多线程,并不是真正的并发执行,而是多个线程通过争抢 GIL 锁来执行,这种情况下 GPU Kernel launch 线程不能得到充分的调度。在 Python 推理服务中,开启多线程反而会导致 GPU Kernel launch 线程频繁被 CPU 的线程打断。由于 GPU kernel lanch 调度不足,这种方式也无法充分利用 GPU 使用率。
2.2.3 解决方案
针对以上问题,我们的解决方案是把 CPU 逻辑与 GPU 逻辑分离在两个不同的进程中。CPU 进程主要负责图片的前处理与后处理,GPU 逻辑则主要负责执行 cuda kernel 函数,即模型推理。
另外由于我们线上有大量推理服务在运行,所以我们基于 Python 开发了一个 CPU 与 GPU 分离的统一框架。针对原有 Flask 或 Kserve 的服务,稍作修改即可使用我们的服务。具体请参考下面的 CPU 与 GPU 分离的统一推理框架相关介绍。
针对线上的某个推理服务,使用我们的框架进行了 CPU 与 GPU 进程分离,压测得出的数据如下,可见 QPS 大约提升了 7 倍左右。
2.3 TensorRT 模型加速原理
TensorRT 是由英伟达公司推出的一款用于高性能深度学习模型推理的软件开发工具包,可以把经过优化后的深度学习模型构建成推理引擎部署在实际的生产环境中。TensorRT 提供基于硬件级别的推理引擎性能优化。下图为业界最常用的 TensorRT 优化流程,也是当前模型优化的最佳实践,即 pytorch 或 tensorflow 等模型转成 onnx 格式,然后 onnx 格式转成 TensorRT 进行优化。
其中 TensorRT 所做的工作主要在两个时期,一个是网络构建期,另外一个是模型运行期。
a.网络构建期 i.模型解析与建立,加载 onnx 网络模型。ii.计算图优化,包括横向算子融合,或纵向算子融合等。iii.节点消除,去除无用的节点。iv.多精度支持,支持 FP32/FP16/int8 等精度。v.基于特定硬件的相关优化。
b.模型运行期 i.序列化,加载 RensorRT 模型文件。ii.提供运行时的环境,包括对象生命周期管理,内存显存管理等。
以下是我们基于 VisualTransformer 模型进行的 TensorRT 优化前后的性能评测报告:
3 框架与工具篇
这一篇章,主要介绍我们自己推出的框架与工具。其中框架为 CPU 与 GPU 分离的 Python 统一推理框架,工具则为 Onnx 转 TensorRT 的半自动化调试工具。相关框架与工具我们在线上大量推理服务推进使用中。
其中 CPU 与 GPU 分离的 Python 统一推理框架解决了普通 Python 推理服务无法自动隔离 CPU 与 GPU 的问题,用户只需要继承并实现框架提供的前处理,推理,后处理相关接口,底层逻辑即可自动把 CPU 与 GPU 进行进程级别隔离。
其中 TensorRT 半自动化调试工具,主要定位并解决模型转 TensorRT 的过程中遇到的各种精度丢失问题。底层基于 TensorRT 的相关接口与工具进行封装开发。简化 TensorRT 的优化参数。
3.1 CPU 与 GPU 分离的统一推理框架
新架构设计方案如下:
方案设计的思路是 GPU 逻辑与 CPU 逻辑分离到两个进程,其中 CPU 进程主要负责 CPU 相关的业务逻辑,GPU 进程主负责 GPU 相关推理逻辑。同时拉起一个 Proxy 进程做路由转发。
(1)Proxy 进程 Proxy 进程是系统门面,对外提供调用接口,主要负责路由分发与健康检查。当 Proxy 进程收到请求后,会轮询调用 CPU 进程,分发请求给 CPU 进程。
(2)CPU 进程 CPU 进程主要负责推理服务中的 CPU 相关逻辑,包括前处理与后处理。前处理一般为图片解码,图片转换。后处理一般为推理结果判定等逻辑。CPU 进程在前处理结束后,会调用 GPU 进程进行推理,然后继续进行后处理相关逻辑。CPU 进程与 GPU 进程通过共享内存或网络进行通信。共享内存可以减少图片的网络传输。
(3)GPU 进程 GPU 进程主要负责运行 GPU 推理相关的逻辑,它启动的时候会加载很多模型到显存,然后收到 CPU 进程的推理请求后,直接触发 kernel lanuch 调用模型进行推理。
该方案对算法同学提供了一个 Model 类接口,算法同学不需要关心后面的调用逻辑,只需要填充其中的前处理,后处理的业务逻辑,既可快速上线模型服务,自动拉起这些进程。
该方案把 CPU 逻辑(图片解码,图片后处理等)与 GPU 逻辑(模型推理)分离到两个不同的进程中。可以解决 Python GIL 锁带来的 GPU Kernel launch 调度问题。
3.2 TensorRT 调试工具
TensorRT 虽然不是完全开源的,但是官方给出了一些接口与工具,基于这些接口与工具我们可以对模型优化流程进行分析与干预。基于 TensorRT 官方提供的接口与工具,我们自己研发了一套工具。用户可以使用我们的工具把模型转成 TensorRT 格式,如果在模型转换的过程中出现精度丢失等问题,也可以使用该工具进行问题定位与解决。
自研工具主要在两个阶段为用户提供帮助,一个阶段是问题定位,另一个阶段是模型转换。具体描述如下:
3.2.1 问题定位
问题定位阶段主要是为了解决模型转 TensorRT 开启 FP16 模式时出现的精度丢失问题。一般分类模型,对精度的要求不是极致的情况下,尽量开启 FP16,FP16 模式下,NVIDIA 对于 FP16 有专门的 Tensor Cores 可以进行矩阵运算,相比 FP32 来说吞吐量提升一倍以上。比如在转 TensorRT 时,开启 FP16 出现了精度丢失问题,自研工具在问题定位阶段的大致工作流程如下:
主要工作流程为:(1)设定模型转换精度要求后,标记所有算子为输出,然后对比所有算子的输出精度。(2)找到最早的不符合精度要求的算子,对该算子进行如下几种方式干预。
标记该算子为 FP32。
标记其父类算子为 FP32。
更改该算子的优化策略(具体参考 TensorRT 的 tactic)
循环通过以上两个步骤,最终找到符合目标精度要求的模型参数。这些参数比如,需要额外开启 FP32 的那些算子等。然后相关参数会输出到配置文件中,如下:
3.2.2 模型转换
模型转换阶段则直接使用上面问题定位阶段得到的参数,调用 TensorRT 相关接口与工具进行转换。此外,我们在模型转换阶段,针对 TensorRT 原有参数与 API 过于复杂的问题也做了一些封装,提供了更为简洁的接口,比如工具可以自动解析 ONNX,判断模型的输入与输出 shape,不需要用户再提供相关 shape 信息了。
4 优化技巧实战篇
在实际应用中,我们期望用户能够对一个推理模型开启 CPU 与 GPU 分离的同时,也开启 TensorRT 优化。这样往往可以得到 QPS 两次优化的叠加效果。比如我们针对线下某个分类模型进行优化,使用的是 CPU 与 GPU 分离,TensorRT 优化,并开启 FP16 半精度,最终得到了 10 倍的 QPS 提升。
以下是我们在模型优化过程中的一些实战技巧,梳理一下,分享给大家。
(1)分类模型,CPU 与 GPU 分离,TensorRT 优化,并开启 FP16,得到 10 倍 QPS 提升某个线上基于 Resnet 的分类模型,对精度损失可以接受误差在 0.001(误差定义:median,atol,rtol)范围内。因此我们对该推理服务进行了三项性能优化:a.使用我们提供的 GPU 与 CPU 分离的统一框架进行改造。b.对模型转 ONNX 后,转 TensorRT。c.开启 FP16 模式,并使用自研工具定位到中间出现精度损失的算子,把这些算子标记为 FP32.
经过以上优化,最终得到了 10 倍 QPS 的提升(与原来 Pytorch 直接推理比较),成本上得到比较大的缩减。
(2)检测模型,CPU 与 GPU 分离,TensorRT 模型优化,QPS 提升 4-5 倍左右。某个线上基于 Yolo 的检查模型,由于对精度要求比较高,所以没有办法开启 FP16,我们直接在 FP32 的模式下进行了 TensorRT 优化,并使用统一框架进行 GPU 与 CPU 分离,最终得到 QPS 4-5 倍的提升。
(3)同模型重复部署,充分利用 GPU 算力资源在实际的场景中,往往 GPU 的算力是充足的,而 GPU 显存是不够的。经过 TensorRT 优化后,模型运行时需要的显存大小一般会降低到原来的 1/3 到 1/2。
为了充分利用 GPU 算力,框架进一步优化,支持可以把 GPU 进程在一个容器内复制多份,这种架构即保证了 CPU 可以提供充足的请求给 GPU,也保证了 GPU 算力充分利用。优化后的架构如下图:
5 总结
采用以上两个推理模型的加速技巧,即 CPU 与 GPU 进程隔离,TensorRT 模型加速。我们对线上的大量的 GPU 推理服务进行了优化,也节省了比较多的 GPU 服务器成本。其中 CPU 与 GPU 进程隔离主要是针对 Python 推理服务的优化,因为在 C++的推理服务中,不存在 Python GIL 锁,也就不存在 Python Kernel launch 线程的调度问题。目前业界开源的 Python 推理服务框架中,还没有提供类似的优化功能,所以我们后续有考虑把 Python 统一推理服务框架进行开源,希望能为社区做一点贡献。此外 TensorRT 的模型优化,我们参考了大量 NIVIDIA 的官网文档,在上层做了封装,后续会进一步深入研究。
版权声明: 本文为 InfoQ 作者【得物技术】的原创文章。
原文链接:【http://xie.infoq.cn/article/cb08e61526ad64e01eb1ced9f】。
本文遵守【CC-BY 4.0】协议,转载请保留原文出处及本版权声明。
评论