ps-lite源码阅读笔记

来源:互联网 发布:湖南有色行情分析软件 编辑:程序博客网 时间:2024/05/18 16:39

- worker.h

worker的用户API;

SyncOpts

std::vector<int> deps : dependencys, 这些timestamp的操作都完成后,本操作才会被执行;

std::function<void()> callback : 本操作执行完后,该callback即被立即调用;是取代Wait的方式;由KVWorker的接收线程调用的(是不是单线程??)

std::vector<Filter> filters; : 压缩,Key-Caching,等Filter们;

KVWorker:

KVWorker(int id = NextID()) : id不知道是什么意思。。。

Push: 1个Key可以对应k个Value;非阻塞立即返回;先拷贝了一份Keys和Vals;返回值是该操作的timestamp,可以交给Wait去等待;

Pull: 1个Key可以对应k个Value;非阻塞立即返回;先拷贝了一份Keys

Wait(int timestamp): 等待一个Push或Pull操作的彻底结束

VPush: 每个Key对应不同数目个Value

VPull: 每个Key对应不同数目个Value

ZPush: Zero-copy版的Push,不复制Keys和Values;用户需要保证该操作完成之前,参数内容不许修改;

ZPull: Zero-copy版的Pull,不复制Keys;用户需要保证该操作完成之前,参数内容不许修改;

ZVPush/ZVPull: Zero-copy版的VPush/VPull

KVCache<Key, Val>* cache_ : ZPush/ZPull/ZVPush/ZVPull/Wait的核心内部实现


- app.h

Customer:

KVCache, KVStore, App, 都继承自Customer

相同custmoer-id的两个Customer之间可以用Request-Response的方式通信

是个接口空壳类,里面的实现是Executor exec_来做的

流程:

1. 当CustomerA和CustomerB的Customer::id相同时,A调用Submit, 向B发送一条request Message;

2. 到达B之后,当该消息的dependency满足时(一维上的DAG,按timestamp时序依赖),调用用户自定义函数ProcessRequest

3. B调用Reply,向A发送一条response Message;

4. A接收到后,调用用户自定义函数ProcessResponse

Submit是异步接口,把request Message queue在后台后立即返回;返回值是一个timestamp,用于同步;B可以用这个timestamp调用WaitReceivedRequest来等待这个请求直到被自己处理完;A可以用这个timestamp调用Wait等待该消息的response到来;A也可使用callback来处理response(注意这里用的是很简易的单线程实现!!)

A可以向一个node group(parameter servers)发request Message, Customer子类定义的Slice函数会被调用,用来按KeyRange划分至多个小Message中去,发到node group中去;

Wait: 如果receiver是个node group, 那要等group里所有node都返回response后,Wait才返回;

ProcessRequest:在KVStore端调用;

ProcessResponse:在KVCache端调用;

WaitReceivedRequest:0次调用,无用;

FinishReceivedRequest:由Executor调用rnode->recv_req_tracker.Finish(timestamp);

NumDoneReceivedRequest:0次调用,无用;

Reply: 调用Executor的Reply,发送reponse;

Executor exec_:核心功能都是调用Executor 的


- executor.h && executor.cc

sent_req_tracker: 相对于请求本身而言,记录发送节点发送到该remote节点的消息,是否已处理完ACK;

recv_req_tracker:相对于请求本身而言,记录remote节点发送到本server节点的消息,是否已被server处理完了;

Run(): 后台一个thread_在服务,PickActiveMsg每次拿到1个再无dependency的消息,然后交给ProcessActiveMsg去处理;

PickActiveMsg(): 扫描刚接收的消息集合recv_msgs_,对每条消息:(扫一遍后仍未找到,则wait在signal上,等有了新消息到达或者有另一条消息Finish时(有解除其他消息dependency的可能)才唤醒)

如果消息发送者已死,则删去该消息,不处理;

如果该消息是request且已被本server处理完成,或者该消息是response且本worker已处理完ACK,则判为重复消息,删之;

对server端处理请求消息而言,必须在该消息的所有dependency消息都被处理完后,才能判定为Active;

response消息到这一步默认判定为Active;

设该消息到active_msg_,从recv_msgs_中删除他,Pick视为成功返回true

ProcessActiveMsg(): 处理active_msg_:

对于request消息:调用Customer的ProcessRequest来处理; 调用FinishRecvReq来标记recv_req_tracker上Finished;调用Customer的Reply来发送response消息;

对于response消息:调用Customer的ProcessResponse来处理; 设置sent_req_tracker上Finished; 如果该response来自group,则group所有response都处理完了才往后走;调用该request的用户自定义callback函数;唤醒PickActiveMsg。注意:pull操作,来自每个server的消息,都在ProcessResponse里merge到一起了,来一个merge一个,通过timestamp,得到当初发出去的哪个request,得知那个request的receiver是group,从而要确保group每个成员的消息都IsFinished(ts)才继续往下走去调用用户callback并唤醒信号量sent_req_cond_(激活ActiveMsg的检测,放行Wait)

CheckFinished(RemoteNode* rnode, int timestamp, bool sent): 判断该消息是否被处理完了,如果RemoteNode是group,则必须group每个成员node都完成了才算完成(如果是KVWorker的Wait, 判断rnode->sent_req_tracker的timestamp是否Finished)

NumFinished:类似CheckFinished,如果RemoteNode是group,统计总共多少个成员node完成了;不是group则返回0或者1

WaitSentReq(int timestamp): KVWorker的Wait; sent_reqs_[timestamp]得到消息,及其RemoteNode,调CheckFinished查是否处理完成,wait在sent_req_cond_信号量上;

WaitRecvReq:没被调过,无用;

QueryRecvReq:没被调过,无用;

FinishRecvReq:接收到消息后,将recv_req_tracker对应的消息置为Finished; 唤醒信号量;

Submit(Message *msg): 为msg打时间戳,时间戳+1;按receiver的range将msg切分成多个子消息;一个一个将子消息通过postoffice发送出去(空的子消息,则标记sent_req_tracker为Finish);

Reply(Message* request, Message* response): 设置response的时间戳(同request的时间戳)等信息,通过postoffice发送出去;

Accept(Message* msg):把msg加到recv_msgs_队列;唤醒Run()线程;

AddNode:太复杂,没看懂呢???


- postoffice.h && postoffice.cc

邮局;SINGLETON模式,进程内独一份,专管消息发送和接受的后台服务;

ThreadsafeQueue<Message*> sending_queue_:线程安全的消息队列,存放待发送的消息们;接收消息没有队列,由recv_thread_直接调用manager_的Recv功能;

std::unique_ptr<std::thread> recv_thread_ : 专门负责接收消息的后台线程;

std::unique_ptr<std::thread> send_thread_: 专门负责发送消息的后台线程;从发送队列里wait_and_pop消息

Manager manager_:发送消息,接受消息,处理control消息,处理task消息(Accept)

Msg分成几种,msg->task.request()表示是Request还是Respone, msg->task.control()表示是不是控制型消息;


- env.h && env.cc

在FLAGS_log_dir目录下初始化日志文件;

根据当前环境变量(或MPI的rank值),首先填充FLAGS_scheduler即scheduler的IP端口信息;再获取当前IP, port, role, id,序列化到字符串FLAGS_my_node中;

如果用MPI,则scheduler是独立进城;workers+servers整个是一个MPI-job;

- van.h && van.cc

网络层;使用ZeroMQ进行实际的网络发送和接收;

- node_assigner.h

为server node分配KeyRange(EvenDivide,按区间均匀划分的),同时为node分配rank id(servers一组,workers一组)



- kv_store.h:

SetReplica和GetReplica函数,是想实现冗余热备份,但目前为空函数暂时未实现该功能。

ProcessRequest里,pull操作就Reply, push操作就暂不Reply, 但是会在后面的Executor::ProcessActiveMsg里Reply一个ACK;

- kv_store_sparse_st.h:

kvstore的单线程版本,即每个请求只用1个线程(即当前线程)来处理。

使用unordered_map来存储<Key,Value>数据。

Handle handle_:用户自定义的Push和Pull操作;

HandlePull: 把data_交给用户去填充msg的values;

HandlePush: 把msg的values交给用户去改写data_;

- kv_store_sparse.h:

kvstore的多线程版本,适用于压力大的情况,把work-load分到该机器的多个CPU上;

