传统的离线天级别模型批训练方式滞后,难以满足业务快速发展的需求,而ODL正是解决这一痛点的关键,这一技术可以让我们充分利用实时数据分布,提升算法效率,拿到更好的业务成果。
这个优化是TensorFlow内置的一项优化,原理是将tf图中常量的计算合并起来。例如:C = A + B,其中A和B都是constant,比如 A = 2,B = 3,则在使用C时,直接使用C = 5,而不是使用C = 2+3。因为如果每使用一次C,都需要计算一次的话,则会浪费了大量宝贵的算力。
在推荐模型中,常用到ConstantFolding优化的是BatchNormalization计算,BatchNormalization节点在训练时是一个fuse过的算子,而在线预测时只是简单的矩阵乘加运算。通过将其解开成普通数学运算,可以支持TensorFlow的优化器对其进行处理,ConstantFolding就可以生效。具体实现可参照TensorFlow源码中的constant_folding。
背景
而对于ODL模型来说,我们发现TensorFlow这一原生的优化选项并没有发挥出作用。在TensorFlow TimeLine上体现为,出现了大量的矩阵运算算子(如Add、Mul、Sub)和WeightsOP,这些矩阵运算算子在BDL模型上是没有的。这一部分的计算,原本应该是被ConstantFolding优化项直接优化掉的。在优化选项失效的情况下,GPU算力有一部分就消耗在了原本不必要的计算上,浪费了宝贵的计算能力。除此之外,对于GPU的执行原理来说,执行一个算子,需要将算子的kernel加载到GPU stream执行流中,GPU的高算力会让GPU kernel计算的执行时间远远小于节点kernel launch时间,产生严重的launch bound。
// 判断使用本地缓存还是调用子图进行计算
bool useCached() {
……
int64 curTime = TimeUtility::currentTimeInSeconds();
// _countInterval和_timeInterval支持自定义值,也可以使用默认值。
if (_currCount++ % _countInterval == 0 || curTime - _lastTime >= _timeInterval) {
_lastTime = curTime;
return false;
}
return true;
}
得益于RTP的灵活性,我们可以将整个模型的图分解成若干个子图,子图之间可以连通调用。因此我们在模型图中,将原本应当被ConstantFolding优化掉的节点抽取出来,得到一张ConstantFoldable子图,通过CallGraphOP算子来进行子图调用。然后将原图中的这些节点从图中删掉,最后通过RerouteTensor,将子图与子图外节点的tensor进行重置,使用CallGraphOP的输出tensor进行替换。
优化后的TimeLine如下所示,可以看到,模型的tf图中,可被折叠的算子都已经被包含到ConstantFoldable子图中,并通过CallGraphOP调用。优化后,ODL模型在线serving阶段,大部分请求将直接使用本地缓存,只有很少量的请求会触发子图的计算。
相对于ODL模型的更新频率来讲,1~2秒的参数延迟对于ODL模型的实时效果来讲基本毫无影响。这项优化在保障模型参数时效性的同时,提升了模型在线serving的性能。在我们推全这项优化之后,集群的GPU负载显著降低。
▐ 全连接网络优化
全连接网络是深度模型中非常常见的一种结构,其基本形式就是矩阵乘法Matmul、矩阵加法BiasAdd及激活函数LeakyRelu。在TensorFlow 1.x中,全连接网络的实现使用的是keras.layers.Dense类。其中当inputs的rank大于2时,调用的是standard_ops.tensordot接口。
@tf_export('keras.layers.Dense')
class Dense(Layer):
……
def call(self, inputs):
inputs = ops.convert_to_tensor(inputs, dtype=self.dtype)
rank = common_shapes.rank(inputs)
if rank > 2:
# Broadcasting is required for the inputs.
outputs = standard_ops.tensordot(inputs, self.kernel, [[rank - 1], [0]])
……
通过tensordot代码可以看出,其生成的tf图是非常复杂的,而且还包含了Gather这样与Cuda Graph不兼容的算子。这不仅会增加全连接网络的调用成本,还会使得Cuda Graph对全连接网络的优化十分受限。我们使用Netron对TensorFlow的原生全连接网络进行了可视化,可以很明显地看出,全连接网络的结构十分的复杂。
除此之外,Cuda Graph优化无法将其覆盖,最终导致在晚高峰期间,GPU的算力无法得到充分释放,模型的RT及P99上涨严重。服务的稳定性无法得到保障,无法为我们的推荐服务提供低延时的排序服务。
我们在离线模型训练阶段,对tf框架中的keras.layers.Dense类的实现部分进行了简化,替换成了简单的Reshape-MatMul-Reshape结构(可参考keras.backend.dot实现),在算法同学使用优化后的tf框架进行训练后,我们重新部署了模型,结构变化符合预期,全连接网络的结构变得更加简洁,且避免了引入与Cuda Graph不兼容的算子,这也帮助我们在模型的GPU优化部分拿到了最大的收益。
▐ 后续的一些适配操作
模型加速核心离不开裁枝、增加并行度、提升计算效率和缓存的使用。在优化ODL模型的过程中,我们首先深入了解RTP系统在线serving的原理,进而通过对比ODL模型与传统BDL模型在线serving时的差异,找到更适合ODL模型的优化方式路径。得益于RTP系统的灵活性,我们将缓存的思想应用于组图优化的过程中,使得ConstantFolding优化思想可以覆盖到ODL模型的在线serving过程。之后我们又进一步深入分析了GPU的性能瓶颈,利用TensorFlow的Timeline功能,我们通过模型压测定位到了GPU执行过程的瓶颈点所在,并在离线训练阶段对模型结构进行了简化,使得Cuda Graph指令集硬件优化技术充分覆盖到了所有GPU计算流程,达到了提升GPU计算效率的效果,并最终通过模型图可视化的方式验证了我们的优化效果符合预期。
在经过我们对ODL模型的特点进行分析及针对性的优化之后,模型的压测单机吞吐量提升了40%左右,更彻底地释放了GPU的算力,同时也显著降低了模型响应时间RT。在晚高峰期间,ODL模型在保障RT和P99没有明显上涨的前提下,顺利度过了流量尖峰,GPU使用率从优化前最高只能达到30%左右,到优化后最高可以达到43%,更加充分挖掘了现有资源的算力,让我们使用更少的机器(节省资源30%),完美地支撑了算法同学迭代ODL模型,拿到了最终完整的算法效果与收益,保障业务持续快速发展。