基于ps-lite实现一个分布式DNN框架

发布于 2021-04-08 01:11

前言

本文旨在介绍一个基于ps-lite实现的分布式DNN框架(YCDL),相比其他开源的大型深度学习框架非常轻量,希望能对大家学习和开发深度学习框架带来一些帮助。

https://github.com/fukang93/YCDL

github

https://zhuanlan.zhihu.com/p/333847581

知乎

目录

  • 背景

  • 整体结构

  • 模块实现

  • Demo

  • 总结

01

背景介绍

目前开源的一些深度学习框架已经非常成熟(比如tensorflow、pytorch等),能够灵活支持图像、视频、文本、语音等各种场景。但是对于需要大规模离散id特征的业务场景(比如推荐算法、广告算法等),开源的深度学习框架支持的不是很好。所以工业界许多公司都会自研深度学习框架或者在开源tensorflow上做改写,来支持这种大规模离散id特征的场景。

YCDL是基于ps-lite的分布式DNN框架,适用于推荐或者广告等领域的ctr、cvr模型。其中ps-lite作为底层的参数服务器(parameter server)用来支持大规模的离散id特征。具体关于参数服务器和ps-lite的介绍这里不做赘述。简单来说,参数服务器是一种编程框架,重点支持大规模参数的分布式存储和通信,而ps-lite是参数服务器的一种实现。

下面会详细阐述YCDL的实现细节。

02

整体结构

YCDL整体用C++实现,通过CMAKE来管理工程项目;主要依赖库有ps-lite(负责参数的存储和通信)和Eigen(加速矩阵运算);模型网络结构由json文件来定义;计算过程包括训练和预测两个过程,其中训练过程包括以下几个步骤:

【数据预处理】 包括生成批处理的输入数据,各种特征处理(category特征one-hot embedding、连续特征离散化等)

【拉取参数】 client从ps-lite拉取模型参数

【前向传播】 依据网络模型结构和输入数据,一层一层做forward计算

【损失计算】 基于模型输出值和label做运算,得到loss值和梯度

【反向传播】 依据网络模型结构和梯度值,一层一层做backward计算

【更新参数】 将更新的梯度值传给ps-lite更新模型参数

预测过程和训练过程类似,只是少了反向传播和更新参数的步骤。YCDL整体的代码结构也是基于上述几个步骤来抽象得到:

【dataload】 负责生成批处理的输入样本

【instance】 包含一个样本的label和特征数据

dist_optimizer】 包含两个功能,一个是实现client侧的pull/push参数的操作(获取/更新参数),一个是实现 parameter server 侧的模型参数更新的算子(包括sgd、adagrad等参数更新方法)

【matrix_val】  在client侧对模型参数和梯度进行抽象,并且负责调用optimizer的pull/push算子

【layer】 负责实现网络层的各种算子(包括全连接层、concat层、activation层、loss层等),其中loss_layer是一种特殊的layer,用于损失计算

【network】 负责连接各个layer,做一层一层的forward和backward算子调用

整个框架如图所示:

03

模块实现

dataload

基于开源的 generator 代码(类似于python中的yield功能) 产出batch数据,并且对各种特征进行加工。使用方法如下:

auto data_iter = dataload(train_file, epoch, batch_size, true, shuffle_num);for (std::vector<YCDL::Instance> instances : data_iter) {
network
.forward(instances);
network
.backward(instances);}

dist_optimizer

optimizer分为worker和server两个部分:

worker的成员变量是ps-lite中的KVWorker,通过调用KVWorker中的Pull/Push来获取server中存储的模型参数。

server的成员变量是ps-lite中的KVServer,通过编写KVServer中的request_handle_函数,来处理模型参数更新机制。模型参数更新机制主要涉及两个部分,第一是优化算法,比如sgd、Adagrad、Adam等的实现;第二是server端参数的同步/异步更新,当有多个worker的时候,是所有worker的梯度都收集后才更新,还是收到一个worker的梯度就立即更新。

// adagrad virtual void push(std::vector<ull> feas, std::vector<std::vector<double>> &vals) override {
for (int i = 0; i < feas.size(); i++) {
ull fea
= feas[i];
auto &arr = _mp[fea], &arr2 = _mp2[fea];
for (int j = 0; j < vals[i].size(); j++) {
arr2
[j] += vals[i][j] * vals[i][j];
arr
[j] -= _learning_rate * vals[i][j] / (sqrt(arr2[j]) + _eps);
}
}}

matrix_val

matrix_val抽象了模型的参数,包含了weight和grad,通过调用optimizer中的pull/push函数来更新参数。分为 DenseMatrixValue 和 SparseMatrixValue 两种,其中dense是针对nn层参数,sparse是针对大规模离散id特征参数。

layer

不同的layer算子通过重写forward和backward函数来实现功能,例如下面的loss layer中实现了logloss 和 mse:

