ps-lite参数服务器概况

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/smartcat2010/article/details/88604671

3种角色:Scheduler(1个实例),Server(M个实例),Worker(N个实例)

Scheduler: 集群控制节点;维护一份所有节点的地址表;集群起来的时候所有节点和Scheduler握手,都到齐了Scheduler才通知大家开始干活儿;途中有新增的或者挂掉的worker节点(目前不能处理server挂掉),Scheduler会通知到大家;(监控其他节点死活;发送控制消息;收集大家的进度)

Server: 存放参数(<Key, Value>们);每个server存放一部分参数,大家的key交集为空并集为全集;按Range存放,每个server存放一个连续区间内的Key们(如果Key分的不够散,就会导致Server负载不均衡,此时需要user去哈希开了Key们)

Worker: 执行算法计算任务;Data-parallel, 每个worker负责一部分Data-partition的计算;期间调用push和pull操作;push是把自己更新好的参数推送至servers; pull是从servers拉取最新的参数;push和pull可能都只是更新参数子集,但通信会涉及所有server

push和pull支持异步;两种异步方式:1. 调用返回一个数(timestamp),Wait在这个数上可以等待;2. 调用可以传进去callback函数,操作完成后自动执行该callback函数;

异步的实现:在push/pull函数里,把参数数据拷贝一份(如果用户指定了Zero-copy版本的push/pull, 则不拷贝,由用户来确保该参数数据不要马上被破坏掉,这里强制wait即为同步版本), 然后返回,不阻塞用户计算任务;后台线程在这份拷贝上,进行数据拆分(按Range分给所有server),数据发送操作; 后台recv线程,会把接收到的数据分片,放进对应消息的槽位里,该消息槽位如果满了(M个server的消息都收齐了),则signal信号量唤醒用户的Wait并调用用户注册的callback函数(注意这里是单线程,阻塞了后台recv线程!!);

Pull操作发送的是Key们,收到的是Key-Value们(参数们);Push操作发送的是Key-Value们,收到的是ACK们;

Push操作发送的一般是梯度们,在server端进行参数update; 如果worker端update好了发送参数们到server端,则同一个参数被2个worker同时计算出2个梯度来,只能有一个梯度生效!(另一个被覆盖掉了)

每个work-server信道上,有从0开始的timestamp计数,每次自动递增1;如果该消息是request且已被本server处理完成,或者该消息是response且本worker已处理完ACK,则判为重复消息,删之;小于某timestamp的则认为是以处理过的;计数是32位的,生产环境中超出则会死!!(bug)

Msg分成几种,是Request还是Respone; 是控制型消息还是数据;

启动的时候,所有进程读环境变量,得知Scheduler的IP和port(其他地址无需知道),连接Scheduler; Scheduler的环境变量要设好多少Server多少Worker,够数了就把所有IP和Port广播给大家告诉大家开始干活儿。

server的参数数据处理上,有2个版本:单线程版,多线程版(充分利用多CPU);多线程版把本range再分成了多个子range,每个range用一个独立的哈希表(unordered_map)来存储,每个线程在其自己的哈希表上操作数据;

pull操作,worker会把key们缓存在本地,key-value们到了以后再去验证一遍是不是这些key们,完全正确才删除缓存;

底层通信库:ZeroMQ

依赖关系:一维时间上的DAG

重要概念:delayed weights; 

同步SGD: 由Scheduler节点来控制,每个minibatch全局同步一次;(同步SGD,每个worker一次计算b/worker_num个样本;异步SGD,每个worker一次计算b个样本)(同步SGD, server在把worker_num个梯度分片全加到一起后,再更新w; 异步SGD, server每收到一个梯度分片,就更新w)

分布式计算框架相比单机的额外开销:1. 发送数据的网络通信开销;2. 多机同步开销(负载不够均衡,各机性能参差不齐)

额外开销:1. 同步模式:样本总数/minibatch数*(通信时间+同步时间);2异步模式:样本总数/minibatch数/worker数

收敛拖慢:1. 同步模式:sqrt(minibatch大小);2.异步模式:sqrt(minibatch大小*最大delay)

烂网络下关键消息丢失会导致机器甚至集群hang住:可配置强制每条消息,发送端收到ACK则往下走,超时则重发该消息;用时间换可靠性