std::vector<std::unordered_map<K, E>> data_ : 每个线程使用1个unordered_map,无锁,线程安全;

SliceKey:每个线程所处理的Key Range是定死的(长度为bucket_size_),在本Node的Key Range内均分;每来一个请求,就把这个请求的所有Key分到每个线程负责的区间上去;如果这组Key不均匀(比如未经过Hash函数或Hash得不够散或Hash范围不够64位),那么这些线程的负载会不均衡;之所以要按区间来划分,而不是按Key数量来划分至线程,应该是为了unordered_map的线程安全,如果按Key数目来均分至各线程,会造成多个线程同时访问同一个unordered_map的冲突;

ThreadPool:通过线程池来执行每个区间上的Pull和Push用户自定义操作;

- kv_cache.h 

KVWorker发送接收功能的实际实现者;

继承自Customer;发送接收消息的功能还是调用Customer的接口来实现的;

Push(const Task& req, const SArray<K>& keys,
                  const SArray<V>& vals, const SArray<int>& vals_size,
                  const Message::Callback& cb):把东西都加到Message里面去,最后交给Custormer::Submit去发送;vals_size是变长value时每个Key对应多少个values的计数数组;msg的value[0]是实际的value数组;msg的value[1]是vals_size这个计数数组(如果是变长values的话);

Pull_: 把Pull请求的东西放到Message里面去,并且把该Message缓存起来(用于收到Response后填充对应的values);这里的callback主要负责Check正确性,调用户的callback,并删除缓存;

Slice:由系统来调用;把一个Message切分成多个小Message(这里按Key的区间范围来切分);里面有message.h的函数来实现;

ProcessResponse:由系统来调用;对push操作不做任何事;对pull操作:ParallelOrderedMatch把收到的裸Message里的数据(从多个Servers下来,不是完全有序的)摆放成有序的存在缓存里;(缓存的东西和用户buffer是同一份存储)

dynamic value size时,可以看到收到所有server的消息,才对所有消息进行拼接;而固定size时,为什么对每个消息都直接处理了???

std::unordered_map<int, KVPair> pull_data_ : 存放的是已发送走的Pull请求的数据存储;收到Response后即填充对应的values; 格式:[channel-id, kv-pair]


- parallel_ordered_match.h

把server上pull来的子range的message, 汇合到最终要返回给用户的大range的message上去;可使用多线程来实现(递归式的多线程函数技巧);但实际kvcache的ProcessResponse里,用的是每个key对应的value个数来做线程个数;里面用的是lower_bound来定位大概范围,不知道性能怎样;个人感觉可以优化,将收到的子消息都缓存起来,最后一个到来了按第一个key从小到大排序依次填充即可(他的dynamic value size部分就是这么干的)


- ps.h

CreateServerNode为什么找不到实现???


启动流程:

1. 进入main函数: 调用ps::StartSystem, 里面调用postoffice::Run和postoffice::Stop

2. postoffice::Run: 初始化日志,解析命令行,调manager_.Init, 创建发送线程和接受线程,调manager.Run

3. manager_.Init:env_.Init, van_.Init, net_usage_.AddMyNode,

                             调用App::Create (在用户的main.cpp里实现),根据角色创建worker或者server或者scheduler

                             对scheduler: 创建NodeAssigner; AddNode添加自己;

                             对worker和server: 向Scheduler发送控制消息REGISTER_NODE

Scheduler接收到REGISTER_NODE:NodeAssigner添加该node; AddNode添加该node;

                AddNode: van_.Connect(node), alive_nodes_.insert, net_usage_.AddNode, customers_的executor()->AddNode,worker和server个数都达到预设数值时:对于scheduler,把所有node的信息放在控制消息ADD_NODE里,广播给所有节点;对于worker和server: 向Scheduler发送控制消息READY_TO_RUN

当woker和server接收到ADD_NODE:对消息里附带的所有node, 依次执行AddNode(node)

当Scheduler接收到READY_TO_RUN:active_nodes_里加上该node; 当active_nodes_达到预期数目,则向所有node发送READY_TO_RUN;inited_ = true

当worker和server接收到READY_TO_RUN:inited_ = true

4. manager.Run:等待(10s则超时报错)直到inited_为true; 执行app_->Run

