众所周知,Batch对于GPU上深度学习模型的运行效率影响很大。。。
是在Inference时。搜索、推荐等场景自带比较大的batch,问题不大。但更多场景面临的往往是稀碎的请求(比如图片服务里一次一张图)。
如果想提高服务的吞吐,把稀碎的请求动态攒成Batch再送GPU处理就是刚需。
NV的Triton包含了Dynamic Batching功能。我也用cpp写过一版。但是发现在部署、特别是给别人用python来调用的时候,始终是比较麻烦的。比如要各种配置环境或用NGC的镜像、走个本地rpc等。。
反过来想,只要程序瓶颈还卡在计算上,就有机会用python写一版至少吞吐上可以打平cpp的Dynamic Batching。好处是使用会方便很多。
出于个人需要和兴趣,之前基于multiprocess.Queue写过一版Dynamic Batching。但是Queue本身对于延迟的影响非常大,数字比较难看。
最近发现Python 3.8支持了共享内存,用python写了个基于SharedMemory的Dynamic Batching。
跟大家分享一下效果。
模型Resnet50,输入(N,3,224,224)。使用某云的V100。
首先使用如下命令下载并生成测试用模型
python fake_resnet50.py
fake_resnet50.py脚本会下载ImageNet上预训练好的resnet50模型
并将其分别转换为jit、torch2trt、onnx等几种格式,保存在test目录下
后面的介绍中,如无特殊说明,使用的jit格式模型进行的测试
我们先测一下Torch性能上限,好对数据有个基本了解。
然后一步步看不同功能的影响。
对应测试命令:
# 测试
python benchmark.py --no_dynamic_batch --worker_num=N --worker_batch=M
多进程Torch + MPS。
进程数量 | Batch | Latency | Throughput |
---|---|---|---|
1 | 1 | 4.54 ms | 220.10 pic/s |
4 | 1 | 8.05 ms | 496.52 pic/s |
8 | 1 | 13.97 ms | 572.57 pic/s |
16 | 1 | 28.15 ms | 526.42 pic/s |
可以看出MPS是很有效的,没有MPS时,多进程轮占时间片,多个进程吞吐基本也就卡在200多。
加了多进程后,多进程的kernel在同一context下调度。在8的时候达到最高。
基于以上数据,再看下Batching的影响。
进程数量 | Batch | Latency | Throughput |
---|---|---|---|
4 | 1 | 8.05 ms | 496.52 pic/s |
1 | 4 | 6.43 ms | 622.07 pic/s |
进程数量 | Batch | Latency | Throughput |
---|---|---|---|
8 | 1 | 13.97 ms | 572.57 pic/s |
1 | 8 | 10.43 ms | 766.93 pic/s |
进程数量 | Batch | Latency | Throughput |
---|---|---|---|
16 | 1 | 28.15 ms | 526.42 pic/s |
1 | 16 | 18.03 ms | 887.20 pic/s |
可以看到MPS虽然对吞吐有帮助,但是有条件的话,Batching依旧是更好的选择。
在测一下Batch=32(或者其他比较高的数字都可),看一下torch框架的上限。
进程数量 | Batch | Latency | Throughput |
---|---|---|---|
1 | 32 | 33.54 ms | 953.60 pic/s |
2 | 32 | 56.98 ms | 1123.20 pic/s |
3 | 32 | 78.96 ms | 1215.47 pic/s |
4 | 32 | 109.89 ms | 1164.80 pic/s |
即便batch比较大了,但MPS依旧有提升。
实际应用中,琐碎请求会带来的性能下降。如果对于延迟的要求没有非常苛刻,那么是可以通过牺牲一部分延迟(用来打Batch),换取更高的吞吐(省钱)。
所以这轮测试的场景是,有N个数据(业务)进程,每个进程数据batch=1,达到MPS+Batching的上限吞吐。
先试一下对上述最大吞吐的case。128个数据(业务)进程,每个进程灌一张图,后台通过共享内存传输数据并打batch。
测试命令:
python benchmark.py --worker_num=128 --worker_batch=1 --max_batch_size=32 --model_num=3 --wait_time=0.01
数据(业务)进程 | GPU模型进程 | Latency | Throughput |
---|---|---|---|
128 | 3 | 103.45 ms | 1237.33 pic/s |
能够达到极限延迟,但比最理想的情况增加了20%+的延迟。
找个小的场景试一下:
python benchmark.py --worker_num=8 --worker_batch=1 --max_batch_size=4 --model_num=2 --wait_time=0.003
数据(业务)进程 | GPU模型进程 | Latency | Throughput |
---|---|---|---|
8 | 2 | 13.04 ms | 613.40 pic/s |
跟前面Torch测试的数字对比,可以理解成这case下8个请求进程被分成两组,总体基本能够达到batch=4的吞吐。
针对1200+的最大吞吐场景分析了一下:
延迟由 batch + MPS 的 79 ms 增加至 Dynamic Batching 的 103ms.其中,
- 19ms 左右是拼batch的时间,其中10ms是命令中的等待时间,还有8.3ms的np.concat时间。
- 分割输出回各数据进程大概用了1ms。
- 各种队列的等待时间。
总的来说没有不太合理的地方,在benchmark里我也把各部分时间收集和打出来了。
虽然源码不长(<1000行),结构也简单。但各种进程和通信还是有点多的。
程序启动时创建context进程,每个数据进程创建模型实例时:
- context 进程会查看是否已存在对应的模型backend进程
- 存在 -> 通过shared memory 建立连接
- 不存在 -> 创建backend进程 -> 创建模型进程
- 多个模型进程是为了充分利用MPS
- 当用户进程中有多段模型时,会创建相应多个backend进程,比如识别+检测等等
- 进程间不传输数据,仅传输shared memory地址和tensor元信息。
原理大概就是这个 shared_memory sample
测试代码:benchmark.py
使用样例:sample.py
- 基本跟用pytorch差不多,load+forward。但是:
- 要指定数据最大尺寸,用来分配shared memory
- 最后要用一个Run函数启动,因为要提前初始化一些进程变量
- 需要为模型指定name。当程序涉及到多个模型的时候,数据进程通过name连接到特定的模型进程。
multiprocess.shared_memory在回收时,在一些系统下会报leak或已经释放的error/warning,一些系统正常。
错的系统我跑官方示例也有错。所以还不好判断是什么原因。如果觉得可以忍又不想烦可以用下面的命令禁掉。
export PYTHONWARNINGS=ignore
目前TensorRT模块支持两种方式转化出的模型:
-
torch2trt: 使用torch2trt 转化和保存的模型(model_type=tensorrt)。
-
onnx2trt: 以及通过 pytorch-onnx-tensorrt 方式转化出的模型(model_type=onnx2trt)
两种方式暂时均只支持FP32和FP16的推理
torch2trt更简单,但推荐使用pytorch-onnx-tensorrt的方式进行模型转换,该方法的fp16推理性能要显著优于torch2trt,后面有测试结果
使用onnx2trt转换的模型的方式如下:
首先使用下面命令,将之前已经导出好的onnx格式模型转化为fp16的tensorrt模型
python onnx2tensorrt.py
然后运行如下命令进行测试:
# benchmark test
python benchmark.py --no_dynamic_batch --worker_num=1 --worker_batch=16 --model_type=onnx2trt --model_path=test/res50_onnx2trt_fp16.trt --data_type=float16
# dynamic test
python benchmark.py --worker_num=64 --worker_batch=1 --max_batch_size=16 --model_num=1 --wait_time=0.01 --model_type=onnx2trt --model_path=test/res50_onnx2trt_fp16.trt --data_type=float16
onnx2trt在fp16下的效果要显著优于torch2trt以及原始的jit,测试记录如下:
benchmark测试设置均为 ( --worker_num=1 --worker_batch=16 )
dynamic batch测试设置均为(--worker_num=64 --worker_batch=1 --max_batch_size=16 --model_num=1 --wait_time=0.01)
模型格式 | throughput(benchmark) | latency(benchmark) | throughput(dynamic) | lantency(dynamic) |
---|---|---|---|---|
torch(jit) | 902 | 17.71 ms | 867 | 73.74 |
torch2trt(fp16) | 1264 | 12.24 ms | 1258 | 50.84 |
onnx2trt(fp16) | 3736 | 4.28 | 3168 | 20.20 |
If 有人感兴趣 and 我有时间 :
- 支持一下TensorCore FP16,以及某个特定版本的TF。
- 输出还没有全用shared memory(主要是我懒),所以大输出模型的 吞吐/延迟 会受到数据拷贝的影响。可以改进。。。