如何使用 tensorflowServing 进行模型部署

TensorFlow Serving 是用于机器学习的灵活,高性能的服务系统,针对生产环境而设计。 TensorFlow 服务 可以轻松部署新算法和实验,同时保持不变服务器体系结构和 API。TensorFlow Serving 开箱即用 与 TensorFlow 模型集成,但可以轻松扩展以服务于其他 模型类型

关键概念

Key Conception

serving_architecture

Loaders

Loaders 管理 Servables 的生命周期。Loader API 是一种支持独立于特定算法,数据或产品用例的通用基础架构。具体来说,Loaders 标准化了用于加载和卸载 Servable 的 API。

Sources

Sources 是可以寻找和提供 Servables 的模块,每个 Source 提供了 0 个或者多个 Servable streams,对于每个 Servable stream,Source 都会提供一个 Loader 实例。

Managers

管理 Servable 的整个的生命周期,包括:

  • loading Servables
  • serving Servables
  • unloading Servables

Managers 监听 Sources 并跟踪所有版本。Managers 尝试满足、响应 Sources 的请求,但是如果所请求的资源不可用,可能会拒绝加载相应版本。Managers 也可以推迟 “卸载”。例如,Managers 可能会等待到较新的版本完成加载之后再卸载(基于保证始终至少加载一个版本的策略)。

Servables

Servable 是 Tensorflow Serving 的核心抽象,是客户端用于执行计算的基础对象,其大小和粒度是灵活的。Tensorflow serving 可以在单个实例的生命周期内处理一个或多个版本的 Servable,这样既可以随时加载新的算法配置,权重或其他数据;也能够同时加载多个版本的 Servable,支持逐步发布和实验。由此产生另外一个概念:Servable stream,即是指 Servable 的版本序列,按版本号递增排序。Tensorflow Serving 将 model 表示为一个或者多个 Servables,一个 Servable 可能对应着模型的一部分,例如,a large lookup table 可以被许多 Tensorflow Serving 共享。另外,Servable 不管理自己的生命周期

Core

Tensorflow Serving core 负责管理 Servables 的 Lifecycle 和 metrics,将 Servables 和 loaders 看作黑箱 (opaque objects)。

广义地说:

  1. Sources create Loaders for Servable Versions.
  2. Loaders are sent as Aspired Versions to the Manager, which loads and serves them to client requests.

例子:

  1. Source 为指定的服务 (磁盘中检测模型权重的新版本) 创建 Loader,Loader 里包含了服务所需要的元数据(模型);
  2. Source 使用回调函数通知 Manager 的 Aspired Version (Servable version 的集合);
  3. Manager 根据配置的 Version Policy 决定下一步的操作(是否 unload 之前的 Servable,或者 load 新的 Servable);
  4. 如果 Manager 判定是操作安全的,就会给 Loader 要求的 resource 并让 Loader 加载新的版本;
  5. 客户端向 Manager 请求服务,可以指定服务版本或者只是请求最新的版本。Manager 返回服务端的处理结果;

Extensibility

Tensorflow Serving 提供了几个可扩展的 entry point,用户可以在其中添加自定义功能。

Version Policy

Version Policy (版本策略) 可以指定单个 Servable stream 中的版本加载和卸载顺序。它包括 Availability Preserving Policy(在卸载旧版本之前加载并准备好新版本)和 Resource Preserving Policy(在加载新版本之前先卸载旧版本)。

Source

New Sources 可以支持新的文件系统,云产品和算法后端,这主要和创建自定义 Source 有关。

Loaders

Loaers 是添加算法、数据后端的扩展点。Tensorflow 就是这样一种算法后端。例如,用户将实现一个新的 Loader,以便对新的 Servable 机器学习模型实例的访问和卸载。

Batcher

将多个请求批处理为单个请求可以显着降低计算成本,尤其是在存在诸如 GPU 的硬件加速器的情况下。Tensorflow Serving 包括一个请求批处理小部件,它允许客户端轻松地将请求中特定类型的计算进行批量处理。

系统环境搭建

系统及软硬件说明

系统:Ubuntu16.04

软件

  • 驱动 450.23.05
  • cuda 11.1
  • cudnn 8.0.5
  • tensorflow nightly-gpu(2.4)
  • python 3.7.9

硬件

  • RTX 3090

导出 Keras 模型

将 keras 中以 model.save(filepath) 保存的模型 h5 文件,转为 tensorflow 的 xx 格式,加载模型时,使用✔️tf.keras.models.load_model 而不是❌keras.models.load_model

1
2
3
4
5
6
7
8
from keras import backend as K
from keras.models import load_model
import tensorflow as tf

# 首先使用tf.keras的load_model来导入模型h5文件
model_path = 'v3_resnet50_unet.h5'
model = tf.keras.models.load_model(model_path, custom_objects=dependencies)
model.save('deploy/tfs/0', save_format='tf') # 导出tf格式的模型文件

