作者:niceperf 团队 (李扬, 郭琳)
大家好,咱们是 niceperf 团队,在天池 DeepRec CTR 模型性能优化大赛中,很荣幸获得了冠军的问题 (Top 1/3802)。这篇文章复盘一下咱们的参赛教训,心愿对大家有所启发。
1. 背景介绍
咱们团队包含两名成员:李扬、郭琳,现就职于国内出名互联网公司,负责广告算法工程师。本次较量的赛题,是在给定的深度学习框架 DeepRec 下,优化 WDL、DeepFM、DLRM、DIN、DIEN、MMoE 六大经典模型的单机 CPU 训练速度。
赛题具备肯定的挑战性,咱们在日常工作中常常应用的训练性能优化伎俩次要是分布式训练和数据 IO 优化,而本次较量限定了是单机条件,而且在数据 IO 方面的性能晋升空间很无限。这就要求咱们联合模型构造与训练框架,做出更粗疏深刻的优化。
好在赛题波及的技术栈与咱们是较为符合的,较量初期短暂适应后就能够上手优化了。首先,DeepRec 的底层框架是 TensorFlow 1.15,咱们在日常工作中已对其外围代码十分相熟;此外,较量所波及的模型,团队成员在理论业务中已应用多年,可能联合对模型构造的了解,更快定位训练性能瓶颈所在之处,便于正当调配较量精力,在劣势方向重点发力。
2. 优化内容
在做优化之前,要先确定优化的切入点,先把 low-hanging fruit 拿到,再做更深刻的优化,有利于较量的推动。咱们逐模型剖析了单步耗时,统计后果如下图所示:
从数据来看,DeepFM 耗时如此之高是不合乎预期的,因而咱们将该模型作为切入点,开展后续的优化。
(1) IndicatorColumn 算子优选 (DeepFM)
DeepFM 的单步训练 timeline 如下图所示:
对上图数据做 op 粒度的下钻剖析后发现,OneHot 为耗时头部算子,而该算子来自于 IndicatorColumn,这里可能存在较大优化空间。
进一步,咱们精读了 IndicatorColumn 的源码。为了兼容定长 & 非定长的特色,其输出类型为 SparseTensor,输入类型为 Tensor,为该特色的 multi-hot 表白。具体处理过程分为三步:
Step 1:调用 sparse_tensor_to_dense,将 SparseTensor 转为 Tensor;Step 2:调用 one_hot,失去每个子特色的 one-hot 表白;Step 3:调用 reduce_sum,将属于同样本的子特色 one-hot 求和,失去 multi-hot 表白。
针对局部高度稠密的特色,sparse_tensor_to_dense 带来了额定的 padding,引入了有效计算。针对这一问题,咱们创立了子类 IndicatorColumnV2,扭转原有的 multi-hot 生成形式,采纳 scatter_nd 代替 sparse_tensor_to_dense + one_hot + reduce_sum。代码示意如下:
tf.scatter_nd(indices=multi-hot 非零值坐标,updates= 全 1 向量,shape= 原输出 sparse tensor 的 dense shape)
优化后单步训练耗时从 500 ms 升高至 75 ms,前后性能比照如下两表所示,其中红框中的 ConcatV2 算子是用来将多个特征向量拼接送入后续 MLP 应用的。
ConcatV2 算子是 “ 并行特色解决 ” 与 “ 串行 MLP 多层计算 ” 的分界点,它的耗时排名从第 9 名变为了第 1 名,可见特色解决阶段的 op 并行效率失去大幅晋升。这一晋升一方面来自于 IndicatorColumn 自身的性能优化,另一方面来自于无限硬件资源下,优化后的 IndicatorColumn 让出了更多线程供其余算子应用,从而带来了更显著的性能晋升。
(2) RNN cell 算子交融 (DIEN)
针对 DeepFM 的优化告一段落,咱们将关注点转向单步耗时第二高的 DIEN。其整体构造如下图所示:
在 DIEN 中有两个 RNN 层,即 GRU 与 VecAttGRU,别离对应上图中两个红框内的构造。因为 RNN 采纳循环构造,所以很天然的想法是针对单步 cell 做优化,晋升单步执行性能从而带动整体性能晋升。
较量中咱们别离针对 GRU cell 与 VecAttGRU cell 编写了前向计算与梯度计算的 c++ 算子,以代替原有的多个算子形成的子图,DIEN 整体耗时升高 ~67.96 秒。以 VecAttGRUCell 为例,前向 op、梯度 op 执行逻辑如下图所示:
(3) Attention layer 算子优选 (DIN & DIEN)
DIN 与 DIEN 中都用到了 Attention layer,在它们的原始实现中,存在大量有效 padding。这是因为不同用户的历史行为序列长度不一,当多个样本 batching 在一起时,容易呈现局部样本序列短、局部样本序列长的景象。
在原始实现中,会将各个样本的 query padding 到对立长度,与 facts 交互后生成三维张量送入后续 MLP 计算。因为引入了有效 padding,导致 MLP 计算过程存在额定计算量,效率较低。
优化办法就是通过组合适合的算子,去除 padding 局部,具体过程如下图所示。
比照两张图的红框局部,进入 MLP 的张量 shape 从 [B,T,4C] 变为 [N,4C]。训练过程中 BT >= N 恒成立,通常一个 batch 内的序列长度不平衡,BT/N 越大,性能收益越大,最终 DIN 与 DIEN 共计耗时升高约 141.38 秒。
(4) 序列特色解析算子交融
在模型输出特色解析环节,存在诸多琐碎的小算子形成的子图,比方求序列均值特色的子图,典型案例如 history_price,原始数据为数值序列通过某个分隔符拼接形成的字符串,构建模型输出时须要将该字符串 split 后转成数值再通过多个步骤求均值,本次较量中咱们编写了两个 c++ 算子 (SparseSequenceLength、StringSplitToNumberAndMean) 替换掉了整个繁冗的过程。其余被替换的还有:求序列特色 hash 分桶的子图、序列特色截断的子图,累积耗时升高约 88.47 秒。
(5) 工作流调度优化
对工作流的优化也是咱们的一项重点工作。
第一个优化点是 “ 异步检查点 ”,思路是将 checkpoint (即检查点) 的保留过程异步化,缩小对模型训练过程的烦扰。具体的优化办法是开发了 AsynchronousCheckpointSaverHook 代替原有的 CheckpointSaverHook,开拓新的线程专门用来保留 checkpoint,共计耗时升高约 31.7 秒。
另一项优化是多线程配置超参优化。如:针对 elm 数据集的相干模型开启 smart stage;stage prefetch 应用独立线程池,算子并行度 intra_op 与 inter_op 均设为 8 (主线程池的配置也统一);stage prefetch threads 设为 1,capacity 设为 2;dataset map threads 设为 1 等等。
配置相干的超参优化暂无放之四海而皆准的规定,是咱们联合给定的硬件资源、数据集,在多个模型整体成果上的粗调后果,调参思路是尝试升高数据 parsing 占用的线程资源,察看是否有性能晋升。最终多个版本优化后,整体耗时升高约 33.4 秒。
3. 优化成果与总结
各优化点的成果汇总如下图所示,整体训练耗时升高 36.65% (2063.25 秒 → 1307.17 秒)。
次要优化办法分为算子优选与算子交融。两者在思路上是统一的,都是抉择性能更好的算子 (组合) 来替换原来的算子 (组合),在本文中的区别只在于是否开发了新的 macro op 替换原有的子图。
一些其余的优化措施包含:异步检查点、多线程超参优化,也对性能晋升有较大帮忙。此外,其余尝试过但没有获得全局稳固收益的措施还有很多,本文不做详尽探讨。
最初感激主办方提供的这次较量机会,过程中主办方技术团队与咱们屡次沟通合作,一直降级较量评测零碎,使评测后果更加稳固,为咱们提供了卓越的竞技环境。通过加入 DeepRec CTR 模型性能优化大赛,对深度学习框架性能优化加深了了解,期待 DeepRec 框架持续推出更多性能优化新个性。
DeepRec 开源地址: https://github.com/alibaba/DeepRec