我们如何将Tensorflow服务生产率提高70%

Tensorflow已成为机器学习(ML)的标准平台,在行业和研究中都非常流行。 已经创建了许多免费的库,工具和框架来训练和维护ML模型。 Tensorflow Serving项目有助于在分布式生产环境中维护ML模型。

我们的Mux服务在基础架构的多个部分使用Tensorflow Serving,我们已经讨论过Tensorflow Serving在视频标题编码中的使用。 今天,我们将专注于通过优化预测服务器和客户端来改善延迟的方法。 模型预测通常是“在线”操作(在请求应用程序的关键路径上),因此,优化的主要目标是以尽可能低的延迟处理大量请求。

什么是Tensorflow服务?


Tensorflow Serving提供了用于部署和维护ML模型的灵活服务器架构。 训练好模型并准备将其用于预测后,Tensorflow Serving需要将其导出为兼容(可服务)格式。

Servable是包装Tensorflow对象的中央抽象。 例如,模型可以表示为一个或多个可服务对象。 因此,Servable是客户端用于执行计算的基本对象。 可使用的大小很重要:较小的型号占用较少的空间,使用较少的内存并且加载速度更快。 要使用Predict API下载和维护,模型必须为SavedModel格式。



Tensorflow Serving结合了基本组件,创建了一个gRPC / HTTP服务器,该服务器可为多个ML模型(或多个版本)提供服务,提供监视组件和自定义架构。

Tensorflow与Docker服务


让我们看一下使用标准Tensorflow Serving设置(无需CPU优化)预测性能的延迟的基本指标。

首先,从TensorFlow Docker集线器下载最新映像:

docker pull tensorflow/serving:latest 

在本文中,所有容器都在具有四个内核(15 GB,Ubuntu 16.04)的主机上运行。

将Tensorflow模型导出到SavedModel


当使用Tensorflow训练模型时,可以将输出保存为变量控制点(磁盘上的文件)。 通过恢复模型的控制点或以冻结的冻结图形格式(二进制文件)直接执行输出。

对于Tensorflow Serving,此冻结的图需要导出为SavedModel格式。 Tensorflow文档包含将经过训练的模型导出为SavedModel格式的示例。

Tensorflow还提供许多官方和研究模型,作为进行实验,研究或生产的起点。

例如,我们将使用深度残留神经网络(ResNet)模型对1000个类别的ImageNet数据集进行分类。 下载经过预先 ResNet-50 v2模型,尤其是SavedModel中的Channels_last(NHWC) 选项 :通常,它在CPU上效果更好。

将RestNet模型目录复制到以下结构中:

 models/ 1/ saved_model.pb variables/ variables.data-00000-of-00001 variables.index 

Tensorflow Serving希望使用数字排序的目录结构进行版本控制。 在我们的示例中,目录1/对应于版本1模型,其中包含带有模型权重(变量)快照的saved_model.pb模型saved_model.pb

加载和处理SavedModel


以下命令在Docker容器中启动Tensorflow Serving模型服务器。 要加载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以协议缓冲区 (protobufs)格式定义API模式。 预测API的GRPC客户端实现打包为Python包tensorflow_serving.apis 。 我们将需要另一个Python软件包tensorflow用于实用程序功能。

安装依赖项以创建一个简单的客户端:

 virtualenv .env && source .env/bin/activate && \ pip install numpy grpcio opencv-python tensorflow tensorflow-serving-api 

ResNet-50 v2模型期望以格式化的channels_last(NHWC)数据结构输入浮点张量。 因此,使用opencv-python读取输入图像,并将其作为float32数据类型加载到numpy数组(高度×宽度×通道)中。 下面的脚本创建了一个预测客户端存根,并将JPEG数据加载到numpy数组中,将其转换为tensor_proto以发出对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 

