Bert模型网络结构较深,参数量庞大,将Bert模型部署成在线服务在实时性和吞吐上面临巨大挑战。本文主要介绍360搜索将Bert模型部署成在线服务的过程中碰到的一些困难以及做的工程方面的优化。
在360搜索场景下对在线Bert服务的延迟和吞吐有极高的要求。经过前期的调研探索和试验,将Bert模型做成在线服务主要有以下3个挑战:
基于以上几个困难点,我们前期调研了TF-Serving、OnnxRuntime、TorchJIT、TensorRT等几个热门的推理框架,在比较了是否支持量化、是否需要预处理、是否支持变长、稳定性和性能以及社区活跃度等几个维度后,最终选用了Nvidia开源的TensorRT。确定了框架选型之后,我们针对Bert在线服务做了几个不同层面的优化。
TensorRT推理框架本身提供的优化有:
12层Bert模型的线上延迟不能满足性能要求,我们将其蒸馏至6层的轻量级小模型。做完知识蒸馏后,在降低计算量的同时也保证了预测效果,6层模型可以达到12层模型精度的99%。经过试验验证,Bert模型层数和在线服务的性能呈正相关的关系,6层模型TP99指标相对于12层模型性能提升1倍。
在Bert模型结构中,大部分Tensor都是fp32精度,但是在推理时不需要反向传播,此时可以降低精度,在保证模型效果的基础上大幅提高模型的吞吐。经过FP16量化之后,模型推理延迟变为原先1/3的同时,吞吐提升为原先的3倍,此时显存占用也为原先模型的1/2。但是相比较原先模型,fp16量化后的模型在万分位后有损失,在360搜索的场景下经过验证,量化后的模型对最终效果几乎无影响。权衡之下,量化后的收益远大于损失。
上图中H2D和D2H分别表示从内存往显存中拷贝数据和从显存往内存中拷贝数据,Kernel表示正在执行核函数。线上请求推理时所做的三个动作为首先将请求数据由内存拷贝至显存,然后GPU发起核函数调用做推理计算,最后将计算结果由显存拷贝至内存。GPU真正执行计算的部分是执行核函数的部分(上图中蓝色部分),数据拷贝时GPU是空闲的(上图中白色部分),此时无论压测压力多大GPU都会有空闲时间,因此利用率不会压满。
解决上述问题的一个方法是增加一条Stream,使得两条Stream的核函数计算部分可以交替执行,增加GPU有效工作时间占比,GPU利用率可以压到98%以上。Stream可以理解为任务队列,H2D可以理解为一次任务,多增加一条Stream不会增加额外的显存占用,多条Stream是共享模型权重的。
上图描述了一个占有2张GPU卡的单个Bert服务进程的运行架构。从左至右依次解释出现的名词,task表示待处理的预测请求,context用来存储这条请求的上下文信息,stream表示任务流,profile描述了模型输入的限制(比如限制输入的最大batch size),engine是TensorRT将原始模型编译优化后的模型。每张GPU卡上加载一个模型,每个模型会有2条Stream共享模型权重对外提供预测服务。
每当Bert服务收到来自客户端的预测请求,这个请求将会被放入任务队列。上图线程池中的4个工作线程每当空闲时会从任务队列中取出一条预测任务,保存好上下文信息后便将请求数据通过Stream拷贝到显存,GPU调用核函数做完推理后再将结果通过Stream传回到内存,此时工作线程将结果存入指定位置后通知上层,一条完整的请求预测流程就完成了。
在搜索场景下,当天的搜索内容会有一部分热词出现,加上缓存可以有效减轻一部分计算量。在搜索系统加入请求级别的缓存之后,平均缓存命中率可达35%,极大地缓解了Bert在线服务的压力。
最开始的在线服务是采用输入维度固定的方式,即输入shape的最后一个维度为离线统计出现过的最大sequence长度,经过线上小流量验证并且统计线上请求之后,发现线上请求长度超过70的sequence占比不到10%。于是我们采取了动态sequence长度的优化方式,即采用一个请求batch中长度最长的sequence为输入长度,对其余的sequence做补零操作,经过这一优化线上性能提升7%。
做完上述优化后在测试以及小流量验证的过程中,我们也碰到了一些问题,分享给大家。
在搜索场景下,有一个多版本模型热加载的需求。开发完上线后观测到一个现象,在热加载新模型的时候,会出现TP99升高的现象,后来经过定位分析找到了原因。
在Bert在线服务做预测的时候,会有一个将模型输入数据从内存拷贝到显存的操作。而Bert服务动态加载模型的时候,也会有一个将模型权重数据从内存拷贝到显存的动作,拷贝模型到显存的时候占据了PCI总线,这时候预测请求数据从内存拷贝到显存就会受到影响,从而TP99就会升高。模型权重拷贝持续约几秒的时间,此时TP95正常,经统计仅有几条请求会有延迟升高,对业务基本无影响。
在前期开发过程中,我们观测到相同的sequence输入模型,在不同的batch size下返回的结果总是不尽相同,而是在某一固定的区间内震荡,例如返回的结果总是介于0.93-0.95之间且不固定。这个现象在TensorRT 7.1.3.4下稳定复现,后与Nvidia的同事沟通反馈,在7.2.2.3这个版本下已经修复。
单个Bert模型仅占用几百MB的显存,但是上了多版本模型的功能后,Bert在线服务有可能加载5-8个模型,如果处理不好有可能会出现OOM的问题。目前我们的处理手段是如果因显存不够而无法正常加载模型,仅仅会提示模型加载失败不会影响正常服务。加载一个新的模型的显存占用量是可以提前判断出来的,主要依据有3个:
经过前期框架调研验证,模型优化,工程架构优化以及部署探索过程后,最终Bert在线服务在360搜索场景下正式上线了。目前经过优化后的6层模型单张T4卡每秒可计算1500条qt,线上高峰期TP99为13ms。工程方面Bert在线服务稳定性和性能得到了保障的基础上,业务效果上相较于baseline也取得了可观的收益。
我们后续会持续探索推进Bert在360的应用落地,目前在搜索场景下工程方面还有一些亟需优化点: