之前的文章中介绍了TF-Serving的CPU和GPU版本的编译方法,现在介绍一下在实际使用中,遇到的各种各样的问题,重点是GPU版本使用batching方面遇到的问题。

CPU版本TF-Serving的问题

CPU版本的TF-Serving有两种形式,一种是python环境;另一种是源码编译的CPU版本(或者直接使用docker)。

CPU版本的主要问题在于性能开销,如果不是online inference,或者online但请求量不大,CPU版本也够用。但如果请求量大,CPU版本的性能就捉襟见肘了,例如我们的线上服务,使用python环境,几千qps就需要几十台物理机,成本太高。同时,Batching在CPU版本上,实测效果并不明显(可能各个模型情况不尽相同),batching主要还是作用在加速设备上,比如GPU(In particular, batching is necessary to unlock the high throughput promised by hardware accelerators such as GPUs

Python版本还有一个问题就是捉摸不定的接口格式,例如”Batched output tensor has 0 dimensions“,开启batch之后,请求必须是多图,关闭batch之后,单图多图都可以,详情可见这个issue。在实际使用中, 按batch大小在客户端做chunk的时候,不可避免的会有剩余单图的情况,这样就比较尴尬,这应该是tf-serving的实现问题。

GPU版本TF-Serving的问题

解决GPU版本的编译问题之后,最主要的问题就是batching的使用和调优上了,虽然TF-Serving官方提供了一个文档说明,但实际使用中,问题依然很多,最主要的问题就是:(1)batching不起作用(issue 1312);(2)Multi GPU环境下的使用问题(issue 311)。下面简单说明一下我们在这两个问题上做的测试以及最终处理方法。

batching没有效果

Batching主要是将多个请求中的数据合并成一个batch同时执行以达到加速的效果,官方文档里提到了四个参数:

  • max_batch_size:batch的大小,影响吞吐/延迟
  • batch_timeout_micros:形成一个batch的最大等待时间,影响最大延迟
  • num_batch_threads:并行batch的数量,控制并行程度
  • max_enqueued_batches:排队的数量,太短影响吞吐,太长影响耗时

从我们实验的结果来看,对于online inference,这几个参数的重要程度依次是batch_timeout_micros max_batch_sizenum_batch_threadsmax_enqueued_batches

以图像识别场景为例,一个请求一张图片,最开始没有开启batch,主要参数都是默认的情况下,P4单卡吞吐 <100 qps。开启batch,但batch_timeout_micros设置的比较大时,耗时很长,效果很差。官方文档中说的“Temporarily set batch_timeout_micros to infinity while you tune max_batch_size to achieve the desired balance between throughput and average latency. Consider values in the hundreds or thousands.”有一定的误导,对于在线预测,由于需要将多个请求合并成一个batch,这个等待的时间并不能很长,几毫秒就够了,也就是接下来所说的,”For online serving, tune batch_timeout_micros to rein in tail latency. The idea is that batches normally get filled to max_batch_size, but occasionally when there is a lapse in incoming requests, to avoid introducing a latency spike it makes sense to process whatever’s in the queue even if it represents an underfull batch. The best value for batch_timeout_micros is typically a few milliseconds”,一开始没有理解这一点,按照文档步骤去做参数调优,测试结果很是让人困扰。直到将batch_timeout_micros改成几毫秒的时候,才看到batch的收益,结合其它几个参数调优,P4单卡吞吐陆续上到了350qps,最终优化到了480qps(合理设置batch size,以及server自身的参数)。

另外文档中还说的”Zero is a value to consider; it works well for some workloads”,也产生了一些误导,主要是没有说明是哪些workloads适用0的情况。对于一次请求一张图片,将batch_timeout_micros设置为0意味着不使用batching,因为每个请求都不等待后续请求,自然也就不能形成batch。但是,在使用客户端batch的时候,可以设置为0测试一下,因为在客户端拼好batch大小的数据以后,server端是可以不用等待直接执行的。接下来解决Multi GPU使用率的问题,就用到了客户端batch这一点。

Multi GPU使用率的问题

开启batching并单卡调优之后,是不是直接扩展到多卡就可以了呢?本以为是的,但现实给了一记暴击。TF-Serving对多GPU的支持是依赖于模型的定义,模型训练时使用多卡,那么inference的时候也要求使用同等数量的卡。如果模型训练用的单卡,虽然从显存占用上看是多卡,但实际只会用第一张卡。所以,要用多张卡,需要使用NVIDIA_VISIBLE_DEVICES环境变量去设置可见的GPU设备,然后启动一个TF-Serving实例,有8张卡,就需要启动8个实例。

为了Load Balance方便,我在这8个实例前加了一个Nginx做反向代理,然后一个请求一张图,加大压力去压,期望能获得480*8的qps,但实际并没有,调整timeout参数也至多只能压到500左右的qps,推测还是因为做了balance,所以在单张卡上难以形成batch。多方寻找原因未果,只能尝试客户端batch的形式,所谓客户端batch,也很简单,就是一个请求中包含多张图片,请求格式参见RESTful API

使用客户端batch之后,吞吐量是上去了,但还会遇到压力结束,TF-Serving进程意外终止的情况,而直接压8个实例则没有这个问题,这个问题没有详细追究,如果有人知晓原因还请告知。最终是在客户端做了Load Balance和batch,随机请求这8个实例,8个GPU的使用率都可以压到80以上,总体的吞吐也能到4000张图/每秒

当然,对于客户端来说,也有一些优化的手段,比如,http请求的并行化,这里推荐使用事件机制,例如evhttpclient,压缩客户端等待时间。

其实还有一些例如tail latency的问题没有解决,不过至此,最主要的吞吐问题已经差不多了,剩余问题解决了再补充。

最后附上一些相关的资料,方便以后查询:

  1. How we improved TensorFlow Serving performance by over 70%
  2. 基于TensorFlow Serving的深度学习在线预估
  3. Optimizing TensorFlow Serving performance with NVIDIA TensorRT
  4. Optimizing TensorFlow Models for Serving