介绍

Tensorflow Serving是Google开源的机器学习平台Tensorflow生态的一部分,它的功能是将训练好的模型跑起来,提供接口给其他服务调用,以便使用模型进行推理预测。

由于tf-serving项目的文档极度缺失,以至于不得不使用本人擅长的野路子面向偶然/面向源代码/面向issue进行xjb尝试,因此整理了本文,希望踩坑经验对您也有所帮助。

项目状况

代码/架构

项目的开源代码都在 https://github.com/tensorflow/serving 项目是由C++开发,它的主要玩法就是在Tensorflow外面包了一层,加了一些访问接口和模型加载处理的东西,详细架构可以去看官方文档的描述

运行环境

在docker容器里跑的,理论上来说也可以把二进制抠出来用,但官方不建议。

版本

目前master分支的代码是2.x最新,旧版本的有其独立分支如r2.0,r1.15等。

tensorflow 1.x产出的savedmodel基本上可以在2.x的serving上使用,具体情况可以看官方关于SavedModel的RPC

开发

当你需要魔改项目的时候,可以用tools/run_in_docker.sh这个脚本进行构建和运行,它会把你本地的目录挂进容器里去跑bazel,这样不用每次从头进行构建,因为他编译一次实在太慢,同时测试运行也可以用这个脚本,具体用法请面向源代码编程。

Tips

如果你在运行上述脚本中遇到了no such package '@zlib//这种报错,可能需要魔跑到bazel目录下手工改个东西,详细参考Building from source error: The repository '@zlib' could not be resolved

项目构建

通常情况下,为了适配你自己的硬件或你自己魔改了代码,就需要重新构建docker镜像。

这个项目采用启动一个docker容器,在里面跑bazel的方式进行构建,相关Dockerfile在tensorflow_serving/tools/docker

Dockerfile分为两种,带.devel的后缀的构建结果是仅有tensorflow_serving二进制的基础镜像,而不带此后缀的是在前者的基础之上配置了模型目录和启动脚本的可供生产使用的镜像。

如果你需要使用gpu或者intel的mkl库,就需要用带gpumkl字样的Dockerfile进行构建,以便能够使用相应的依赖库。

构建Tips

跑构建时需要注意这些事情:

  1. 如果你自己改了源码,那么构建时Dockerfile.devel这个文件是需要修改的,因为它默认是从github仓库中把代码拉下来跑构建,没有用你本地的代码。
  2. 为了达到最佳的性能,需要调整TF_SERVING_BUILD_OPTIONS这个参数,加一些构建参数,设置CPU支持的指令集之类的,详细参考building-an-optimized-serving-binary
  3. 如果内存不够大之类的,也请设置TF_SERVING_BUILD_OPTIONS,还是看上面那篇文档。
  4. 构建请自觉科学上网,不然有一些东西会拉不下来。请注意是需要给docker容器加代理,给你宿主机加代理是没啥用的。

部署

主要可以参考官方的docker部署的文档TensorFlow Serving with Docker

我主要设置了这些参数,供参考

tensorflow_model_server --port=8500 --rest_api_port=8501 --enable_batching=true --model_config_file_poll_wait_seconds=1000 --model_config_file=/data/models.conf --monitoring_config_file=/data/monitor.conf 
--batching_parameters_file=/data/batching.conf --tensorflow_intra_op_parallelism=9 --tensorflow_inter_op_parallelism=9

其中几个配置文件的内容

batching.conf

这个用来设置batch的大小和线程数什么的,做性能优化的时候可以调整

max_batch_size { value: 512 }
batch_timeout_micros { value: 0 }
max_enqueued_batches { value: 10 }
num_batch_threads { value: 10 }

monitor.conf

开启了metrics,可以用prometheus收集数据,v2.4或更高的版本才有predict延迟等数据

prometheus_config: {
	enable: true,
    path: "/metrics"
}

models.conf

用来配置加载哪些模型,这里可用指定模型的加载版本和路径,路径填个s3或者hdfs路径也可以加载,但我个人试用了下觉得不太好用,所以目前还是采用了本地文件系统加载,旁边跑个另外的程序进行管理,负责拉模型到本地喂给serving用。