导出之后,有以下目录结构

image-20210205165552193

导出之后,使用以下命令查看模型的 signature、input、output,后续客户端调用需要这些信息。

1
saved_model_cli show --dir tfs/0/ --all
1
2
3
4
5
6
7
8
9
10
11
12
signature_def['serving_default']:
The given SavedModel SignatureDef contains the following input(s):
inputs['input_1'] tensor_info:
dtype: DT_FLOAT
shape: (-1, 512, 1024, 3)
name: serving_default_input_1:0
The given SavedModel SignatureDef contains the following output(s):
outputs['activation_49'] tensor_info:
dtype: DT_FLOAT
shape: (-1, 524288, 1)
name: StatefulPartitionedCall:0
Method name is: tensorflow/serving/predict

以上可以确定,signature、input、output 分别为:serving_default,input_1,activation_49

saved_model_cli 为 tensorflow 的 python 工具包,位于 tensorflow/python/tools/saved_model_cli.py 下,一般安装了 tensorflow,可以直接找到该命令。

Docker 部署模型

拉取 tfs 的 docker 镜像

1. 安装 docker

安装过程参考官方安装文档 Install Docker Engine on Ubuntu

2. 安装 nvidia-docker

在 docker 上安装 nvidia 插件,以便使得应用在 GPU 上运行,安装过程参考:Installation Guide

3. 拉取 tfs 镜像

docker 官网上包含不同版本的 tfs 镜像,根据需求需要版本,使用以下命令拉取 tfs 镜像

1
sudo docker pull tensorflow/serving:nightly-gpu

启动 tfs 容器

使用以下命令启动 tfs 容器

1
2
3
4
5
sudo nvidia-docker run -p 8500:8500  \
-v "[path]/tfs:/models/resnet50_unet" \
-e MODEL_NAME=resnet50_unet \
-e CUDA_VISIBLE_DEVICES=1 \
-t 9e73a1470b72&

其中 9e73a1470b72 为 tfs 镜像的 id,可通过 docker image ls 查看