所得张量包含整数值和符号概率形式的预测。

 outputs { key: "classes" value { dtype: DT_INT64 tensor_shape { dim { size: 1 } } int64_val: 238 } } outputs { key: "probabilities" ... 

对于单个请求,这样的延迟是不可接受的。 但是并不奇怪:Tensorflow Serving二进制文件默认情况下是针对大多数用例设计的,适用于最广泛的设备。 您可能在标准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 

这表示在尚未对其进行优化的CPU平台上运行的TensorFlow Serving二进制文件。

构建优化的二进制文件


根据Tensorflow 文档 ,建议使用二进制文件将在其上运行的主机上的CPU的所有优化方法从源代码编译Tensorflow。 组装时,特殊标志可激活特定平台的CPU指令集:

指令集标志
AVX--copt = -mavx
AVX2--copt = -mavx2
Fma--copt = -mfma
上证4.1--copt = -msse4.1
上证4.2--copt = -msse4.2
全部受处理器支持--copt = -march =本机

克隆特定版本的Tensorflow服务。 在我们的情况下,这是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 Serving开发人员图像使用Basel工具进行构建。 我们为特定的CPU指令集配置它:

 TF_SERVING_BUILD_OPTIONS="--copt=-mavx --copt=-mavx2 --copt=-mfma --copt=-msse4.1 --copt=-msse4.2" 

如果没有足够的内存,请在标志--local_resources=2048,.5,1.0过程中使用--local_resources=2048,.5,1.0标志限制内存消耗。 有关标志的信息,请参见Tensorflow Serving和Docker帮助以及Bazel文档

根据现有图像创建一个工作图像:

 #!/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 . 

使用TensorFlow标志配置ModelServer以支持并发。 以下选项将两个线程池配置为并行操作:

 intra_op_parallelism_threads 

  • 控制一个操作并行执行的最大线程数;
  • 用于并行化具有本质上独立的子操作的操作。

 inter_op_parallelism_threads 

  • 控制并行执行独立操作的最大线程数;
  • Tensorflow Graph操作彼此独立,因此可以在不同的线程中执行。

默认情况下,两个参数都设置为0 。 这意味着系统本身会选择适当的数字,这通常意味着每个内核一个线程。 但是,可以为多核并发手动更改该参数。

然后以与上一个相同的方式运行Serving容器,这次使用从源代码编译的Docker映像,并使用针对特定处理器的Tensorflow优化标志:

 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_servingtensorflow库会对延迟产生重大影响。 对tf.contrib.util.make_tensor_proto每个不必要的调用也会增加一秒钟的时间。

您可能会问:“我们是否不需要TensorFlow Python软件包来实际向Tensorflow服务器发出预测请求?” 实际上,实际上并不需要 tensorflow_servingtensorflow包。

如前所述,Tensorflow预测API被定义为原型缓冲区。 因此,可以将两个外部依赖项替换为相应的tensorflowtensorflow_serving -然后您无需在客户端上提取整个(沉重的)Tensorflow库。

首先,摆脱tensorflowtensorflow_serving并添加grpcio-tools包。

 pip uninstall tensorflow tensorflow-serving-api && \ pip install grpcio-tools==1.0.0 

克隆tensorflow/tensorflowtensorflow/serving存储库,并将以下protobuf文件复制到客户端项目:

 tensorflow/serving/ tensorflow_serving/apis/model.proto tensorflow_serving/apis/predict.proto tensorflow_serving/apis/prediction_service.proto tensorflow/tensorflow/ tensorflow/core/framework/resource_handle.proto tensorflow/core/framework/tensor_shape.proto tensorflow/core/framework/tensor.proto tensorflow/core/framework/types.proto 

将这些protobuf文件复制到protos/目录,并保留原始路径:

 protos/ tensorflow_serving/ apis/ *.proto tensorflow/ core/ framework/ *.proto 

为了简单起见,可以将prediction_service.proto简化为仅实现Predict RPC,以便不下载服务中指定的其他RPC的嵌套依赖关系。 这是简化的prediction_service.的示例。

使用grpcio.tools.protoc创建Python gRPC实现:

 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 

...并替换为从protos/tensorflow_serving/apis生成的protobuffers:

 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) 

导入protobuffers并构建一个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脚本在这里 。 运行更新的入门客户端,该客户端针对优化的Tensorflow Serving发出预测请求:

 python tf_inception_grpc_client.py --image=images/pupper.jpg total time: 0.58314920859s 

下图显示了优化版本的Tensorflow Serving与标准版本相比的预测执行时间(运行10多次):



平均延迟减少了约3.38倍。

带宽优化


Tensorflow服务可以配置为处理大量数据。 带宽优化通常是针对“独立”批处理执行的,其中严格的延迟边界不是严格的要求。

服务器端批处理


文档中所述,Tensorflow Serving本机支持服务器端批处理。

延迟和吞吐量之间的权衡由批处理参数确定。 它们使您可以实现硬件加速器能够提供的最大吞吐量。

要启用打包,请设置--enable_batching--batching_parameters_file 。 根据SessionBundleConfig设置参数。 对于CPU上的系统,将num_batch_threads设置为可用内核数。 对于GPU,请参阅此处的适当参数。

在服务器端填满整个程序包后,发布请求将合并为一个大请求(张量),并与合并的请求一起发送到Tensorflow会话。 在这种情况下,确实涉及CPU / GPU并行性。

Tensorflow批处理的一些常见用途:

  • 使用异步客户端请求填充服务器端数据包
  • 通过将模型图的组件传输到CPU / GPU,可以更快地进行批处理
  • 在一台服务器上处理来自多个模型的请求
  • 强烈建议将批处理用于“离线”处理大量请求

客户端批处理


客户端批处理将几个传入请求分组为一个。

由于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:

 outputs { key: "classes" value { dtype: DT_INT64 tensor_shape { dim { size: 2 } } int64_val: 238 int64_val: 121 } } ... 

硬件加速


关于GPU的几句话。

学习过程自然会在GPU上使用并行化,因为深度神经网络的构建需要大量计算才能获得最佳解决方案。

但是对于输出结果,并行化不是很明显。 通常,您可以加快神经网络向GPU的输出速度,但是您需要仔细选择和测试设备,并进行深入的技术和经济分析。 硬件并行化对于“自治”结论(大量)的批处理更有价值。

在转向GPU之前,请仔细考虑业务需求,并仔细分析成本(货币,运营,技术),以获取最大利益(减少延迟,高吞吐量)。

Source: https://habr.com/ru/post/zh-CN445928/


All Articles