class LossLayer : public NNLayer {
public:
void load_conf(const nlohmann::json &conf) override {
_label
= global_matrix_value().get_matrix_value(conf["label"]);
_input
= global_matrix_value().get_matrix_value(conf["input"]);
_loss_func
= conf["loss"];

// for auc stat
global_matrix_value().add_matrix_value("predict", _input);
}

void forward() override {
}

void backward() override {
if (!_input->_trainable) {
return;
}
if (!_input->_has_grad) {
_input
->_grad.setZero(_input->_val.rows(), _input->_val.cols());
_input
->_has_grad = true;
}
if (_loss_func == "logloss") {
auto tmp = _input->_val;
tmp
= 1.0 / (1.0 + (-_input->_val.array()).exp());
_input
->_grad += tmp - _label->_val;
} else if (_loss_func == "mse") {
_input
->_grad += _input->_val - _label->_val;
}
}

std
::shared_ptr <MatrixValue> _label;
std
::shared_ptr <MatrixValue> _input;
std
::string _loss_func;
};

network

network负责初始化参数,初始化网络层,按层forward和backward。

Network(std::shared_ptr<Optimizer> opt) {
_optimizer
= opt;
load_weight();
load_layer();}void forward(std::vector<Instance> &instances, bool is_train = true) {

std
::vector<std::vector<SLOT_ID_FEAS>> feas_lines;
std
::vector<double> labels;
for (int j = 0; j < instances.size(); j++) {
Instance
&ins = instances[j];
feas_lines
.push_back(ins.slot_feas);
labels
.push_back(ins.label);
}
_input
->pull(feas_lines, is_train);
_label
->update_value(labels, labels.size(), 1);

for (int i = 0; i < _matrix_vec.size(); i++) {
auto &weight = _matrix_vec[i];
weight
->pull(is_train);
}

for (int i = 0; i < _layer_vec.size(); i++) {
auto &layer = _layer_vec[i];
layer
->forward();
}}void backward(std::vector<Instance> &instances) {
for (int i = _layer_vec.size() - 1; i >= 0; i--) {
auto &layer = _layer_vec[i];
layer
->backward();
}

for (int i = 0; i < _matrix_vec.size(); i++) {
auto &weight = _matrix_vec[i];
weight
->push();
}

std
::vector<std::vector<SLOT_ID_FEAS>> feas_lines;
for (int j = 0; j < instances.size(); j++) {
Instance
&ins = instances[j];
feas_lines
.push_back(ins.slot_feas);
}
_input
->push(feas_lines);}

04

Demo

基于YCDL训练一个nn模型,数据来源是UCI credit card(数据集),该数据集是一个二分类任务,特征有16个(包括连续值特征和category特征),随机将数据集split成训练集和测试集(训练集和测试集比例 = 10:1)。

模型网络结构为:input层(id特征离散化为embedding)->  全连接层 -> activation层(sigmoid)-> 全连接层 ->  loss层(logloss),optimizer选择的是Adagrad。具体模型网络配置如下:

{
"optimizer": {
"name": "DistAdagrad",
"learning_rate": 0.1,
"eps": 1e-7,
"sync_mode": 1
},
"epoch": 100,
"batch_size": 50,
"shuffle_num": 10,
"test_file": "./data/uci_data/bank/test",
"train_file": "./data/uci_data/bank/train",
"input_params": {
"slots": "0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15",
"dim": 2,
"input_name": "input",
"label_name": "label"
},
"params": [
{
"name": "weight1",
"row": 32,
"col": 50,
"need_gradient": true
},
{
"name": "weight2",
"row": 50,
"col": 1,
"need_gradient": true
}
],
"layers": [
{
"name": "DenseLayer",
"input1": "input",
"input2": "weight1",
"output": {
"name": "out1"
}
},
{
"name": "ActivationLayer",
"input": "out1",
"output": {
"name": "out2"
},
"act": "sigmoid"
},
{
"name": "DenseLayer",
"input1": "out2",
"input2": "weight2",
"output": {
"name": "out22"
}
},
{
"name": "LossLayer",
"input": "out22",
"label": "label",
"loss": "logloss"
}
]}

训练模式为伪分布式(单机启动多个服务,用来模拟多台机器启动多个服务的情况),其中server数设置为2,client数设置为2,运行结果如下:

epoch_num: 1
train:
auc: 0.722387, loss: 0.049665, acc: 0.886026, pre: 0.558566, recall: 0.144715, pre_avg: 0.0969355, label_avg: 0.117541
test:
auc: 0.704131, loss: 0.0495007, acc: 0.88925, pre: 0.507463, recall: 0.152809, pre_avg: 0.0983812, label_avg: 0.11125

