Tensorflow已經(jīng)成長(zhǎng)為事實(shí)上的機(jī)器學(xué)習(xí)(ML)平臺(tái),在業(yè)界和研究領(lǐng)域都很流行。對(duì)Tensorflow的需求和支持促成了大量圍繞訓(xùn)練和服務(wù)機(jī)器學(xué)習(xí)(ML)模型的OSS庫(kù)、工具和框架。Tensorflow服務(wù)是一個(gè)構(gòu)建在分布式生產(chǎn)環(huán)境中用于服務(wù)機(jī)器學(xué)習(xí)(ML)模型的推理方面的項(xiàng)目。
今天,我們將重點(diǎn)討論通過(guò)優(yōu)化預(yù)測(cè)服務(wù)器和客戶機(jī)來(lái)提高延遲的技術(shù)。模型預(yù)測(cè)通常是“在線”操作(在關(guān)鍵的應(yīng)用程序請(qǐng)求路徑上),因此我們的主要優(yōu)化目標(biāo)是以盡可能低的延遲處理大量請(qǐng)求。
首先讓我們快速概述一下Tensorflow服務(wù)。
什么是Tensorflow服務(wù)?
Tensorflow Serving提供靈活的服務(wù)器架構(gòu),旨在部署和服務(wù)機(jī)器學(xué)習(xí)(ML)模型。一旦模型被訓(xùn)練過(guò)并準(zhǔn)備用于預(yù)測(cè),Tensorflow服務(wù)就需要將模型導(dǎo)出為Servable兼容格式。
Servable是封裝Tensorflow對(duì)象的中心抽象。例如,模型可以表示為一個(gè)或多個(gè)可服務(wù)對(duì)象。因此,Servables是客戶機(jī)用來(lái)執(zhí)行計(jì)算(如推理)的底層對(duì)象。可服務(wù)的大小很重要,因?yàn)檩^小的模型使用更少的內(nèi)存、更少的存儲(chǔ)空間,并且將具有更快的加載時(shí)間。Servables希望模型采用SavedModel格式,以便使用Predict API加載和服務(wù)。
Tensorflow Serving將核心服務(wù)組件放在一起,構(gòu)建一個(gè)gRPC/HTTP服務(wù)器,該服務(wù)器可以服務(wù)多個(gè)ML模型(或多個(gè)版本)、提供監(jiān)視組件和可配置的體系結(jié)構(gòu)。
Tensorflow服務(wù)與Docker
讓我們使用標(biāo)準(zhǔn)Tensorflow服務(wù)(無(wú)CPU優(yōu)化)獲得基線預(yù)測(cè)性能延遲指標(biāo)。
首先,從Tensorflow Docker hub中提取最新的服務(wù)鏡像:
docker pull tensorflow/serving:latest
出于本文的目的,所有容器都在4核15GB Ubuntu 16.04主機(jī)上運(yùn)行。
將Tensorflow模型導(dǎo)出為SavedModel格式
使用Tensorflow訓(xùn)練模型時(shí),輸出可以保存為變量檢查點(diǎn)(磁盤上的文件)。可以通過(guò)恢復(fù)模型檢查點(diǎn)或其轉(zhuǎn)換的凍結(jié)圖(二進(jìn)制)直接運(yùn)行推理。
為了使用Tensorflow服務(wù)來(lái)提供這些模型,必須將凍結(jié)圖導(dǎo)出為SavedModel格式。Tensorflow文檔提供了以SavedModel格式導(dǎo)出訓(xùn)練模型的示例。
我們將使用深度殘差網(wǎng)絡(luò)(ResNet)模型,該模型可用于對(duì)ImageNet的1000個(gè)類的數(shù)據(jù)集進(jìn)行分類。下載預(yù)訓(xùn)練的ResNet-50 v2模型(https://github.com/tensorflow/models/tree/master/official/resnet#pre-trained-model),特別是channels_last(NHWC) convolution SavedModel,它通常更適合CPU。
復(fù)制下列結(jié)構(gòu)中的RestNet模型目錄:
Tensorflow Serving期望模型采用數(shù)字排序的目錄結(jié)構(gòu)來(lái)管理模型版本控制。在這種情況下,目錄1/對(duì)應(yīng)于模型版本1,其中包含模型體系結(jié)構(gòu)saved_model.pb以及模型權(quán)重(變量)的快照。
加載并提供SavedModel
以下命令在docker容器中啟動(dòng)Tensorflow服務(wù)模型服務(wù)器。為了加載SavedModel,需要將模型的主機(jī)目錄掛載到預(yù)期的容器目錄中。
docker run -d -p 9000:8500 -v $(pwd)/models:/models/resnet -e MODEL_NAME=resnet -t tensorflow/serving:latest
檢查容器日志顯示,ModelServer正在運(yùn)行,準(zhǔn)備在gRPC和HTTP端點(diǎn)上為resnet模型提供推理請(qǐng)求:
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 ...
預(yù)測(cè)客戶端
Tensorflow Serving將API服務(wù)模式定義為協(xié)議緩沖區(qū)(protobufs)。預(yù)測(cè)API的gRPC客戶端實(shí)現(xiàn)打包為tensorflow_serving.apisPython包。我們還需要tensorflowpython包來(lái)實(shí)現(xiàn)實(shí)用功能。
讓我們安裝依賴項(xiàng)來(lái)創(chuàng)建一個(gè)簡(jiǎn)單的客戶端:
virtualenv .env && source .env/bin/activate && pip install numpy grpcio opencv-python tensorflow tensorflow-serving-api
該ResNet-50 v2模型期望在channels_last(NHWC)格式的數(shù)據(jù)結(jié)構(gòu)中使用浮點(diǎn)Tensor輸入。因此,使用opencv-python讀取輸入圖像,opencv-python以float32數(shù)據(jù)類型加載到numpy數(shù)組(height x width x channels)中。下面的腳本創(chuàng)建預(yù)測(cè)客戶端存根,將JPEG圖像數(shù)據(jù)加載到numpy數(shù)組中,轉(zhuǎn)換為張量原型,提出gRPC預(yù)測(cè)請(qǐng)求:
#!/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圖像運(yùn)行客戶機(jī)的輸出如下所示:
python tf_serving_client.py --image=images/pupper.jpg
total time: 2.56152906418s
輸出張量的預(yù)測(cè)結(jié)果為整數(shù)值和特征概率
對(duì)于單個(gè)請(qǐng)求,這種預(yù)測(cè)延遲是不可接受的。然而,這并非完全出乎意料;服務(wù)于二進(jìn)制文件的默認(rèn)Tensorflow目標(biāo)是針對(duì)最廣泛的硬件范圍,以涵蓋大多數(shù)用例。您可能已經(jīng)從標(biāo)準(zhǔn)的Tensorflow服務(wù)容器日志中注意到:
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服務(wù)二進(jìn)制文件在不兼容的CPU平臺(tái)上運(yùn)行,并未進(jìn)行優(yōu)化。
構(gòu)建CPU優(yōu)化服務(wù)二進(jìn)制
根據(jù)Tensorflow文檔,建議從源代碼編譯Tensorflow,并在運(yùn)行二進(jìn)制文件的主機(jī)平臺(tái)的CPU上使用所有可用的優(yōu)化。Tensorflow構(gòu)建選項(xiàng)公開了一些標(biāo)志,以支持構(gòu)建特定于平臺(tái)的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服務(wù)開發(fā)鏡像使用Bazel作為構(gòu)建工具。處理器特定CPU指令集的構(gòu)建目標(biāo)可以指定如下:
TF_SERVING_BUILD_OPTIONS="--copt=-mavx --copt=-mavx2 --copt=-mfma --copt=-msse4.1 --copt=-msse4.2"
如果memory是約束,則可以使用--local_resources=2048,.5,1.0 flag 限制內(nèi)存密集型構(gòu)建過(guò)程的消耗。
以開發(fā)鏡像為基礎(chǔ)構(gòu)建服務(wù)鏡像:
#!/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特定的標(biāo)志來(lái)啟用會(huì)話并行性。以下選項(xiàng)配置兩個(gè)線程池來(lái)并行執(zhí)行:
intra_op_parallelism_threads
- 控制用于并行執(zhí)行單個(gè)操作的最大線程數(shù)。
- 用于并行化具有子操作的操作,這些子操作本質(zhì)上是獨(dú)立的。
inter_op_parallelism_threads
- 控制用于并行執(zhí)行獨(dú)立不同操作的最大線程數(shù)。
- Tensorflow Graph上的操作彼此獨(dú)立,因此可以在不同的線程上運(yùn)行。
兩個(gè)選項(xiàng)的默認(rèn)值都設(shè)置為0。這意味著,系統(tǒng)會(huì)選擇一個(gè)合適的數(shù)字,這通常需要每個(gè)CPU核心有一個(gè)線程可用。
接下來(lái),與之前類似地啟動(dòng)服務(wù)容器,這次使用從源碼構(gòu)建的docker映像,并使用Tensorflow特定的CPU優(yōu)化標(biāo)志:
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
容器日志不應(yīng)再顯示CPU警告警告。在不更改任何代碼的情況下,運(yùn)行相同的預(yù)測(cè)請(qǐng)求會(huì)使預(yù)測(cè)延遲降低約35.8%:
python tf_serving_client.py --image=images/pupper.jpg
total time: 1.64234706879s
提高預(yù)測(cè)客戶端的速度
服務(wù)器端已針對(duì)其CPU平臺(tái)進(jìn)行了優(yōu)化,但超過(guò)1秒的預(yù)測(cè)延遲似乎仍然過(guò)高。
加載tensorflow_serving和tensorflow庫(kù)的延遲成本很高。每次調(diào)用tf.contrib.util.make_tensor_proto也會(huì)增加不必要的延遲開銷。
我們實(shí)際上并不需要的tensorflow或tensorflow_serving包進(jìn)行預(yù)測(cè)的請(qǐng)求。
如前所述,Tensorflow預(yù)測(cè)API被定義為protobufs。因此,可以通過(guò)生成必要的tensorflow和tensorflow_servingprotobuf python存根來(lái)替換這兩個(gè)外部依賴項(xiàng)。這避免了在客戶端本身上Pull整個(gè)Tensorflow庫(kù)。
首先,擺脫tensorflow和tensorflow_serving依賴關(guān)系,并添加grpcio-tools包。
pip uninstall tensorflow tensorflow-serving-api && pip install grpcio-tools==1.0.0
克隆tensorflow/tensorflow和tensorflow/serving存儲(chǔ)庫(kù)并將以下protobuf文件復(fù)制到客戶端項(xiàng)目中:
將上述protobuf文件復(fù)制到protos/目錄中并保留原始路徑:
為簡(jiǎn)單起見,predict_service.proto可以簡(jiǎn)化為僅實(shí)現(xiàn)Predict RPC。這樣可以避免引入服務(wù)中定義的其他RPC的嵌套依賴項(xiàng)。這是簡(jiǎn)化的一個(gè)例子prediction_service.proto(https://gist.github.com/masroorhasan/8e728917ca23328895499179f4575bb8)。
使用grpcio.tools.protoc以下命令生成gRPC python實(shí)現(xiàn):
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
現(xiàn)在tensorflow_serving可以刪除整個(gè)模塊:
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
導(dǎo)入Tensorflow庫(kù)是為了使用輔助函數(shù)make_tensor_proto,該函數(shù)用于將 python / numpy對(duì)象封裝為TensorProto對(duì)象。
因此,我們可以替換以下依賴項(xiàng)和代碼段:
import tensorflow as tf ... tensor = tf.contrib.util.make_tensor_proto(features) request.inputs['inputs'].CopyFrom(tensor)
使用protobuf導(dǎo)入并構(gòu)建TensorProto對(duì)象:
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)。運(yùn)行更新的初始客戶端,該客戶端將預(yù)測(cè)請(qǐng)求發(fā)送到優(yōu)化的Tensorflow服務(wù):
python tf_inception_grpc_client.py --image=images/pupper.jpg
total time: 0.58314920859s
下圖顯示了針對(duì)標(biāo)準(zhǔn),優(yōu)化的Tensorflow服務(wù)和客戶端超過(guò)10次運(yùn)行的預(yù)測(cè)請(qǐng)求的延遲:
從標(biāo)準(zhǔn)Tensorflow服務(wù)到優(yōu)化版本的平均延遲降低了約70.4%。
優(yōu)化預(yù)測(cè)吞吐量
Tensorflow服務(wù)也可以配置為高吞吐量處理。優(yōu)化吞吐量通常是為“脫機(jī)”批處理完成的,在“脫機(jī)”批處理中并不嚴(yán)格要求延遲界限。
服務(wù)器端批處理
延遲和吞吐量之間的權(quán)衡取決于支持的batching 參數(shù)。
通過(guò)設(shè)置--enable_batching和--batching_parameters_file標(biāo)記來(lái)啟用batching。可以按SessionBundleConfig的定義設(shè)置批處理參數(shù)(https://github.com/tensorflow/serving/blob/d77c9768e33e1207ac8757cff56b9ed9a53f8765/tensorflow_serving/servables/tensorflow/session_bundle_config.proto)。對(duì)于僅CPU系統(tǒng),請(qǐng)考慮設(shè)置num_batch_threads可用的核心數(shù)。
在服務(wù)器端達(dá)到全部批處理后,推理請(qǐng)求在內(nèi)部合并為單個(gè)大請(qǐng)求(張量),并在合并的請(qǐng)求上運(yùn)行一個(gè)Tensorflow會(huì)話。在單個(gè)會(huì)話上運(yùn)行一批請(qǐng)求是CPU/GPU并行性真正能夠發(fā)揮作用的地方。
使用Tensorflow服務(wù)進(jìn)行批量處理時(shí)需要考慮的一些用例:
- 使用異步客戶機(jī)請(qǐng)求填充服務(wù)器端上的batches
- 通過(guò)將模型圖組件放在CPU / GPU上來(lái)加速批處理
- 在從同一服務(wù)器提供多個(gè)模型時(shí)交錯(cuò)預(yù)測(cè)請(qǐng)求
- 強(qiáng)烈建議對(duì)“離線”高容量推理處理進(jìn)行批處理
客戶端批處理
在客戶端進(jìn)行批處理將多個(gè)輸入組合在一起以生成單個(gè)請(qǐng)求。
由于ResNet模型需要NHWC格式的輸入(第一維是輸入數(shù)),我們可以將多個(gè)輸入圖像聚合成一個(gè)RPC請(qǐng)求:
... 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)
對(duì)于一批N個(gè)圖像,響應(yīng)中的輸出張量將具有請(qǐng)求批次中相同數(shù)量的輸入的預(yù)測(cè)結(jié)果,在這種情況下N = 2:
硬件加速
對(duì)于訓(xùn)練,GPU可以更直觀地利用并行化,因?yàn)闃?gòu)建深度神經(jīng)網(wǎng)絡(luò)需要大量計(jì)算才能獲得最佳解決方案。
但是,推理并非總是如此。很多時(shí)候,當(dāng)圖執(zhí)行步驟放在GPU設(shè)備上時(shí),CNN將會(huì)加速推斷。然而,選擇能夠優(yōu)化價(jià)格性能最佳點(diǎn)的硬件需要嚴(yán)格的測(cè)試、深入的技術(shù)和成本分析。硬件加速并行對(duì)于“脫機(jī)”推理batch processing更有價(jià)值。