5. Run:

Worker的Run: 干活儿

Server的Run: 啥事儿不干直接返回

Scheduler的Run:啥事儿不干直接返回(wormwhole例子是真的指挥干活儿了)

6. postoffice::Stop:调Manager::Stop:

对于worker和server:向Scheduler发送READY_TO_EXIT,等待在done_上直到为true(即收到来自Scheduler的EXIT消息)才往后走;

对于Scheduler:等待,直到active_nodes_为空才往后走(收到一个READY_TO_EXIT则把对应的node从active_nodes_里删去);把EXIT发送给所有node

7. 尘归尘,土归土,世界终于恢复了平静,一切都结束了!


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

一条消息走过的路:

Worker端:

Worker.h:

ZPush, ZPull,ZVPush, ZVPull, 都调用cache_->Push或cache_->Pull

                  Wait,调用cache_->Wait

Kv_Cache.h:

Push: 把东西放到msg里,调用Submit提交msg

Pull: 调用Pull_;Pull_: 把东西放到msg里, 暂存在unordered_map里(pull_data_[chl]),为的是完成后作一些check; 注册callback函数(里面完成check, 调用用户callback); 最后Submit提交msg;

App.h:

                  Customer::Submit,调用Executor::Submit

Executor.cc:

把msg打上时间戳为time_, 递增本机的time_;

暂存到sent_reqs_[ts];

把msg按照server个数,来且分成多个子msg;

把这些子msg依次交给Postoffice发送(sys_.Queue(m))

Postoffice.h:

                  Queue:push到发送队列sending_queue_里; 在发送线程中被顺序发出;

 

Server端:

Postoffice.h:

接收线程的Recv里, 对control类型消息:调用manager_.Process;对普通消息(Push/Pull),调用manager_.customer(id)->executor()->Accept(msg)

Executor.cc:

Accept: recv_msgs_.push_back(msg),signal信号量;

Run循环, PickActiveMsg末尾的信号量被唤醒, 进入下一个PickActiveMsg;

PickActiveMsg: 扫描recv_msgs_;对里面每个msg:

发送方如果已死,则删掉该msg不处理了;

重复接收(XXXX_req_tracker.IsFinished(ts)),则删掉该msg不处理了;

对worker发往server的请求, 检查其所有的依赖(wait_time)是否完成(recv_req_tracker.IsFinished),都完成了就可以处理它了!

处理: 设该消息为active_msg_;从recv_msgs_中删去该消息; 调Run循环里的ProcessActiveMsg

                  ProcessActiveMsg:

                                    对worker发往server的请求, 调用obj_.ProcessRequest;在tracker中记录该消息Finished; 对于Push操作调用obj_.Reply发送ACK

Kv_store.h:

                  ProcessRequest:HandlePush/ HandlePull; 对于Pull操作,发送response消息;(对于Push操作由后面调用端ProcessActiveMsg发送ACK;

                  HandlePush/HandlePull: 最后都调FinishReceivedRequest把消息Finish记在tracker了;

  

Worker端:

Postoffice.h: 同上;

Executor.cc:

                  Accept:同上;

                  Run循环: 同上;

PickActiveMsg: 同上,唯一不同的就是不用检查依赖了;

ProcessActiveMsg:对server发往worker的response: 调用obj_.ProcessResponse处理; Finish(ts); sent_reqs_.find(ts)拿到它; 判断ServerGroup里所有的Server的该ts消息都Finish了,才彻底Finish这条发往ServerGroup的消息并调用callback; notify信号量sent_req_cond_(Wait操作就是卡在他上面)

Kv_Cache.h:

                  ProcessResponse:对push操作返回的ACK,不处理;

                                    对pull操作返回的数据: 拿到pull_data_里对应的东西; 调用ParallelOrderedMatch把本次受到的数据合并到buffer里面去

                 

Wait操作:

Worker.h: Wait, 调用KVCache::Wait

App.h:  

Wait:Customer::Wait, 调用Executor::WaitSentReq

Executor.cc:

WaitSentReq :Wait在sent_req_cond_上,唤醒后则CheckFinished(rnode, timestamp, false)

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


使用经验:

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

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

1 0
原创粉丝点击