点击率预估模型训练,在早期阶段由于模型结构比较简单,稀疏Embedding占比非常大而稠密参数较少,因此异步训练存在的参数更新冲突和延迟问题并不严重,异步训练是普遍采用的方式。随着Attention等复杂结构在稠密网络部分的应用,稠密参数的影响力变大,异步训练带来的参数更新问题越来越严重,制约着模型训练效果,另外随着GPU的应用,同步训练的性能问题也有缓解,所以同步训练渐渐成为主流。同步训练有两种方式,一种是基于Parameter Server的同步训练,一种是基于AllReduce方式的训练。以目前推荐系统领域依然重度使用的TensorFlow为例,第一种经常采用TensorFlow SyncReplicasOptimizer,第二种经常采用Horovod TensorFlow。但这两种方式都存在一个简单却多年无人去解决的问题,对于用户群体这么大的框架来说,有点匪夷所思。
同步机制在TensorFlow等分布式机器学习框架中非常重要,比如TensorFlow有以下场景需要做同步:1) 当chief worker训练完一轮后,保存模型前需要等所有worker都完成再保存模型。 2) BSP方式的SGD训练,需要每个batch做同步。 如果不做同步可能会出现如下问题: 1) TensorFlow大部分使用方案都是异步SGD,而且使用global_step做停止条件,不能保证所有worker负责的数据训练相同的轮数,速度快的worker所负责的数据将会获得更多step。 2) chief worker结束时会保存模型参数,但还存在其他worker没结束,所以模型没有完全训练完整。
2018年,公司的分布式模型训练普遍向`TensorFlow on Yarn`迁移。在公司的Hadoop集群上,使用TensorFlow通过DataSet读数据方式进行分布式训练时,在每个Epoch的最后一个Batch会卡住,导致任务一直停在那里无法结束。集群节点都是`CentOS, linux kernel 3.10.0`。如果用老的Queue读取数据不会出现这个问题,并且这个问题不是必现,只有在分布式且节点比较多的时候发生的概率比较高。
在离线训练时,为了效率考虑,我们经常把数据转成TFRecord格式,然后直接调用TensorFlow提供的Reader来读入TFRecord数据。这样在生成的`graph.pb`中,Reader会对应多个节点,如果在c++中直接导入这个`graph.pb`我们就不能使用`std::vector<std::pair<std::string, tensorflow::Tensor>>`作为`session.Run(...)`的输入了,本文讲解一下怎样处理这种情况。
之前的一篇文章中[使用TensorFlow C++ API构建线上预测服务 - 第一篇](https://mathmach.com/6d246b32/),详细讲解了怎样用TensorFlow C++ API导入模型做预测,但模型`c = a * b`比较简单,只有模型结构,并没有参数,所以文章中并没讲到怎样导入参数,本文使用一个复杂的模型继续讲解。
目前,TensorFlow官方推荐使用Bazel编译源码和安装,但许多公司常用的构建工具是CMake。TensorFlow官方并没有提供CMake的编译示例,但提供了MakeFile文件,所以可以直接使用make进行编译安装。另一方面,模型训练成功后,官方提供了TensorFlow Servering进行预测的托管,但这个方案过于复杂。对于许多机器学习团队来说,一般都有自己的一套模型托管和预测服务,如果使用TensorFlow Servering对现存业务的侵入性太大,使用TensorFlow C++ API来导入模型并提供预测服务能方便的嵌入大部分已有业务方案,对这些团队来说比较合适。
使用MapReduce on Yarn或者Spark on Yarn来生成TFRecord的过程中,会发生Hadoop和TensorFlow依赖的Protobuf版本不一致导致冲突的问题,本文通过两种方案来解决这种问题。