tfs 容器可用参数解释:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
--port = 8500                     用于侦听gRPC API的端口
--rest_api_port = 0 用于侦听HTTP / REST API的端口。如果设置为零,将不会导出HTTP
/ REST API。此端口必须与--port中指定的端口不同。
--rest_api_num_threads = 160 用于HTTP / REST API处理的线程数。如果未设置,
将根据CPU数量自动设置。
--rest_api_timeout_in_ms = 30000 HTTP / REST API调用超时。
--enable_batching = false bool 启用批处理
--batching_parameters_file =“” 字符串如果非空,请从提供的文件名读取ascii BatchingParameters
protobuf,并使用包含的值代替默认值。
--model_config_file =“” 字符串如果非空,请从提供的文件名读取ascii ModelServerConfig
协议,然后在该文件中提供模型。此配置文件可用于指定要使用的
多个模型以及其他高级参数,包括非默认版本策略。
(如果使用了--model_name和--model_base_path,则将被忽略。)
--model_name =“ default” 模型的字符串名称(如果设置了--model_config_file标志,则忽略
--model_base_path =“” 导出的字符串路径(如果设置了--model_config_file标志,
则忽略该字符串,否则为必需)
--file_system_poll_wait_seconds = 1 以秒为单位的两次新模型版本的文件系统每次轮询之间的间隔
--flush_filesystem_caches=true bool 如果为true(默认值),则在所有可服务对象的初始加载之后以及
随后的每个可服务对象重新加载之后(如果加载线程数为1),
将刷新文件系统缓存。如果在加载可服务对象之后访问模型文件,
则可以减少模型服务器的内存消耗,并以潜在的高速缓存未命中为 代价。
--tensorflow_session_parallelism=0 用于运行Tensorflow会话的线程数。默认情况下自动配置。请注意,
如果--platform_config_file为非空,则将忽略此选项。
--ssl_config_file =“” 字符串如果非空,请从提供的文件名读取ascii SSLConfig协议
并设置安全的gRPC通道
--platform_config_file =“” 字符串如果非空,请从提供的文件名读取ascii PlatformConfigMap
protobuf,然后使用该平台配置而不是Tensorflow平台。
(如果使用,则--enable_batching将被忽略。)
--per_process_gpu_memory_fraction=0.00 float每个进程占用GPU内存空间的分数,
该值介于0.0和1.0之间(默认值为0.0)。如果为1.0,则服务
器将在服务器启动时分配所有内存;如果为0.0,
则Tensorflow将自动选择一个值。
--saved_model_tags =“ serve” 字符串对应于要从SavedModel加载的元图def的逗号分隔的标记集。
--grpc_channel_arguments =“” 字符串要传递给grpc服务器的参数的逗号分隔列表。
(例如grpc.max_connection_age_ms = 2000)
--enable_model_warmup = true bool 启用模型预热,该预热在加载时触发延迟初始化(例如TF优化),
以减少第一个请求的延迟。
--version = false bool 显示版本

客户端的编写

基于 Python 编写客户端时,需要安装 tensorflow_serving、grpc 库包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import cv2
import grpc
import numpy as np
import os.path as ops
import tensorflow as tf

from tensorflow_serving.apis import predict_pb2, prediction_service_pb2_grpc

def request_server_grpc(img_resized, server_url):
'''
用于向TensorFlow Serving服务请求推理结果的函数。
:param img_resized: 经过预处理的待推理图片数组,numpy array,shape:(h, w, 3)
:param server_url: TensorFlow Serving的地址加端口,str,如:'0.0.0.0:8500'
:return: 模型返回的结果数组,numpy array
'''
# Request.
channel = grpc.insecure_channel(server_url)
stub = prediction_service_pb2_grpc.PredictionServiceStub(channel)
request = predict_pb2.PredictRequest()
request.model_spec.name = "resnet50_unet" # 模型名称,启动容器命令的model_name参数
request.model_spec.signature_name = "serving_default" # 签名名称,刚才叫你记下来的
# "input_1"是你导出模型时设置的输入名称,刚才叫你记下来的
request.inputs["input_1"].CopyFrom(
tf.make_tensor_proto(img_resized, shape=[1, ] + list(img_resized.shape)))
# print(request)

response = stub.Predict(request, 5.0) # 5 secs timeout
return np.asarray(response.outputs["activation_49"].float_val) # fc2为输出名称,刚才叫你记下来的

if __name__=='__main__':
img=cv2.imread([imgpath_xx],cv2.COLOR_BGR2RGB)

#预处理
res_image=cv2.resize(img,(1024,512))
res_image = res_image / 255
res_image=res_image.astype('float32')

#向tfs发送请求
port='8500'
server_url = r'0.0.0.0:'+port
response=request_server_grpc(res_image, server_url) #调用服务端

#处理返回结果
predict=response.copy()
#....后处理

Web 服务

1
TensorFlow模型的计算图,一般输入的类型都是张量,你需要提前把你的图像、文本或者其它数据先进行预处理,转换成张量才能输入到模型当中。而一般来说,这个数据预处理过程不会写进计算图里面,因此当你想使用TensorFlow Serving的时候,需要在客户端上写一大堆数据预处理代码,然后把张量通过gRPC发送到serving,最后接收结果。现实情况是你不可能要求每一个用户都要写一大堆预处理和后处理代码,用户只需使用简单POST一个请求,然后接收最终结果即可。因此,这些预处理和后处理代码必须由一个“中间人”来处理,这个“中间人”就是Web服务。

可以使用 Tornado 来搭建 web 服务

版本管理

待完善

应用例子

例子来源于美团技术团队基于 TensorFlow Serving 的深度学习在线预估

该例子针对广告精排的业务场景,使用 tfs 进行模型部署,针对高速的推断进行逐步的优化,并突破现有 tfs 的束缚,解决模型切换的毛刺问题,使得 tfs 部署后在性能上满足业务场景

性能优化措施

  • 请求端优化:使用 OpenMP 多线程并行处理请求,时间从 5ms 降低到 2ms

  • 构建模型的 ops 优化:分析构建模型的 ops 中的耗时操作,将其分离出去,或者使用低阶 API 替代高阶 API

  • XLA,JIT 优化:优化 Tensorflow 的计算图,剪除荣誉的计算

模型切换毛刺问题

模型切换时,大量的请求超时,原因有两个:一是更新、加载模型和处理请求的线程共用线程池,切换模型时无法处理请求;二是模型采用 Lazy Initialization 加载,第一次请求需要等待计算图初始化。

问题一的解决办法:

uint32 num_load_threads = 0; uint32 num_unload_threads = 0; 设置为 1,

问题 2 的解决办法:

模型加载后进行一次预热

参考资料

Tensorflow Serving 部署 tensorflow、keras 模型详解_jeffery0207 的博客 - CSDN 博客
TensorFlow Serving + Docker + Tornado 机器学习模型生产级快速部署 - 知乎
SignatureDefs in SavedModel for TensorFlow Serving | TFX
使用 tensorflow serving 部署 keras 模型(tensorflow 2.0.0) - 知乎
基于 TensorFlow Serving 的深度学习在线预估 - 美团技术团队
TensorFlow Serving 入门 - 简书
TensorFlow Serving 入门教程(Windows)_I’m George 的博客 - CSDN 博客
TensorFlow Serving 使用 及 部署_Eric’s Blog-CSDN 博客