「导语」在模型训练完成后,我们需要使用保存后的模型进行线上预测,即模型 Serving 服务。 TensorFlow 团队提供了专门用于模型预测的服务系统 TensorFlow Serving,它专为生产环境设计,具备高性能且具有很强大的灵活性,本文将从服务搭建,服务配置,远程访问等多个方面对 TensorFlow Serving 进行详细地介绍。
Serving 服务搭建
官方推荐使用 Docker
来完成 TensorFlow Serving
服务的搭建,这是最容易也是最直接的搭建方式。为便于操作,本文也采取了这种搭建方式。
在搭建 Serving
服务时,首先要确保系统已经安装了 Docker
程序,然后通过命令 docker pull tensorflow/serving:version
来获取指定版本的 TensorFlow Serving
镜像,接着就可以使用该镜像来启动 Serving
服务了。
需要注意的是 TensorFlow Serving
服务的版本最好与训练时用到的 TensorFlow
版本保持一致,以减少不必要的兼容性问题。可以从 dockerhub
网站查询该镜像的所有可用版本,比如稳定版 (latest
) ,指定版本 (2.2.0
) 或者每天更新版 (nightly
) 等。
如果要使用 GPU
来支持模型的预测服务,则需要获取 TensorFlow Serving
镜像相应的 GPU
版本 (docker pull tensorflow/serving:version-gpu
) 并且要配置好本地的 GPU
环境。
单模型 Serving 服务
当我们训练完成并保存好一个模型后,可以很方便地启动一个 Serving
服务来进行测试与验证。
首先将保存的 SavedModel
格式的模型文件放置于本地的 /path/to/tfs/models/model/version
目录下,该目录的结构如下所示:
model/ |
其中 model
表示将要加载的模型名称, 0
表示该模型的可用版本,这里只有一个版本,也可以存在多个版本。版本目录下的文件即为 SavedModel
格式的文件,在模型保存
一文中已经对这些文件存储的内容做过介绍,这里就不再赘述。
然后使用以下命令即可启动一个 Serving
服务:
docker run -d -p 8501:8501 \ |
该 Serving
服务仅加载了一个模型,模型的名称为 model
,版本号为 0
。搭载该 Serving
服务的容器暴露了 8501
端口,用于接收 HTTP
接口的访问请求。
我们可以在容器启动时通过设置容器内的环境变量 MODEL_BASE_PATH
来指定模型的存储路径,该环境变量的默认值为 /models
,表示从容器内的 /models
路径加载模型。同样地,可以通过 MODEL_NAME
环境变量来指定要加载的模型名称,该环境变量的默认值为 model
。基于环境变量的 Serving
服务启动命令如下所示:
docker run -d -p 8501:8501 \ |
除了设置容器内的环境变量,我们还可以使用 Serving
服务的启动参数 --model_base_path
和 --model_name
来分别指定模型的存储路径和模型名称。启动命令如下所示:
docker run -d -p 8501:8501 \ |
在 Serving
服务成功启动后,可以通过访问特定的 HTTP
接口来查看模型是否加载成功。其请求和响应的示例如下所示:
curl http://localhost:8501/v1/models/model |
当模型状态为 AVAILABLE
时表示该模型的相应版本已经成功加载,并且已经准备好提供 Serving
服务了。如果调用该接口返回错误或没有返回,则说明模型加载出现问题,此时可以根据返回的错误信息或者使用 docker logs containerid
查看容器的日志以定位问题。
需要注意的是,如果模型的存储路径下包含多个模型版本,则 Serving
服务默认会加载最大版本号对应的模型,也就是说一个模型同时只能有一个版本提供 Serving
服务。
多模型 Serving 服务
一般而言,线上的 Serving
服务中会同时部署多个模型或者模型的多个版本,以供 A/B Test
测试使用,进而区分不同模型的实际运行效果。而上一节介绍的单模型 Serving
服务由于其模型名称和加载的模型版本都是固定的,因此并不能满足这一需求,此时可以使用带有配置文件 (models.config
) 的 Serving
服务来实现上述目标。
模型配置文件的内容是基于 ModelServerConfig 结构生成的, ModelServerConfig 是 model_server_config.proto
文件中定义的一种数据结构 (message
) ,具体定义信息可参见其源码。该配置文件的基础
示例如下所示:
model_config_list { |
配置文件中的每一个 config
都对应于一个将要被加载的模型,其中 base_path
对应于该模型的加载路径, name
对应于模型的名称,如果该路径中存在多个版本的模型文件,那么 TensorFlow Serving
还是会选择最大版本号的模型来提供 Serving
服务。
将上述配置文件放置于 /path/to/tfs/config
目录并使用如下命令即可启动一个多模型的 TensorFlow Serving
服务:
docker run -d -p 8501:8501 \ |
同理在 Serving
服务启动后可以使用 HTTP
接口来查看各个模型的加载信息:
curl http://localhost:8501/v1/models/first_model |
此时我们就可以使用多个模型来同时进行线上 Serving
服务了。
Serving 服务配置
模型配置
models.config
配置文件除了可以指定多个模型的名称和加载路径以外,还可以指定模型的版本策略 (version policy
)、版本的标签 (labels
) 以及请求日志的记录 (logging
) 方式等,为我们提供了极大的便利。
在同一模型路径下, Serving
服务会默认加载版本号最大的那个模型,而通过修改配置文件中的 model_version_policy
配置项我们可以指定要加载的模型版本。假如我们要使用版本号为 N
的模型提供 Serving
服务,首先需要将 model_version_policy
策略设置为 specific
,然后在 specific
中提供相应的版本号 N
即可。指定版本号的模型配置如下所示:
config { |
这种配置选项在最新版本的模型 Serving
出现问题需要回滚到一个已知的较好版本时会比较有用。
如果需要同时使用多个版本的模型来提供 Serving
服务,也是同样地设置方式,只不过需要在 specific
中提供多个版本号。模型配置如下所示:
config { |
有时为模型的版本号指定一个别名或标签会很有帮助,这样客户端在调用时就不必知道具体的版本号,直接访问对应的标签即可。比如我们可以设置一个稳定版 (stable
) 标签,它可以指向任一版本号的模型,客户端只需访问 stable
标签即可获取最新 Serving
服务。如果有新的稳定版本,则只需要修改 stable
标签指向的版本号,而客户端对这次改变是无感知的,因此也无需做任何修改。注意只有通过 gRPC
接口访问时才可以使用标签功能,HTTP
接口则不支持。
带有标签的模型配置如下所示:
config { |
如果模型需要回滚,则只需要修改配置文件中相应标签对应的版本号即可。
注意只有模型版本已被加载的且该模型版本的状态为 AVAILABLE
时,才能给该模型版本指定一个标签。如果需要给一个尚未加载的模型版本指定标签(比如在 Serving
启动时,配置文件中同时提供了模型版本和标签),则需要将 --allow_version_labels_for_unavailable_models
启动参数设置为 true
。
而上述启动参数,只适用于为新标签指定版本号。如果要给一个已在使用的标签重新分配版本,则必须将其分配给一个已加载的模型版本,这样可以避免在版本切换时发送到该标签的请求失效而造成线上故障。因此,如果要将标签 stable
从版本 N
重新分配到版本 N + 1
,则必须首先提交同时包含版本 N
和版本 N + 1
的配置,待版本 N + 1
加载成功后,再提交将标签 stable
指向版本 N + 1
的配置以完成标签的更新操作。
监控配置
在线上 Serving
时,我们需要了解 Serving
服务的各项指标,以便于我们及时发现问题并作出调整。
我们可以使用 --monitoring_config_file
参数来启用 Serving
服务的监控指标获取功能。该参数需要指定一个配置文件,其内容对应于 monitoring_config.proto
文件中定义的 MonitoringConfig 数据结构 (message
) ,具体定义可参见源码。配置文件的内容如下所示:
prometheus_config { |
其中 enable
表示是否开启监控, path
表示获取监控指标的 uri
。将上述配置保存到 monitor.config
文件中并将该文件放置于 /path/to/tfs/config
目录下,然后使用如下命令即可启动带有监控指标的 Serving
服务:
docker run -d -p 8501:8501 \ |
访问 http://localhost:8501/metrics
即可看到 TensorFlow Serving
服务的实时监控指标信息。部分监控指标如下所示:
TYPE :tensorflow:api:op:using_fake_quantization gauge |
为了更直观的监控这些指标,可以将所有的指标都采集到 Prometheus
并使用 Grafana
进行可视化展示。部分指标可视化的结果如下图所示:
批处理配置
Serving
服务可以对客户端的请求进行批处理,以实现更好的吞吐量。对于 Serving
服务加载的所有模型和模型版本,批处理操作将会在全局范围内进行统一调度,以确保可以最大程度的利用基础计算资源。可以使用 --enable_batching
参数来启用批处理操作,并使用 --batching_parameters_file
启动参数指定批处理参数配置文件 (batching.config)
来对与批处理有关的参数进行配置。
批处理参数配置文件的内容如下所示:
max_batch_size { value: 128 } |
其中 max_batch_size
参数用来控制吞吐量和延迟之间的权衡,同时它也可以防止因 batch
过大超出了资源限制而导致请求失败。 batch_timeout_micros
参数表示执行一次批处理操作之前所要等待的最长时间(即使可能尚未达到 max_batch_size
也会执行),主要用于控制尾部延迟。 num_batch_threads
表示并行执行批处理操作的最大数量。 max_enqueued_batches
表示可以进入调度程序队列的批处理任务的数量,用来对排队带来的延迟进行限制。
将 batching.config
文件并放置于 /path/to/tfs/config
目录下,然后使用以下命令即可启动一个带有批处理操作的 Serving
服务:
docker run -d -p 8501:8501 \ |
注意在调整 max_batch_size
和 max_enqueued_batches
参数时,不能设置的过大,避免因内存占用过高而导致内存溢出,从而影响线上的 Serving
服务。
服务启动参数
除了上面介绍过的一些 Serving
服务启动参数外,还有其它一些启动参数需要了解,使用这些参数可以对部署的 Serving
服务进行微调,以达到想要的效果。下面对其中一些比较重要的启动参数做简要介绍:
--port
:gRPC
服务的监听端口,默认为8500
端口。--rest_api_port
:HTTP
服务的监听端口,默认为8501
端口。--rest_api_timeout_in_ms
:HTTP
请求的超时限制(毫秒),默认为30000
毫秒。--model_config_file
:模型配置文件路径。--model_config_file_poll_wait_seconds
:定期加载模型配置文件的时间间隔(秒),默认为0
,表示Serving
服务启动后就不再定期加载。--file_system_poll_wait_seconds
:定期加载新模型版本的时间间隔(秒),默认为1
秒。--enable_batching
:bool
,是否开启请求批处理操作,默认为false
。--batching_parameters_file
:批处理操作的参数配置文件路径。--monitoring_config_file
:用于监控的配置文件路径。--allow_version_labels_for_unavailable_models
:bool
,是否可以对尚未加载的模型分配标签,默认为false
。--model_name
:模型名称,在单模型Serving
时使用。--model_base_path
:模型加载路径,在单模型Serving
时使用。--enable_model_warmup
:bool
,是否使用数据对模型进行预热,以减少第一次请求的响应延迟,默认为false
。因为TensorFlow
运行时具有延迟初始化的组件,这可能导致在模型加载后发送给模型的第一次请求的响应时间比较长,而且这个延迟的时间可能比单个请求所需的时间高好几个数量级。为了减小延迟初始化对请求响应的影响,可以在模型加载时提供一组请求样本来触发组件的初始化,这个过程被称为模型预热
。- 更多的启动参数可以参见源码。
完整示例
一个配置齐全的多模型 Serving
服务的启动方式如下所示:
docker run -d -p 8500:8500 -p 8501:8501 \ |
HTTP 接口访问
我们可以通过访问 Serving
服务的 HTTP
接口来查询模型信息,包括模型的加载状态,模型的元数据以及模型的预测结果等。
所有的请求和响应的 Body
都是 json
格式的,对于错误的请求, Serving
服务会返回如下格式的错误信息:
{ |
模型状态
访问 http://host:port/v1/models/${MODEL_NAME}[/versions/${MODEL_VERSION}]
接口可以获取模型的指定版本的状态信息,其中 /versions/${MODEL_VERSION}
是可选部分,如果不指定则会返回该模型所有版本的状态信息。请求和响应示例如下所示:
curl http://localhost:8501/v1/models/first_model/versions/0 |
模型 Metadata
访问 http://host:port/v1/models/${MODEL_NAME}[/versions/${MODEL_VERSION}]/metadata
接口可以获取模型的指定版本的元数据信息。其中 /versions/${MODEL_VERSION}
是可选部分,如果不指定则会返回该模型最新版本的元数据。请求和响应示例如下所示:
curl http://localhost:8501/v1/models/first_model/versions/0/metadata |
元数据信息中包含了该模型输入张量的数据类型和维度信息,还包含了输入张量的名称 (key
) ,这里为 input_1
,这对于后面使用数据进行模型预测十分重要,因为数据的 input_key
以及输入格式必须要与 metadata
中的定义保持一致。
模型预测
访问 http://host:port/v1/models/${MODEL_NAME}[/versions/${MODEL_VERSION}]:predict
接口可以获取指定输入数据的预测值,即给定模型的数据输入,获取模型的预测输出。 其中 /versions/${MODEL_VERSION}
是可选部分,如果不指定则会默认使用最新版本的模型进行预测。
访问该接口需要提供一个 json
格式的请求 Body
,其格式如下所示:
{ |
其中 signature_name
表示模型的签名,如果不指定,则默认为 serving_default
。 instances
和 inputs
表示模型的输入,在请求的 Body
中必须指定其中的一个,它们后接指定格式的输入数据。
instances
表示以行的形式提供模型的输入数据,其 json
格式的请求如下所示:
{ |
当模型仅有一个带名称的输入时,也可以省略 input_key
,其 json
格式的请求如下所示:
{ |
利用 curl
并使用上述方式进行预测请求的示例如下所示:
curl -d '{"instances": [{"input_1": [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]}]}' -X POST http://localhost:8501/v1/models/first_model:predict |
这里响应结果中 json
的 key
为 predictions
, value
值为一个列表,对应于每一个样本的输出值。由于模型只有一个输出张量,所以这里省略了输出的 key
值 output_1
。
inputs
表示以列的形式提供模型的输入数据,其 json
格式的请求如下所示:
{ |
同样地,当模型仅有一个带名称的输入时,也可以省略 input_key
,其 json
格式的请求如下所示:
{ |
利用 curl
并使用上述方式进行预测请求的示例如下所示:
curl -d '{"inputs": {"input_1": [[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]]}}' -X POST http://localhost:8501/v1/models/first_model:predict |
这里响应 json
的 key
为 outputs
, value
为一个列表,对应于每一个样本的输出值。由于模型只有一个输出张量,所以这里也省略了输出的 key
值 output_1
。
对于有多个输入张量和多个输出张量的模型, instances
输入方式的请求和响应示例如下所示:
{ |
{ |
inputs
输入方式的请求和响应示例如下所示:
{ |
{ |
总结一下这两种输入方式, instances
的输入数据可以理解为是字典 (dict
) 组成的列表 (list
) ,列表中的每一个字典元素都是一个输入样本,字典的 key
为模型输入张量的名称, value
为张量对应的数值,这也是我们容易理解的输入方式;而 inputs
的输入数据可以理解为是纯字典,字典的 key
为模型输入张量的名称,而 value
是一个列表,列表中的每一个元素都是一个数据样本对应的值。另外,对于每种输入方式,其响应输出的格式也是同理。
gRPC 远程调用
除了可以使用 HTTP
接口进行模型访问外, TensorFlow Serving
还提供了 gRPC
接口来更高效地完成模型的访问请求。
目前官方仅提供了 Python
语言对应的 gRPC API
,如需使用,首先要安装与 TensorFlow Serving
版本相对应的 python
包 tensorflow-serving-api
: pip install tensorflow-serving-api==2.2.0
,然后使用它提供的 API
接口来进行 gRPC
请求。
使用 gRPC
请求 Serving
服务的 Python
版示例代码如下所示:
import grpc |
上述代码执行流程如下:
- 首先要创建一个
gRPC
通道 (channel
) ,并使用该通道初始化一个客户端存根 (stub
) ,用于调用远程的Predict
函数。 - 然后要初始化一个请求对象
request
,并且需要指定该请求对象的一些属性值,比如请求的模型名称,模型的版本以及输入张量的数据等等。 - 最后使用
stub
将该请求发送给Serving
服务,并返回结果。 - 如果模型的
outputs
的key
值有多个,可以通过output_filter
来筛选指定的输出。另外可以通过output
的float_val
属性来获取返回结果的数值。
执行上述代码,打印的结果如下所示:
outputs { |
在真实的线上环境中,数据预测往往是一个独立的服务,它从客户端接收数据请求,然后将数据转发给 Serving
服务以获取预测值,最后将预测的结果返回。而这项服务会涉及到客户端的高并发请求,因此我们可能不会使用 Python
框架来完成这项任务,而往往会选择一些更加成熟的,具有高性能的后端框架来做请求转发,比如 Go
语言的 gin
等 web
框架,它们高并发的场景下会有比较优异的表现。
为了使用其它语言的高性能框架,我们需要安装该语言的 TensorFlow Serving gRPC API
库来进行 Serving
请求,而官方并没有提供除 Python
以外的其它语言的 gPRC API
,此时就需要我们基于源码中的 .proto
文件来生成相应的 API
代码文件。
API
代码文件的生成工作可以交由 protoc
工具来完成,需要注意的是, TensorFlow Serving
源码中的部分 .proto
文件会依赖于 TensorFlow
源码中的一些 .proto
文件,所以在使用 protoc
编译代码文件时,需要将 TensorFlow
和 TensorFlow Serving
两者的代码都拉取到本地进行统一生成。
生成了相应语言版本的 API
代码文件后,即可使用与 Python
相似的 gRPC
接口来访问 Serving
服务了。
模型本地测试
在保存了 SavedModel
格式的模型之后,除了使用 TensorFlow Serving
服务加载模型来进行验证外,还可以使用 SavedModel
命令行工具 (CLI
) 来直接检查保存的模型。 CLI
可以使你快速地确认 SavedModel
中输入及输出张量的数据类型以及它们的维度是否与模型定义中的张量相匹配。另外,还可以使用 CLI
进行简单的数据测试,通过向模型传递样本数据并获取模型的输出以验证该模型的可用性。
CLI
工具一般在安装 TensorFlow
时就已经随之一起安装在系统中了,它是一个可执行程序,位于 TensorFlow
安装目录的 bin
目录下,名为 saved_model_cli
。可以直接使用 saved_model_cli -h
来查看该 CLI
工具的使用方法。
saved_model_cli
有两种常用的操作,分别为 show
和 run
。
Show 模型信息
show
操作表示查看 SavedModel
的基本信息,与访问 HTTP
的 metadata
接口获取元数据信息类似。它的使用方法如下所示:
saved_model_cli show [-h] --dir DIR [--all] [--tag_set TAG_SET] [--signature_def SIGNATURE_DEF_KEY] |
比如要查看版本号为 0
的 first_model
模型的基本信息,可以通过如下命令获取:
saved_model_cli show --dir /models/first_model/0 --tag_set serve --signature_def serving_default |
Run 模型运算
run
操作表示执行一次模型的计算操作,给定输入数据然后返回输出结果。它的使用方法如下所示:
saved_model_cli run [-h] --dir DIR --tag_set TAG_SET --signature_def SIGNATURE_DEF_KEY |
run
操作提供了 3
种数据输入的方式,它们分别为 --inputs
, --input_exprs
以及 --input_examples
,在执行 run
操作时,必须提供这 3
种输入方式中的 1
种。
--inputs INPUTS
表示从文件中读取numpy
数组作为输入数据,INPUTS
可以是input_key=filename
和input_key=filename[variable_name]
中的任意1
种格式,filename
的文件格式可以是.npy
,.npz
或pickle
中的一种,saved_model_cli
会使用numpy.load
方法去加载该文件。当文件格式为
.npy
时,文件中的数组会直接作为input_key
的输入数据。当文件格式为
.npz
时,如果指定了variable_name
则会加载.npz
文件中名为的variable_name
的.npy
文件作为input_key
的输入数据,如果没有指定variable_name
则会加载.npz
的任意一个.npy
文件作为input_key
的输入数据。当文件格式为
pickle
时,如果指定了variable_name
那么saved_model_cli
会假设pickle
文件中存储的数据为一个字典,然后读取variable_name
对应的value
值作为input_key
的输入数据,如果没有指定variable_name
, 那么会将pickle
文件中的所有内容作为input_key
的输入数据。--input_exprs INPUT_EXPRS
表示使用Python
的表达式作为输入数据,在需要使用一些简单的样本数据对SavedModel
模型做测试时比较有用。其中INPUT_EXPRS
既可以是简单的list
如input_key=[1, 1, 1]
也可以是numpy
函数,如input_key=np.ones((1, 3))
--input_examples INPUT_EXAMPLES
表示使用tf.train.Example
作为输入数据。其中INPUT_EXAMPLES
的格式为input_key=[{"age":[22,24],"education":["BS","MS"]}]
,input_key
的值是一个字典 (dict
) 的列表 (list
) ,字典的key
为模型输入特征的名称,value
为每一个特征的数值列表 (list
) 。能否使用该种数据输入方式,需要根据SavedModel
模型的基本信息来决定。
一般而言,使用 --input_exprs INPUTS
指定输入数据是最快速、最便捷的校验 SavedModel
模型可用性的方式,其使用方法如下所示:
saved_model_cli run --dir /models/first_model/0 --tag_set serve --signature_def serving_default --input_expr "input_1=np.ones((1,31))" |
参考资料
- TensorFlow Serving with Docker
- TensorFlow Serving Configuration
- TensorFlow Serving RESTful API
- SavedModel Command Line Interface
- Python GRPC Client Example