Press "Enter" to skip to content

TensorFlow Serving璀璨发展史

0. 简介

随着TensorFlow 1.8.0正式版的发布,TensorFlow Serving最近也发布了1.8.0版本,最大的特性就是原生支持了RESTful API。在1.8.0以前,大家用TensorFlow训练模型的例子很多,但上线模型服务的方式如八仙过海,例如研发能力强的团队可能直接读本地Variables和Graph实现Serving,也有公司用Google CloudML上线使用预设的gcloud接口,或者类似我们之前分享过的用Python实现通用的SimpleTensorFlowServing并且支持RESTful接口。

即使在Google,据说内部只用TensorFlow,但Serving方式也有多种,例如模型并行的分布式模型不能直接跑在TensorFlow Serving上,Google CloudML对外提供的HTTP接口和开源的TensorFlow Serving接口不一致。但随着项目的发展,TensorFlow team、TensorFlow Serving team以及Google Cloud team有着更好的协作,TensorFlow Serving整个项目的发展与我理想中应有的模型预估服务越趋接近,里面的架构演化与接口设计非常精彩,于是决定把这段波澜壮阔的项目发展史介绍一下。

1. 定义模型格式

2015年底Google开源了TensorFlow,2016年初有一个名为“TensorFlow Serving”的项目也在Github开源tensorflow/serving,但没有引起过多关注。我们在当时0.4.0版本就开始关注TensorFlow Serving了,当时唯一的TensorFlow模型预估服务竟没有得到大公司的关注和贡献,主要原因是当时用TensorFlow建模多但仅限于research,实际应用生产服务的很少,而且模型格式不统一,一般都是在实现TensorFlow的training代码逻辑上加上inference的逻辑。

当时看过了源码,TensorFlow Serving使用C++实现,Bazel构建,依赖于TensorFlow的C++代码,实现了一个gRPC server对外提供gRPC接口。当时官方提供了一个Inception server,可以加载使用TensorFlow官方脚本(当时models的代码在tensorflow/tensorflow,现在改到独立的repo中tensorflow/models)训练得到的模型,而那时候TensorFlow并没有标准的模型格式。在我往期的技术分享中介绍过,TensorFlow的模型格式可以有很多种,只要是包含所有op的Graph和Variable信息都可以作为模型发布,感兴趣可以看看

tobe:TensorFlow开发 – 机器学习模型赋能iOS应用​zhuanlan.zhihu.com图标

和https://medium.com/@tobe_ml/all-tensorflow-models-can-be-embedded-into-mobile-devices-1932e80579e5。

最早TensorFlow Serving推荐的模型格式是使用exportor接口导出的模型,而如果现在还使用exportor API的话会收到一个警告,因为这个接口即将被废弃,官方推荐使用最新的SavedModel格式。本质上exportor和SavedModel导出的文件都是一堆protobuf二进制文件,内容和大小与TensorFlow Graph + Checkpoint文件几乎一样,因为做inference只需要拿到op和权重就足够了。这里有个优化点,当时我们特别希望SavedModel格式和Checkpoint可以合并,这样训练过程中的任意一个保存点都可以用来"online training"或者“online serving”,但因为TensorFlow Serving对预估需要额外的meta和目录结构支持,所以和TensorFlow team谈不拢就分开两种格式,现在大家能看到的标准TensorFlow SavedModel格式大概是这样的。

2. 实现通用接口

前面提到,最找TensorFlow Serving官方提供的是一个Inception server实现,也就是说可以通过bazel build编译生成一个Inception图像分类模型服务的binary。一开始我们也是这样做的,在项目中编写cc文件,通过SessionBundle加载TensorFlow模型文件,使用Session::Run的C++接口来做inference,由于依赖TensorFlow core代码,我们选择在TensorFlow Serving项目中写代码和bazel build。

开发的过程中和组内大牛讨论发现,我们的TensorFlow模型接口和Inception模型相当类似,输入的结构都是Tensor,输出结构也是Tensor,代码都可以用std::vector<std::pair<string, Tensor>>来表示,于是有了个大胆的想法,把我们的Server实现成一个通用的TensorFlow Serving,这样不同模型不需要重新编译和重复写C++代码才能上线。本着为社区做贡献的心态,我们准备好代码和详细的设计文档,很快提交了个IssueConsider the unified interface and general inference service for all models · Issue #165 · tensorflow/serving,由于当时还没有SavedModel的signature,因此模型的输入和输出都是通过tf.add_to_collection接口导出的Graph文件中的。