可以挂接用户自定义的通信消息压缩函数

  • 接收数据的线程,收到的消息放入对应customer的队里里
  • 发送数据的线程
  • 每个customer(每个连接有一个customer)有一个从队列里取数据并处理的线程
  • 间歇地向 Scheduler 发送心跳的线程。
  • 如果定义了环境变量 PS_RESEND,那么 Scheduler、Worker 和 Server 还会启动一个监控线程(超时没收到ACK则重发消息)

给定上界 MAX(2^32-1, or 2^64-1) 和 Server 数量 N,第 i 个 Server 负责的范围是 [MAX/N*i, MAX/N*(i+1)); 注意不能整除的情况!

使用经验:

Worker采用Wait方式+多线程并发,一开始跑起来的时候性能差,因为每个任务的任务量(计算量和通信量)几乎相等,所以(计算-》Pull-》计算-》Push)的流程,所有线程一上来都卡在计算上,CPU满但是网络为空,然后所有线程都卡在Pull的通信上,网络为满但是CPU为空;增大线程个数使得线程数为CPU个数的1.5~2倍后,性能有所改善,因为顺序被打乱了,原先近乎齐步走;理想方式:采用callback异步,且确保callback是被后台多线程并发执行的(用户自己在callback里启动新线程或者调线程池);如果callback被后台用单线程实现,那必须把callback里的计算操作enqueue给线程池去做;

ps-lite要求所有Keys按从小到大的顺序交给Push或Pull,为的是按Range区间均匀划分至各个Parameter Server;每个Server内部,开启多线程的话(默认是单线程,压力大的情况会造成单CPU性能瓶颈),也会按Range来均匀划分至各个thread;如果Keys没有严格Hash好,不均匀或者取值范围在uint64内只占一部分,那就会被影射到少数Server的少数thread上,导致负载极其不均衡;

=======================================

原算法(Worker端):

1. 对1个mini-batch, 得到每个sample的非0特征值的feature-id,排序(ps-lite要求Key必须有序),去重

2. 以这组feature-id为Key, 从Server上Pull,得到对应的weights

3. 对每个sample[i], 对其所有非0特征值的feature-id对应的weight, 进行加和,得到sum_w[i]

4. 对每个sample[i]的sum_w[i],得到梯度delta[i] = sigmoid(sum_w[i]) - label[i]

5. 对每个sample[i], 扫描其所有feature-id, 设其对应的weight为weight[k],累加gradient[k] += delta[i]

6. 把所有gradient[k],Push给Server, 去更新weights

原实现:

3. 使用feature-id --> weight的map(unordered_map) ,即下面的weight[idx]

5. 使用feature-->gradient的map(unordered_map), 即下面的gradient[idx]

缺点:一个batch大小1000,每个sample个数平均2000, 1000*2000*8Byte=16MB, 在cache中放不下,频繁访问内存,造成速度慢;

原实现代码:

            for(int row = start; row < end; ++row){
                float wx = bias;
                int sample_size = train_data->fea_matrix[row].size();
                for(int j = 0; j < sample_size; ++j){
                    idx = train_data->fea_matrix[row][j].fid;
                    wx += weight[idx];
                }
                pctr = sigmoid(wx);
                float delta = pctr - train_data->label[row];
                for(int j = 0; j < keys_size; j++){
                    gradient[(*keys)[j]] += delta;
                }
            }

优化实现:

1. 把所有sample的所有key, 放到struct数组里,struct字段:{key, sample-id}

2. 把struct数组(名字为sortedKS)按key从小到大排序

3. 把key单独放在一个数组(名字为keys)里,向Server去Pull Weights, 得到weights数组

4. 对sortedKS和keys进行类归并扫描操作,匹配中的,找到struct对应的sample-id,更新对应的weight:

    sum_w[sortedKS[soredKS_id].sample-id] += weights[keys_id]

5. 循环sum_w(长度为sample个数),  得到梯度delta[i] = sigmoid(sum_w[i]) - label[i]

6. 同步骤4,再次类归并扫描,匹配中的,累加对应的gradient: 

    gradient[keys_id] += delta[sortedKS[soredKS_id].sample-id]

优点:无Hash表;顺序扫描数组;sum_w和gradient只有几KB, 可以放入cache

猜你喜欢

转载自blog.csdn.net/smartcat2010/article/details/88604671
今日推荐