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的预测请求:
收到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指令集:
克隆特定版本的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文档 。
根据现有图像创建一个工作图像:
使用
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_serving
和
tensorflow
库会对延迟产生重大影响。 对
tf.contrib.util.make_tensor_proto
每个不必要的调用也会增加一秒钟的时间。
您可能会问:“我们是否不需要TensorFlow Python软件包来实际向Tensorflow服务器发出预测请求?” 实际上,实际上并不
需要 tensorflow_serving
和
tensorflow
包。
如前所述,Tensorflow预测API被定义为原型缓冲区。 因此,可以将两个外部依赖项替换为相应的
tensorflow
和
tensorflow_serving
-然后您无需在客户端上提取整个(沉重的)Tensorflow库。
首先,摆脱
tensorflow
和
tensorflow_serving
并添加
grpcio-tools
包。
pip uninstall tensorflow tensorflow-serving-api && \ pip install grpcio-tools==1.0.0
克隆
tensorflow/tensorflow
和
tensorflow/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 ...
完整的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之前,请仔细考虑业务需求,并仔细分析成本(货币,运营,技术),以获取最大利益(减少延迟,高吞吐量)。