当然正如上面的评论所说,Google马上也发布了PredictionService接口,就是定义了一个protobuf文件,声明了这个“通用预估服务”的输入输出就是灵活多变的map<String, Tensor>,大家在目前最新的TensorFlow Serving中依然可以看到当时的接口定义tensorflow/serving。

通用的Inference接口定义,是继通用的TensorFlow模型格式后第二个重要里程碑事件,也是TensorFlow设计者在考虑预估服务前已经留下的重要的拓展接口,无论是图像数据、文本数据、结构化数据、稀疏数据都可以用Tensor来表示(不用怀疑Tensor的表达能力,SparseTensor也可以由三个稠密的Tensor来表示),加上变长的Map数据结构,用户用一个Python的字典或者JSON就可以完整表达一个机器学习或者深度学习模型的Inference接口。

除此之外,TensorFlow很早就在core代码(C++和Python都支持)上加入了模型导入导出的saved_model模块,而前面介绍的这个模型签名可以很方便导出到模型的Graph文件中。不要小看这个设计,在TensorFlow Serving开源很久以后,Amazon也发布了一个“通用的”模型预估服务mxnet-model-serverawslabs/mxnet-model-server,这个服务的接口毫无意外地也用了Map<String, Tensor(NDArray)>作为预估接口,但实际上MXNet或者ONNX本身是没有导出模型签名的功能的,因此用户在导出MXNet的二进制模型的同时,需要手写一个类似下面的模型输入输出描述JSON文件,才能使用mxnet-model-server上线。近期我们也希望向MXNet社区贡献,让MXNet模型导出API可以原生支持这种通用的模型接口描述属性。

3. 多客户端支持

TensorFlow Serving在支持通用模型以后,理论上服务管理人员和运维人员就不需要额外开发预估服务了,只需要在待部署的服务器环境编译一次TensorFlow Serving就可以了,因为C++应用对服务器架构或者是否使用GPU类库有依赖,因此源码编译不可避免,当时社区也没有把编译好的二进制包放在apt和yum源上(到本文发布为止使用apt-get下载的TensorFlow Serving也只有CPU版本)。

但制约用户使用TensorFlow Serving的,并不是服务端不好编译或者Serving性能问题,而是客户端必须使用当时还比较新的gRPC接口。gRPC通信的数据结构都使用protobuf定义,官方提供了多种主流编程语言的SDK,但如果要使用TensorFlow Serving,还必须依赖TensorFlow和TensorFlow Serving的protobuf文件生成的客户端代码。以最常用的Python客户端为例,用户首先要下载TensorFlow和TensorFlow Serving源码(protobuf有依赖不好直接下载),然后使用protoc编译成Python library,最后在代码里面生成TensorProto对象才能请求服务端,返回的也不是期望的Numpy ndarray数据而又是一个TensorProto对象,示例如下。

因此当时TensorFlow Serving的Github issue里面经常重复出现一类问题,就是有没有预编译好的Python SDK可以直接import访问TensorFlow Serving服务。虽然官方也提供了一个Inception的gRPC Python client,但注意的是如果参考官方项目使用bazel build出来的Python脚本,import依赖的路径和本地写Python脚本依赖的路径不同,因此这类Issue我都会推荐我们在tensorflow_template_applicationtobegit3hub/tensorflow_template_application预编译好和示例客户端代码。当然最近TensorFlow官方提供了一个tensorflow-serving-api包,可以直接pip install安装和使用,不过构建TensorProto和解析TensorProto的代码还是需要客户端实现。

除了Python,在我们生产环境还有用Java、Scala、Golang等语言,尤其在离线批量预估场景,我们常用Spark来实现多节点的分布式预估客户端,这就需要我们编写TensorFlow Serving的Scala gRPC client。当然Scala是JVM语言,我们用protoc编译protobuf文件生成Java类库,只要实现一个Java客户端可以加载预估数据和请求服务端即可。TensorFlow Serving还不够流行很重要一个原始是,这些客户端代码都是我们自己根据protobuf的定义自己实现,在官方文档或者博客上都没有找到对应的代码例子,不过这也让tensorflow_template_application这样的第三方项目在大量的客户端搜索中得到关注,也有社区贡献者分享贡献了C++客户端、Golang客户端等代码。

Be First to Comment

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注