epoch_num: 2
train:
auc: 0.774777, loss: 0.0446702, acc: 0.892019, pre: 0.658615, recall: 0.168869, pre_avg: 0.107862, label_avg: 0.117541
test:
auc: 0.749038, loss: 0.044903, acc: 0.894, pre: 0.585366, recall: 0.161798, pre_avg: 0.107448, label_avg: 0.11125

epoch_num: 3
train:
auc: 0.826456, loss: 0.0414073, acc: 0.894349, pre: 0.685606, recall: 0.186829, pre_avg: 0.102009, label_avg: 0.117541
test:
auc: 0.803738, loss: 0.0419598, acc: 0.895, pre: 0.601626, recall: 0.166292, pre_avg: 0.100755, label_avg: 0.11125

epoch_num: 4
train:
auc: 0.856686, loss: 0.0401306, acc: 0.898013, pre: 0.640632, recall: 0.301404, pre_avg: 0.16531, label_avg: 0.117541
test:
auc: 0.844017, loss: 0.0411285, acc: 0.8945, pre: 0.553991, recall: 0.265169, pre_avg: 0.163222, label_avg: 0.11125

epoch_num: 5
train:
auc: 0.87598, loss: 0.0385586, acc: 0.898328, pre: 0.696043, recall: 0.239678, pre_avg: 0.0662957, label_avg: 0.117541
test:
auc: 0.865775, loss: 0.0393647, acc: 0.8945, pre: 0.572327, recall: 0.204494, pre_avg: 0.0637237, label_avg: 0.11125

epoch_num: 6
train:
auc: 0.88926, loss: 0.0366161, acc: 0.901507, pre: 0.685579, recall: 0.299339, pre_avg: 0.0915445, label_avg: 0.117541
test:
auc: 0.879411, loss: 0.0376768, acc: 0.8965, pre: 0.584699, recall: 0.240449, pre_avg: 0.088683, label_avg: 0.11125

epoch_num: 7
train:
auc: 0.898788, loss: 0.0353137, acc: 0.903691, pre: 0.692986, recall: 0.324319, pre_avg: 0.108059, label_avg: 0.117541
test:
auc: 0.888408, loss: 0.036555, acc: 0.89875, pre: 0.59901, recall: 0.27191, pre_avg: 0.105224, label_avg: 0.11125

epoch_num: 8
train:
auc: 0.906711, loss: 0.0345134, acc: 0.904661, pre: 0.673099, recall: 0.367258, pre_avg: 0.118715, label_avg: 0.117541
test:
auc: 0.896222, loss: 0.0358812, acc: 0.89875, pre: 0.584746, recall: 0.310112, pre_avg: 0.115217, label_avg: 0.11125

epoch_num: 9
train:
auc: 0.90982, loss: 0.0340825, acc: 0.90585, pre: 0.683131, recall: 0.371181, pre_avg: 0.11505, label_avg: 0.117541
test:
auc: 0.896645, loss: 0.0358332, acc: 0.8995, pre: 0.591489, recall: 0.31236, pre_avg: 0.111392, label_avg: 0.11125

epoch_num: 10
train:
auc: 0.912408, loss: 0.0337313, acc: 0.906651, pre: 0.694651, recall: 0.367258, pre_avg: 0.11059, label_avg: 0.117541
test:
auc: 0.897747, loss: 0.0356334, acc: 0.901, pre: 0.609865, recall: 0.305618, pre_avg: 0.10664, label_avg: 0.11125

epoch_num: 11
train:
auc: 0.91614, loss: 0.0332522, acc: 0.907234, pre: 0.695669, recall: 0.37469, pre_avg: 0.125679, label_avg: 0.117541
test:
auc: 0.901517, loss: 0.0351311, acc: 0.90175, pre: 0.615044, recall: 0.31236, pre_avg: 0.122213, label_avg: 0.11125

epoch_num: 12
train:
auc: 0.919186, loss: 0.0328013, acc: 0.908423, pre: 0.702345, recall: 0.383361, pre_avg: 0.112704, label_avg: 0.117541
test:
auc: 0.90226, loss: 0.0350276, acc: 0.90075, pre: 0.605263, recall: 0.310112, pre_avg: 0.108852, label_avg: 0.11125

可以观察到模型的训练和测试auc都能逐渐提升,loss逐渐降低。

05

总结

YCDL目前只是一个草稿版本的深度学习框架,初衷只是用来学习和了解深度学习框架的细节。由于时间关系,还有很多地方没有完善(比如运行效率,自动求导,各种网络层rnn/cnn/attention的实现等),后续会持续更新完善,也欢迎大家一起提pr优化代码。

深度传送门
专注深度推荐系统与CTR预估
67篇原创内容

关于深度传送门

你点的每个,我都认真当成了喜欢

本文来自网络或网友投稿,如有侵犯您的权益,请发邮件至:aisoutu@outlook.com 我们将第一时间删除。

相关素材