model_config_list: {
    config: {
    name:"my_model"
    base_path:"/data/models/my_model"
    model_platform: "tensorflow"
    model_version_policy: {all{}}
  },
}

调用

HTTP REST接口

直接参考官方文档即可

gRPC接口

如果你用python来调,官方项目里已经有生成好的客户端可用直接用了,但如果你像我一样不幸用go,或者其他语言。就比较麻烦,需要根据他项目里的proto文件用protoc编译出你要用的语言的客户端,其中有用的其实只有api目录下的proto生成出来的东西,但它依赖了一堆其他目录下的proto,甚至还有隔壁tensorflow仓库里的proto。需要按照以下步骤进行构建。

  1. clone两个仓库到本地:servingtensorflow两个项目的代码。注意分支要对应。比如你想用1.15版本,那么两个仓库都要用r1.15分支。

  2. 然后用protoc进行编译,具体可以参考我糊出来的这个可能能跑的构建脚本

  3. 把构建出来的一坨东西放到你的项目下使用,grpc具体用法参考gRPC文档Basics tutorial - Creating a stub

其他语言的使用步骤类似。

如果需要压测gRPC接口,请参考旧文用 ghz 对 gRPC 服务进行压测

疑难杂症

内存泄漏

这里有个祖传的issue好几年了也没关,大家遇到各种内存问题,可官方人员就是没法复现,也不给修。但我们也遇到了,一定是我太菜。迄今为止,遇到过两种内存泄漏,具体情况也可以在那个issue里看到,依靠面向偶然的方式可以修复。

1.随着请求数增加,内存持续增加

似乎是非batch的情况下有奇奇怪怪的bug,所以启动参数加上--enable_batching=true可解

2.模型版本热更新时,某个版本已经卸载,但内存没有释放

经过反复尝试,发现把malloc换成jemalloc可以解决,我也不知道为啥,but it works.

魔改Dockerfile后重新构建即可,加上

RUN apt-get update && apt-get install -y libjemalloc-dev
ENV LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so.1

模型热更新的时候有抖动(延时升高)

  1. 在模型中增加warmup,加上之后,serving会在挂载模型前进行预热。参考文档saved_model_warmup

  2. 模型配置里开启多版本同时在线。我目前的做法是配置文件里修改model_version_policy: {all{}},即加载目录下所有版本(参考上面部署一节的models.conf配置文件),使用另外的脚本来控制目录下的版本更新(就是把新版本放进去,旧版本删掉),保证目录中在更新时,同时至少存在两个版本(已在线的原版本,和新版本)的模型,这样能在版本切换的过程更加平滑。

HTTP接口和gRPC接口的行为不一致

你可能注意到在调用http的predict接口的时候,少几个多几个tensor也不会报错,但通过gRPC调用时,就会报出来,这是因为里面的处理逻辑不同,http接口对这个情况对缺失tensor进行了填充,但gRPC里没有,因此在通过gRPC调用前,我们需要把tensor补全才能成功调用。

性能问题

我们的场景是推荐系统的在线预估,对延时要求比较高。但tf serving这玩意在能接受的响应速度下单机QPS挺低的,QPS一高起来,延时就飙升,很迷。我们目前采用了以下方法降低延时,有一定的效果,但还在继续尝试寻找更多的优化方案。

  1. 优化模型结构(可以借助tensorboard来进行分析)
  2. 使用工具修改模型结构,对模型进行量化处理
  3. 将请求切片,分散到多个实例上并发计算(这样就可以加机器了)。仔细调整每个batch的大小和切片数,可能会找到一个效果比较好的平衡点
  4. 调整tensorflow_intra_op_parallelismtensorflow_inter_op_parallelism,我们都设成了和核数一致,效果还行
  5. 如果是cpu跑在线预估,不要用mkl版,因为它的优化更多的是提升吞吐量,但会增加延时,所以跑得比普通的cpu版要慢。解释在这里build with "--config=mkl" infernece-latency become longer(CPU)

如果有什么更好的优化方案或者发现本文有什么问题,欢迎评论区交流。