Tensorflow已经成长为事实上的机器学习(ML)平台,在业界和研究领域都很流行。对Tensorflow的需求和支持促成了大量围绕训练和服务机器学习(ML)模型的OSS库、工具和框架。Tensorflow服务是一个构建在分布式生产环境中用于服务机器学习(ML)模型的推理方面的项目。
今天,我们将重点讨论通过优化预测服务器和客户机来提高延迟的技术。模型预测通常是“在线”操作(在关键的应用程序请求路径上),因此我们的主要优化目标是以尽可能低的延迟处理大量请求。
首先让我们快速概述一下Tensorflow服务。
Tensorflow Serving提供灵活的服务器架构,旨在部署和服务机器学习(ML)模型。一旦模型被训练过并准备用于预测,Tensorflow服务就需要将模型导出为Servable兼容格式。
Servable是封装Tensorflow对象的中心抽象。例如,模型可以表示为一个或多个可服务对象。因此,Servables是客户机用来执行计算(如推理)的底层对象。可服务的大小很重要,因为较小的模型使用更少的内存、更少的存储空间,并且将具有更快的加载时间。Servables希望模型采用SavedModel格式,以便使用Predict API加载和服务。
Tensorflow Serving将核心服务组件放在一起,构建一个gRPC/HTTP服务器,该服务器可以服务多个ML模型(或多个版本)、提供监视组件和可配置的体系结构。
Tensorflow服务与Docker
让我们使用标准Tensorflow服务(无CPU优化)获得基线预测性能延迟指标。
首先,从Tensorflow Docker hub中提取最新的服务镜像:
docker pull tensorflow/serving:latest
出于本文的目的,所有容器都在4核15GB Ubuntu 16.04主机上运行。
将Tensorflow模型导出为SavedModel格式
使用Tensorflow训练模型时,输出可以保存为变量检查点(磁盘上的文件)。可以通过恢复模型检查点或其转换的冻结图(二进制)直接运行推理。
为了使用Tensorflow服务来提供这些模型,必须将冻结图导出为SavedModel格式。Tensorflow文档提供了以SavedModel格式导出训练模型的示例。
我们将使用深度残差网络(ResNet)模型,该模型可用于对ImageNet的1000个类的数据集进行分类。下载预训练的ResNet-50 v2模型(https://github.com/tensorflow/models/tree/master/official/resnet#pre-trained-model),特别是channels_last(NHWC) convolution SavedModel,它通常更适合CPU。
复制下列结构中的RestNet模型目录:
Tensorflow Serving期望模型采用数字排序的目录结构来管理模型版本控制。在这种情况下,目录1/对应于模型版本1,其中包含模型体系结构saved_model.pb以及模型权重(变量)的快照。
加载并提供SavedModel
以下命令在docker容器中启动Tensorflow服务模型服务器。为了加载SavedModel,需要将模型的主机目录挂载到预期的容器目录中。
docker run -d -p 9000:8500 -v $(pwd)/models:/models/resnet -e MODEL_NAME=resnet -t tensorflow/serving:latest
检查容器日志显示,ModelServer正在运行,准备在gRPC和HTTP端点上为resnet模型提供推理请求:
I tensorflow_serving/core/loader_harness.cc:86] Successfully loaded servable version {name: resnet version: 1} I tensorflow_serving/model_servers/server.cc:286] Running gRPC ModelServer at 0.0.0.0:8500 ... I tensorflow_serving/model_servers/server.cc:302] Exporting HTTP/REST API at:localhost:8501 ...
预测客户端
Tensorflow Serving将API服务模式定义为协议缓冲区(protobufs)。预测API的gRPC客户端实现打包为tensorflow_serving.apisPython包。我们还需要tensorflowpython包来实现实用功能。
让我们安装依赖项来创建一个简单的客户端:
virtualenv .env && source .env/bin/activate && pip install numpy grpcio opencv-python tensorflow tensorflow-serving-api
该ResNet-50 v2模型期望在channels_last(NHWC)格式的数据结构中使用浮点Tensor输入。因此,使用opencv-python读取输入图像,opencv-python以float32数据类型加载到numpy数组(height x width x channels)中。下面的脚本创建预测客户端存根,将JPEG图像数据加载到numpy数组中,转换为张量原型,提出gRPC预测请求:
#!/usr/bin/env python from __future__ import print_function import argparse import numpy as np import time tt = time.time() import cv2 import tensorflow as tf from grpc.beta import implementations from tensorflow_serving.apis import predict_pb2 from tensorflow_serving.apis import prediction_service_pb2 parser = argparse.ArgumentParser(description='incetion grpc client flags.') parser.add_argument('--host', default='0.0.0.0', help='inception serving host') parser.add_argument('--port', default='9000', help='inception serving port') parser.add_argument('--image', default='', help='path to JPEG image file') FLAGS = parser.parse_args() def main(): # create prediction service client stub channel = implementations.insecure_channel(FLAGS.host, int(FLAGS.port)) stub = prediction_service_pb2.beta_create_PredictionService_stub(channel) # create request request = predict_pb2.PredictRequest() request.model_spec.name = 'resnet' request.model_spec.signature_name = 'serving_default' # read image into numpy array img = cv2.imread(FLAGS.image).astype(np.float32) # convert to tensor proto and make request # shape is in NHWC (num_samples x height x width x channels) format tensor = tf.contrib.util.make_tensor_proto(img, shape=[1]+list(img.shape)) request.inputs['input'].CopyFrom(tensor) resp = stub.Predict(request, 30.0) print('total time: {}s'.format(time.time() - tt)) if __name__ == '__main__': main()
使用输入JPEG图像运行客户机的输出如下所示:
python tf_serving_client.py --image=images/pupper.jpg
total time: 2.56152906418s
输出张量的预测结果为整数值和特征概率
对于单个请求,这种预测延迟是不可接受的。然而,这并非完全出乎意料;服务于二进制文件的默认Tensorflow目标是针对最广泛的硬件范围,以涵盖大多数用例。您可能已经从标准的Tensorflow服务容器日志中注意到:
I external/org_tensorflow/tensorflow/core/platform/cpu_feature_guard.cc:141] Your CPU supports instructions that this TensorFlow binary was not compiled to use: AVX2 FMA
这表示Tensorflow服务二进制文件在不兼容的CPU平台上运行,并未进行优化。
构建CPU优化服务二进制
根据Tensorflow文档,建议从源代码编译Tensorflow,并在运行二进制文件的主机平台的CPU上使用所有可用的优化。Tensorflow构建选项公开了一些标志,以支持构建特定于平台的CPU指令集:
在本例中,我们将使用1.13:
USER=$1 TAG=$2 TF_SERVING_VERSION_GIT_BRANCH="r1.13" git clone --branch="$TF_SERVING_VERSION_GIT_BRANCH" https://github.com/tensorflow/serving
Tensorflow服务开发镜像使用Bazel作为构建工具。处理器特定CPU指令集的构建目标可以指定如下:
TF_SERVING_BUILD_OPTIONS="--copt=-mavx --copt=-mavx2 --copt=-mfma --copt=-msse4.1 --copt=-msse4.2"
如果memory是约束,则可以使用--local_resources=2048,.5,1.0 flag 限制内存密集型构建过程的消耗。
以开发镜像为基础构建服务镜像:
#!/bin/bash USER=$1 TAG=$2 TF_SERVING_VERSION_GIT_BRANCH="r1.13" git clone --branch="${TF_SERVING_VERSION_GIT_BRANCH}" https://github.com/tensorflow/serving TF_SERVING_BUILD_OPTIONS="--copt=-mavx --copt=-mavx2 --copt=-mfma --copt=-msse4.1 --copt=-msse4.2" cd serving && docker build --pull -t $USER/tensorflow-serving-devel:$TAG --build-arg TF_SERVING_VERSION_GIT_BRANCH="${TF_SERVING_VERSION_GIT_BRANCH}" --build-arg TF_SERVING_BUILD_OPTIONS="${TF_SERVING_BUILD_OPTIONS}" -f tensorflow_serving/tools/docker/Dockerfile.devel . cd serving && docker build -t $USER/tensorflow-serving:$TAG --build-arg TF_SERVING_BUILD_IMAGE=$USER/tensorflow-serving-devel:$TAG -f tensorflow_serving/tools/docker/Dockerfile .
ModelServer可以配置tensorflow特定的标志来启用会话并行性。以下选项配置两个线程池来并行执行:
intra_op_parallelism_threads
inter_op_parallelism_threads
两个选项的默认值都设置为0。这意味着,系统会选择一个合适的数字,这通常需要每个CPU核心有一个线程可用。
接下来,与之前类似地启动服务容器,这次使用从源码构建的docker映像,并使用Tensorflow特定的CPU优化标志:
docker run -d -p 9000:8500 -v $(pwd)/models:/models/resnet -e MODEL_NAME=resnet -t $USER/tensorflow-serving:$TAG --tensorflow_intra_op_parallelism=4 --tensorflow_inter_op_parallelism=4
容器日志不应再显示CPU警告警告。在不更改任何代码的情况下,运行相同的预测请求会使预测延迟降低约35.8%:
python tf_serving_client.py --image=images/pupper.jpg
total time: 1.64234706879s
服务器端已针对其CPU平台进行了优化,但超过1秒的预测延迟似乎仍然过高。
加载tensorflow_serving和tensorflow库的延迟成本很高。每次调用tf.contrib.util.make_tensor_proto也会增加不必要的延迟开销。
我们实际上并不需要的tensorflow或tensorflow_serving包进行预测的请求。
如前所述,Tensorflow预测API被定义为protobufs。因此,可以通过生成必要的tensorflow和tensorflow_servingprotobuf python存根来替换这两个外部依赖项。这避免了在客户端本身上Pull整个Tensorflow库。
首先,摆脱tensorflow和tensorflow_serving依赖关系,并添加grpcio-tools包。
pip uninstall tensorflow tensorflow-serving-api && pip install grpcio-tools==1.0.0
克隆tensorflow/tensorflow和tensorflow/serving存储库并将以下protobuf文件复制到客户端项目中:
将上述protobuf文件复制到protos/目录中并保留原始路径:
为简单起见,predict_service.proto可以简化为仅实现Predict RPC。这样可以避免引入服务中定义的其他RPC的嵌套依赖项。这是简化的一个例子prediction_service.proto(https://gist.github.com/masroorhasan/8e728917ca23328895499179f4575bb8)。
使用grpcio.tools.protoc以下命令生成gRPC python实现:
PROTOC_OUT=protos/ PROTOS=$(find . | grep ".proto$") for p in $PROTOS; do python -m grpc.tools.protoc -I . --python_out=$PROTOC_OUT --grpc_python_out=$PROTOC_OUT $p done
现在tensorflow_serving可以删除整个模块:
from tensorflow_serving.apis import predict_pb2 from tensorflow_serving.apis import prediction_service_pb2
并替换为生成的protobufs protos/tensorflow_serving/apis:
from protos.tensorflow_serving.apis import predict_pb2 from protos.tensorflow_serving.apis import prediction_service_pb2
导入Tensorflow库是为了使用辅助函数make_tensor_proto,该函数用于将 python / numpy对象封装为TensorProto对象。
因此,我们可以替换以下依赖项和代码段:
import tensorflow as tf ... tensor = tf.contrib.util.make_tensor_proto(features) request.inputs['inputs'].CopyFrom(tensor)
使用protobuf导入并构建TensorProto对象:
from protos.tensorflow.core.framework import tensor_pb2 from protos.tensorflow.core.framework import tensor_shape_pb2 from protos.tensorflow.core.framework import types_pb2 ... # ensure NHWC shape and build tensor proto tensor_shape = [1]+list(img.shape) dims = [tensor_shape_pb2.TensorShapeProto.Dim(size=dim) for dim in tensor_shape] tensor_shape = tensor_shape_pb2.TensorShapeProto(dim=dims) tensor = tensor_pb2.TensorProto( dtype=types_pb2.DT_FLOAT, tensor_shape=tensor_shape, float_val=list(img.reshape(-1))) request.inputs['inputs'].CopyFrom(tensor)
完整的python脚本在这里可用(https://gist.github.com/masroorhasan/0e73a7fc7bb2558c65933338d8194130)。运行更新的初始客户端,该客户端将预测请求发送到优化的Tensorflow服务:
python tf_inception_grpc_client.py --image=images/pupper.jpg
total time: 0.58314920859s
下图显示了针对标准,优化的Tensorflow服务和客户端超过10次运行的预测请求的延迟:
从标准Tensorflow服务到优化版本的平均延迟降低了约70.4%。
Tensorflow服务也可以配置为高吞吐量处理。优化吞吐量通常是为“脱机”批处理完成的,在“脱机”批处理中并不严格要求延迟界限。
服务器端批处理
延迟和吞吐量之间的权衡取决于支持的batching 参数。
通过设置--enable_batching和--batching_parameters_file标记来启用batching。可以按SessionBundleConfig的定义设置批处理参数(https://github.com/tensorflow/serving/blob/d77c9768e33e1207ac8757cff56b9ed9a53f8765/tensorflow_serving/servables/tensorflow/session_bundle_config.proto)。对于仅CPU系统,请考虑设置num_batch_threads可用的核心数。
在服务器端达到全部批处理后,推理请求在内部合并为单个大请求(张量),并在合并的请求上运行一个Tensorflow会话。在单个会话上运行一批请求是CPU/GPU并行性真正能够发挥作用的地方。
使用Tensorflow服务进行批量处理时需要考虑的一些用例:
客户端批处理
在客户端进行批处理将多个输入组合在一起以生成单个请求。
由于ResNet模型需要NHWC格式的输入(第一维是输入数),我们可以将多个输入图像聚合成一个RPC请求:
... batch = [] for jpeg in os.listdir(FLAGS.images_path): path = os.path.join(FLAGS.images_path, jpeg) img = cv2.imread(path).astype(np.float32) batch.Append(img) ... batch_np = np.array(batch).astype(np.float32) dims = [tensor_shape_pb2.TensorShapeProto.Dim(size=dim) for dim in batch_np.shape] t_shape = tensor_shape_pb2.TensorShapeProto(dim=dims) tensor = tensor_pb2.TensorProto( dtype=types_pb2.DT_FLOAT, tensor_shape=t_shape, float_val=list(batched_np.reshape(-1))) request.inputs['inputs'].CopyFrom(tensor)
对于一批N个图像,响应中的输出张量将具有请求批次中相同数量的输入的预测结果,在这种情况下N = 2:
对于训练,GPU可以更直观地利用并行化,因为构建深度神经网络需要大量计算才能获得最佳解决方案。
但是,推理并非总是如此。很多时候,当图执行步骤放在GPU设备上时,CNN将会加速推断。然而,选择能够优化价格性能最佳点的硬件需要严格的测试、深入的技术和成本分析。硬件加速并行对于“脱机”推理batch processing更有价值。