Fork me on GitHub

标签 performance 下的文章

TF-Serving使用Batch提升性能

之前的文章中介绍了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

记 CentOS7一处内存cache导致的性能问题

问题背景

线上业务集群迁移到新机器,在运行一段时间后,发现新机器不是很稳定,各个机器的 cpu idle 和耗时会出现一些尖峰式的震荡,idle 最低的时候会压到0,同时也会出现不少超时请求,看起来,机器在那一瞬间像被卡住了一样,但又能较快恢复。如下图所示

6F205DED-DBB5-412F-AB47-BBC944D7F36A.png

与 NUMA 和 NUMA balancing 的问题不同,之前出问题的机器会比较稳定的耗时长,idle 低,性能差,而这些机器则是不稳定出现,而且出问题的时间很短,很难抓住现场。而且这些机器所在机房本身又有超电风险,又加开了睿频,一时间,我们也很难确定问题是来自于外部还是来自于机器本身。

问题解决

问题的结局也很偶然,是有同学在测试其它业务的时候发现系统 buff/cache 过高,而他们的业务需要申请大量内存,当 buff/cache 填满之后,系统性能明显下降,手动清理之后恢复。看表象和我们的情况不太一致,我们的业务也不像他们那样大量使用内存,抱着试一试的态度,也看了一眼系统 buff/cache 的情况,还真发现两者存在正相关的关系(当然不是所有 buff/cache 高的机器都有抖动的情况,而是抖动的机器绝大多数都是 buff/cache 高),如下图所示:

A8F5039A-EDDC-4CED-805A-B3F751B64793.png

手动释放 cache 也比较简单,直接执行echo 3 > /proc/sys/vm/drop_caches命令即可清除,注意在如上面这样 buff/cache 巨大的情况下,清理会执行较长时间,也会占用一个 cpu,不建议在高峰期操作,对正常机器,这个操作也是流量有损的。另:官方文档里强调这个操作可能会导致严重性能问题,不建议在生产环境未经实验就开始使用。

至于为何会有问题,Tuning kernel memory for performance这篇文章里会有介绍,在此不班门弄斧。问题大致原因是,虽然 buff/cache 被认为是可用的内存,但使用时,也需要甄别那些块是可以释放使用的,因此当所有内存都被 buff/cache 占用后,在某些情况下,清理释放这些内存块也需要大量占用 cpu 资源导致系统响应变慢。

应对这个问题的简单方法是写个定时任务定时清理内存,但这样治标不治本,另外一种较持久的方法是设置保留内存的大小,在上面文章的 solution 里也有介绍,设置/proc/sys/vm/min_free_kbytes的大小即可,一般设置内存大小的10%为保留内存,当系统发现 free 的内存小于该值的时候,会开始寻找可以释放的 buff/cache,使得 free 的值在保留值之上。不过注意这样的方式机器重启之后就会失效,一劳永逸的方法是修改/etc/sysctl.conf,修改vm.min_free_kbytes的值即可。另外文中提到,对于耗时敏感的服务,任何程度的 swap 都会导致性能受损,可以将vm.swappiness设置为0,不使用 swap。

再次强调,生产环境直接修改这些值可能会导致严重后果,请先在灰度环境或者流量低峰测试之后再进行操作。建议详细阅读Tuning kernel memory for performance这篇文章,正确理解相关参数的含义。

开发运维中遇到的一些小但很坑的问题

系统迁移过程中发现的一些坑(CentOS 6 到 CentOS 7),并不一定有普适性,记录在此:

TCP_NODELAY

socket 交互过程,即使一个简单的小数据包,发现网络传输上的耗时也有将近40ms(或者38ms,39ms的样子),同样的代码,在原系统上毫无问题,新的系统上就死活表现不正常。最终发现是TCP_NODELAY未开启,简单的说,就是数据包要凑够一定大小才发送,否则就等一个固定的时间间隔才发送,主要是对于以前带宽小,网络条件不好时候的特定优化,对如今耗时敏感的场景并不合适。相关介绍,请参见:TCP启用和禁用 TCP_NODELAY 有何影响?

gettimeofday

gettimeofday,在不同系统上,表现不同,最坏情况下,性能开销比较大,如果是需要计算时间间隔,可以实测与 clock_gettime 的对比(建议简单写个循环,测试一下耗时,在我们的环境上,gettimeofday 进入了系统调用,开销差了10倍)。相关讨论见:faster equivalent of gettimeofdaygettimeofday() should never be used to measure time

CentOS 7 更换内核

centos 7上更换内核的文章很多,除了 rpm 或者 yum 安装新内核的包外,就是更改 grub 的配置了,绝大多数文章也会说到先查看新内核 menuentry 的序号,然后修改/etc/default/grub 中 GRUB_DEFAULT 为对应的序号即可,或者将GRUB_DEFAULT 改为 saved 然后使用 grub2-set-default 设置新内核的序号或者 title 即可,然而……我遇到的情况则是以上操作重启后均未生效,百般实验,最终实践证明,grub2-mkconfig -o /boot/grub2/grub.cfg 使之前的操作生效,这一步应该是在重启前,修改 grub 配置之后,最终证明靠谱的操作顺序是:

# 安装对应的内核rpm 包
rpm -ivh --oldpackaged --replacefiles  [kernel-headers.x86_64  kernel-devel.x86_64 kernel-tools-libs.x86_64 kernel-tools.x86_64 kernel.x86_64]

