• MindSpore Serving模型部署,如何提升吞吐量,降低推理时延


    1.    关于MindSpore Serving


    MindSpore Serving是一个轻量级、高性能的服务模块,旨在帮助MindSpore开发者在生产环境中高效部署在线推理服务。当用户使用MindSpore完成模型训练后,导出MindSpore模型,即可使用MindSpore Serving创建该模型的推理服务。MindSpore Serving详情可参考 serving: A lightweight and high-performance service module that helps MindSpore developers efficiently deploy online inference services in the production environment.

    2.    最简单直接的将模型部署在Serving服务器

    ResNet-50模型部署为案例,运行本Serving样例所需文件和目录结构如下,其中serving_client.py和test_image为客户端运行所需。

    1. ├── resnet50
    2. │ ├── 1
    3. │ │ └── resnet50_1b_cifar10.mindir
    4. │ └── servable_config.py
    5. │── serving_server.py
    6. ├── serving_client.py
    7. └── test_image
    8. ├── airplane.jpg 48KB
    9. ├── car.jpg 79KB
    10. ├── cat.jpg 330KB
    11. └── horse.jpg 83KB

    其中

    • resnet50_1b_cifar10.mindir为MindSpore训练后导出的模型,导出脚本可参考导出resnet50模型
    • 目录1下表示为版本1的模型。
    • servable_config.py为模型配置脚本,声明模型文件,定义模型的输入输出处理流程。
    • serving_server.py为服务启动脚本,指定模型部署的设备,启动gRPC或RESTful服务器。
    • serving_client.py为客户单脚本,读取图片,向客户端发送请求,获取推理服务结果并打印。
    • test_image中有大小不同的各类图片,图片较大时预处理时间较长。

    先以最简单的方式配置和部署resnet50模型。

    servable_config.py定义,通过declare_model声明了模型文件,定义了predict方法,输入为image,传给模型,输出为label,返回自模型结果。

    1. from mindspore_serving.server import register
    2. resnet_model = register.declare_model(model_file="resnet50_1b_cifar10.mindir", model_format="MindIR")
    3. @register.register_method(output_names=["label"])
    4. def predict(image):
    5. x = register.add_stage(resnet_model, image, outputs_count=1)
    6. return x

    serving_server.py定义,部署本地目录下的resent50到设备0,并启动地址为127.0.0.1:5500的gRPC服务器:

    1. import os
    2. import sys
    3. from mindspore_serving import server
    4. def start():
    5. servable_dir = os.path.dirname(os.path.realpath(sys.argv[0]))
    6. config = server.ServableStartConfig(servable_directory=servable_dir, servable_name="resnet50", device_ids=0)
    7. server.start_servables(config)
    8. server.start_grpc_server("127.0.0.1:5500")
    9. if __name__ == "__main__":
    10. start()

    serving_client.py定义,客户端读取两张图片,使用MindData进行预处理,将预处理结果发送给Serving服务器,对服务器返回的结果进行后处理,打印图片的便签:

    1. import os
    2. from mindspore_serving.client import Client
    3. import numpy as np
    4. import mindspore.dataset.vision.c_transforms as VC
    5. # cifar 10
    6. idx_2_label = ['airplane', 'automobile', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck']
    7. def preprocess(image):
    8. image_size = 224
    9. mean = [0.4914 * 255, 0.4822 * 255, 0.4465 * 255]
    10. std = [0.2023 * 255, 0.1994 * 255, 0.2010 * 255]
    11. decode = VC.Decode()
    12. resize = VC.Resize([image_size, image_size])
    13. normalize = VC.Normalize(mean=mean, std=std)
    14. hwc2chw = VC.HWC2CHW()
    15. image = decode(image)
    16. image = resize(image)
    17. image = normalize(image)
    18. image = hwc2chw(image)
    19. return image
    20. def postprocess(score):
    21. max_idx = np.argmax(score)
    22. return idx_2_label[max_idx]
    23. def read_images():
    24. """Read images for directory test_image"""
    25. image_files = []
    26. images_buffer = []
    27. for path, _, file_list in os.walk("./test_image/"):
    28. for file_name in file_list:
    29. image_file = os.path.join(path, file_name)
    30. image_files.append(image_file)
    31. for image_file in image_files:
    32. with open(image_file, "rb") as fp:
    33. images_buffer.append(fp.read())
    34. return image_files, images_buffer
    35. def predict(image_files, images_buffer):
    36. client = Client("localhost:5500", "resnet50", "predict")
    37. instances = []
    38. for image in images_buffer:
    39. image = preprocess(image)
    40. instances.append({"image": image})
    41. result = client.infer(instances)
    42. for file, instance in zip(image_files, result):
    43. score = instance["score"]
    44. label = postprocess(score)
    45. print(f"{file}, label: {label}")
    46. if __name__ == '__main__':
    47. image_files, images_buffer = read_images()
    48. predict(image_files, images_buffer)

    执行serving_client.py

    1. ./test_image/car.jpg, label: automobile
    2. ./test_image/horse.jpg, label: horse
    3. ./test_image/cat.jpg, label: cat
    4. ./test_image/airplane.jpg, label: airplane

    我们重复进行上述过程的图片预处理、推理和后处理10次,每次100张图片(images_buffer *25共100张),平均每次耗时1788ms,平均每张图片17.88ms

    3.   预处理、后处理后置Serving服务器

    我们可以将预处理、后处理后置到Serving服务器,以减少客户端和服务器通信数据量,其次使能Serving服务器推理与预处理、后处理并发处理能力。

    我们将客户端serving_client.py中的预处理、后处理挪到服务器Servable_config.py中。

    服务器Servable_config.py:

    1. from mindspore_serving.server import register
    2. import numpy as np
    3. import mindspore.dataset.vision.c_transforms as VC
    4. resnet_model = register.declare_model(model_file="resnet50_1b_cifar10.mindir", model_format="MindIR")
    5. # cifar 10
    6. idx_2_label = ['airplane', 'automobile', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck']
    7. def preprocess(image):
    8. image_size = 224
    9. mean = [0.4914 * 255, 0.4822 * 255, 0.4465 * 255]
    10. std = [0.2023 * 255, 0.1994 * 255, 0.2010 * 255]
    11. decode = VC.Decode()
    12. resize = VC.Resize([image_size, image_size])
    13. normalize = VC.Normalize(mean=mean, std=std)
    14. hwc2chw = VC.HWC2CHW()
    15. image = decode(image)
    16. image = resize(image)
    17. image = normalize(image)
    18. image = hwc2chw(image)
    19. return image
    20. def postprocess(score):
    21. max_idx = np.argmax(score)
    22. return idx_2_label[max_idx]
    23. @register.register_method(output_names=["label"])
    24. def predict(image):
    25. image = register.add_stage(preprocess, image, outputs_count=1)
    26. score = register.add_stage(resnet_model, image, outputs_count=1)
    27. label = register.add_stage(postprocess, score, outputs_count=1)
    28. return label

    客户端serving_client.py

    1. import os
    2. import time
    3. from mindspore_serving.client import Client
    4. def read_images():
    5. """Read images for directory test_image"""
    6. image_files = []
    7. images_buffer = []
    8. for path, _, file_list in os.walk("./test_image/"):
    9. for file_name in file_list:
    10. image_file = os.path.join(path, file_name)
    11. image_files.append(image_file)
    12. for image_file in image_files:
    13. with open(image_file, "rb") as fp:
    14. images_buffer.append(fp.read())
    15. return image_files, images_buffer
    16. def predict(image_files, images_buffer):
    17. client = Client("localhost:5500", "resnet50", "predict")
    18. instances = []
    19. for image in images_buffer:
    20. instances.append({"image": image})
    21. result = client.infer(instances)
    22. for file, instance in zip(image_files, result):
    23. label = instance["label"]
    24. print(f"{file}, label: {label}")
    25. if __name__ == '__main__':
    26. image_files, images_buffer = read_images()
    27. predict(image_files, images_buffer)

    我们重复进行上述过程的图片预处理、推理和后处理10次,每次100张图片(images_buffer *25共100张),平均每次耗时1343ms,平均每张图片13.43ms

    可以看到将预处理、后处理挪至服务器侧,降低客户端和服务器通信,利用Serving服务器预处理、后处理与模型推理的并发能力,可以显著提升推理吞吐量,降低每张图片的平均处理时延。

    4.    多卡并发部署

    如果设备存在多卡,我们可利用多卡并发能力,提升吞吐量。
    我们更新serving_server.py中的部署脚本,修改代码部署到卡0和卡1(device_ids=(0,1))。

    1. import os
    2. import sys
    3. from mindspore_serving import server
    4. def start():
    5. servable_dir = os.path.dirname(os.path.realpath(sys.argv[0]))
    6. config = server.ServableStartConfig(servable_directory=servable_dir, servable_name="resnet50", device_ids=(0,1))
    7. server.start_servables(config)
    8. server.start_grpc_server("127.0.0.1:5500")
    9. if __name__ == "__main__":
    10. start()

    我们重复进行上述过程的图片预处理、推理和后处理10次,每次100张图片(images_buffer *25共100张),平均每次耗时692.7ms,平均每张图片6.93ms
    可以看到增加设备数量,可以显著提升推理吞吐量,降低每张图片的平均处理时延。

    5.    额外进程提升Python预处理、后处理性能

    由于Python GIL的存在,Python实现的预处理和后处理在一个进程内无法通过多线程实现并发,如果Python任务的处理时间大于模型推理时间,有必要增加Python进程并发处理Python任务。

    1)获取Python任务和模型推理执行时间

    启动serving_server.py前,打开GLOG_v设置INFO级别日志(export GLOG_v=1)。
    启动服务器,运行客户端,接着查看worker日志serving_logs/我们可以观察到,
    测试的四张图片每张图片差异不同,预处理大约耗时4~22ms,平均每张大约12ms。

    1. method predict stage 1 function resnet50.preprocess get result 0 ~ 0 cost time 11.744976043701172 ms
    2. method predict stage 1 function resnet50.preprocess get result 0 ~ 0 cost time 7.365942001342773 ms
    3. method predict stage 1 function resnet50.preprocess get result 0 ~ 0 cost time 22.826194763183594 ms
    4. method predict stage 1 function resnet50.preprocess get result 0 ~ 0 cost time 3.9374828338623047 ms

    推理大约耗时2.15ms每张图片。

    Model resnet50_1b_cifar10.mindir InvokePredict Time Cost # 2.15127 ms

    后处理大约耗时0.32ms每张图片。

    method predict stage 3 function resnet50.postprocess get result 0 ~ 0 cost time 0.32401084899902344 ms

    Python任务预处理+后处理时间已经明显大约推理时间6倍。

    2)增加额外的处理Python任务的进程

    我们仅将模型部署到设备0,1: device_ids=(0,1),增加4个额外的worker进程处理Python任务,共6个并行的worker: num_parallel_workers=6。
    在serving_server.py中:

    1. import os
    2. import sys
    3. from mindspore_serving import server
    4. def start():
    5. servable_dir = os.path.dirname(os.path.realpath(sys.argv[0]))
    6. config = server.ServableStartConfig(servable_directory=servable_dir, servable_name="resnet50", device_ids=(0,1), num_parallel_workers=6)
    7. server.start_servables(config)
    8. server.start_grpc_server("127.0.0.1:5500")
    9. if __name__ == "__main__":
    10. start()

    将日志级别恢复默认警告级别,unset GLOG_v,启动serving服务器。

    我们重复进行上述过程的图片预处理、推理和后处理10次,每次100张图片(images_buffer *25共100张),平均每次耗时349ms,平均每张图片3.49ms
    当Python任务处理时间大于模型推理时间,可以看到增加额外的Python任务处理进程,可以显著提升推理吞吐量,降低每张图片的平均处理时延。

    3) 相关运行结构图

    存在额外Python任务进程,Serving运行结构图如下所示,图中,配置了2个Worker分别占用设备0和1(device_ids),可以处理模型推理和Python任务,配置了1个额外Worker,仅处理Python任务,共3(num_parallel_workers)个Worker:

    6.    增加模型的batch_size

    Serving当前不支持动态batch_size,动态batch_size开发中,模型的batch_size需用用户模型导出时指定。如果当增加模型batch size,平均每个batch的处理时间变小,则可权衡吞吐量和时延,适当增加模型的batch size。
    由于本例子中,Python预处理时间为主要时间,模型推理时间较短,每张图片总体推理时间受batch_size影响不大。 

  • 相关阅读:
    java InputSreamReader类、OutputStreamWriter类
    【数据中台商业化】数据中台微前端实践
    vue多条件查询
    JAVA全局异常处理
    Centos7 安装JDK1.8详细过程
    c++编程(18)——deque的模拟实现(2)容器篇
    小程序线上调试优化
    AMS的启动
    C语言——求1/1-1/2+1/3-......+1/99-1/100的值
    rod disable-dev-shm-usage 踩坑
  • 原文地址:https://blog.csdn.net/Kenji_Shinji/article/details/126867262