# 查看新内核 menutry 启动顺序
cat /boot/grub2/grub.cfg |grep menuentry

# 设置内核启动顺序
sed -i "s/GRUB_DEFAULT=0/GRUB_DEFAULT=1/g" /etc/default/grub

# 设置生效
grub2-mkconfig -o /boot/grub2/grub.cfg

GPU 显存变小

事情是这样子的,有一批P4的线上 GPU 机器是分批到位的,cuda 驱动也由厂商安装好,安装线上服务时发现第一批机器显存比其它机器要少接近500M,导致不能与其他机器一样同构部署。具体表现是使用 nvidia-smi 命令查询时,显示显存只有 7606 MiB,而正常的机器则有 8115 MiB。这个问题困扰了好几天,各种更换内核,重装驱动未果,正准备让厂商替换时,突然发现其 ECC 并没有关闭,使用 nvidia-smi -e 0 关闭重启之后,发现显存正常,恍然大悟。ECC memory 是一种修正错误内存技术,需要占用一定存储空间,关闭之后不进行检查,性能也能有10%~15%的提升。果然各种奇奇怪怪的问题最终的原因都很简单

其它

...

记 CentOS 7上一个诡异的性能问题

问题背景

线上系统从 CentOS 6 迁往 CentOS 7,整个过程较为顺利,但在使用中发现了一个奇怪的问题,在处理计算任务的机器上,CPU计算部分的耗时在不同节点上存在差异且差异显著,如下图所示:

A12280BA-1D10-4FC6-8C61-66C73A1A424B.png

出问题的机器计算耗时波动剧烈,耗时差别达到30%~50%,极端的 case 会更高,但并非所有机器、所有请求都受到影响。在初期问题机器只集中在个别机器和高峰时间段,高峰过后就会恢复,到后期则是随机的机器和全时间段,压力退去之后也不能恢复,重启大法也不能奈何。
这个问题的诡异之处就在于它的飘忽不定,定位经历了很长时间,也是很偶然的看到一篇文章才解决了问题

解决过程

CentOS 7集群上线之初由于存在两种机型,差别仅在于磁盘的区别,而出问题的机器则集中在同一种机型上,所以开始很自然的认为是机器配置导致的问题,观察各项监控,发现 cpu.system占用较高,说明有频繁的系统调用,最常见的情况自然是 IO 有问题,排查网络交互均正常,内存看不出问题,加上磁盘本身就存在区别,程序又有大量的日志输出,监控 agent 一直槽点多多,怀疑的重点转移到磁盘读写这块儿了,刚好磁盘的监控确实也有点不同(下图中下方红线部分):

A5B1559E-2447-476A-8BB1-BC8712399D0A.png

由于代码都是原来的一套,在原来的机器上也没有问题,那么性能更强的新机器上,出问题的概率也很小,那么问题很可能是出现在读日志的监控 agent 上了,鉴于新agent 属于第一次使用,本身 cpu 占用率也很高,在各项指标都指向它且没有强力证据自证清白,只好当仁不让的背起了这口锅(当然最后事实证明并不是 agent 的原因,记录事情发展经过)。排除 agent 的可能后,再回过头来反复观察对比各项监控(top,vmstate,iostate,sar 以及系统监控等),除了cpu.system,cpu.context_switch,load 等始终存在区别,其它指标时常反复,没有什么线索。考虑到已花了较多的时间,而且只有固定的几台有问题,干脆在机型替换时直接换掉,一了百了。

但问题在替换机器之后发生了很大的变化,如下图:

DBEDD890-94AF-491F-A44B-917B25C6CBC3.png

在替换之前,只有固定的机器在高峰期耗时变长,而替换之后,原本正常的机器也随机的开始不正常,并且在高峰过后,这种不正常的状况会一直持续,无法自行恢复,仿佛进入了一种混沌的状态。这下问题就有点严重了。

重新集中观察对比各项监控指标,除了system 和 context_switch 之外,此时又观察到 softirq 以及 migration 进程存在区别。同时,设计实验证明,在异常时,并非整机有问题,而是部分请求(或者说部分进程)出现长耗时,而同期其它请求正常,在继续排除了 NGXIN PHP 等常驻进程的问题后,问题范围可以缩小一些了,猜测系统配置问题的可能性较大,只是是哪个,还得找。

继续实验的同时,借助 Google,将各种已明确的指标和现象抽出关键字各种搜索,终于,很幸运找到了一篇文章,大意是从 CentOS 6 向 CentOS 7 迁移后,也发生了性能下降的问题,显著的表现也是 context_switch 和 softirq 指标显著不同,load 存在区别,性能不及预期的情况,这位仁兄在历经各种心酸血泪(各种更新内核,更换 OS重装,重装驱动等等)之后才解决,初读并没觉得和我们是一个问题,再次阅读的时候,发现其中指标差异有点类似,抱着试试的态度,尝试了一下他的解决方案,问题解决!

问题解决

方案得来不易,怎么做的就不直接剧透了,和 NUMA Balancing 有关,想了解具体情况的,点击博客贡献访问量吧。

关于 NUMA,其中一篇介绍如下:
NUMA架构的CPU — 你真的用好了么? - CSDN博客