对境准备:对于多个GPU而言,一台机器2个GPU,参数交换的流程图:

        

参数交换从main()进入train()函数,在train函数中找到对应源码为:

  1. . . . . .
  2. if (gpus.size() > ) {
  3. caffe::P2PSync<float> sync(solver, NULL, solver->param());
  4. sync.run(gpus);
  5. } else {
  6. LOG(INFO) << "Starting Optimization";
  7. solver->Solve();
  8. }

因为GPU的个数>1,所以执行sync(solver, NULL, solver->param())和run()函数,首先会执行P2PSync类的构造函数,然后执行run()函数,run函数的代码如下:

  1. void P2PSync<Dtype>::run(const vector<int>& gpus) {
  2. vector<DevicePair> pairs;
  3. DevicePair::compute(gpus, &pairs);
  4. SolverParameter param(solver_->param());
  5. vector<shared_ptr<P2PSync<Dtype> > > syncs(gpus.size());
  6.  
  7. // Build the GPU tree by finding the parent for each solver
  8. for (int attempts = ; attempts < pairs.size(); ++attempts) {. . . . . . .
  9. }
  10. for (int i = ; i < syncs.size(); ++i) {
  11. syncs[i]->StartInternalThread();
  12. }
  13. solver_->Solve();
  14. for (int i = ; i < syncs.size(); ++i) {
  15. syncs[i]->StopInternalThread();
  16. }
  17. }

在run()函数中,首先会执行compute()函数,该函数的作用是产生GPU Pairs,GPU Pairs的含义是[parent:child],对于2个GPU而言,GPU Pairs为[-1:0],[0:1],默认根GPU的parent是其本身。然后通过一个for循环构建GPU树,对于2个GPU而言,GPU树如下图所示:

        

接下来调用一个for循环为每个GPU开启一个线程,值得注意的是for循环是从i=1开始的,即为每个子GPU单独开启一个线程(这里为GPU1开启一个线程),也就是调用StartInternalThread()函数,该函数的代码如下:

  1. void InternalThread::StartInternalThread() {. . . . .
  2. try {
  3. thread_.reset(new boost::thread(&InternalThread::entry, this, device, mode,
  4. rand_seed, solver_count, root_solver));
  5. }. . . . . . .
  6. }

该函数接着会执行entry()函数,该函数代码如下:

  1. void InternalThread::entry(int device, Caffe::Brew mode, int rand_seed,
  2. int solver_count, bool root_solver) {
  3. . . . . . .
  4. InternalThreadEntry();
  5. }

该函数又会去调用InternalThreadEntry()函数,该函数是正式进入迭代运算的入口,代码如下:

  1. void P2PSync<Dtype>::InternalThreadEntry() {
  2. Caffe::SetDevice(solver_->param().device_id());
  3. CHECK(Caffe::root_solver());
  4. Caffe::set_root_solver(false);
  5. // See if there is a defined seed and reset random state if so
  6. if (solver_->param().random_seed() >= ) {
  7. Caffe::set_random_seed(
  8. solver_->param().random_seed() + solver_->param().device_id());
  9. }
  10. solver_->Step(solver_->param().max_iter() - initial_iter_);
  11. }

GPU1调用Step()函数,进入迭代过程,见如下源码:

  1. void Solver<Dtype>::Step(int iters) {
  2. . . . . . . . . . .
  3. while (iter_ < stop_iter) {
  4. . . . . . . . . . .
  5. for (int i = ; i < callbacks_.size(); ++i) {
  6. 0_[i]->on_start();
  7. }
  8. const bool display = param_.display() && iter_ % param_.display() == ;
  9. net_->set_debug_info(display && param_.debug_info());
  10. // accumulate the loss and gradient
  11. Dtype loss = ;
  12. for (int i = ; i < param_.iter_size(); ++i) {
  13. loss += net_->ForwardBackward(bottom_vec);//计算loss,一次前后向
  14. }
  15. loss /= param_.iter_size();//loss归一化
  16. . . . . . . .
  17. for (int i = ; i < callbacks_.size(); ++i) {
  18. callbacks_[i]->on_gradients_ready();
  19. }
  20. ApplyUpdate();
  21. . . . . . . . . . .
  22. ++iter_;
  23. }
  24. }

整个Step函数的运行如上所示,首先根GPU(GPU0)有整个网络的网络参数,callbacks_.size()指的是GPU树的parent的个数(在这里是1),on_start()函数的作用就是把根GPU(GPU0)的网络参数分发到每一个子GPU(GPU1),GPU1会先进入这个函数,on_start()函数的部分代码如下:

  1. void P2PSync<Dtype>::on_start() {
  2. . . . . . . .
  3. // Wait for update from parent
  4. if (parent_) {
  5. P2PSync<Dtype> *parent = queue_.pop();//取队列中的第一个gpu节点为根gpu
  6. CHECK(parent == parent_);
  7. }
  8. . . . . . .

当执行到queue_.pop()时,会调用blocking_queue.cpp的pop()方法,pop()方法的内容如下:

  1. T BlockingQueue<T>::pop(const string& log_on_wait) {
  2. boost::mutex::scoped_lock lock(sync_->mutex_);
  3. while (queue_.empty()) {
  4. if (!log_on_wait.empty()) {
  5. LOG_EVERY_N(INFO, )<< log_on_wait;
  6. }
  7. sync_->condition_.wait(lock);//如果queue_为空,就一直阻塞。
  8. }

该方法内部有wait()函数,因为此时queue_为空,所以GPU1就会被堵塞,因为GPU0和GPU1是两个线程并行运行,所以GPU0会执行run()函数中的下一步,也就是solver_->Solve(),Solve()函数的代码如下:

  1. void Solver<Dtype>::Solve(const char* resume_file) {
  2. int start_iter = iter_;
  3. . . . . .
  4. //LOG(INFO) <<"This is the sign of the train begin?********Ni****Jian*********"; //test for nijian
  5. Step(param_.max_iter() - iter_);
  6. . . . . .
  7. }

Solve()函数会调用Step()函数进入迭代过程,当GPU0进入on_start()函数后,会把队列中的GPU0出队列,同时会激活被堵塞的GPU1,接下来的on_start()函数代码如下:

  1. . . . . .
  2. // Update children
  3. for (int i = children_.size() - ; i >= ; i--) {
  4. Dtype* src = data_;
  5. Dtype* dst = children_[i]->data_;
  6. #ifdef DEBUG
  7. . . . .
  8. #endif
  9. CUDA_CHECK(cudaMemcpyAsync(dst, src, size_ * sizeof(Dtype),
  10. cudaMemcpyDeviceToDevice, cudaStreamDefault));//每个子GPU把信息传入到根GPU,异步操作
  11. CUDA_CHECK(cudaStreamSynchronize(cudaStreamDefault));//根GPU把信息同步传到各个子GPU
  12. children_[i]->queue_.push(this);
  13. }
  14. #endif
  15. }

在该部分代码中,src指的是GPU0的data(网络参数),dst指的是GPU1的data(网络参数),通过调用cudaMemcpyAsync()函数来放置一个请求,表示在cudaStreamDefault流中执行一次内存复制操作,然后调用cudaStreamSynchronize()等待cudaStreamDefault流中的操作完成后实现流的同步。经过这两个函数后,GPU0完成了把网络参数分发给GPU1,然后children_[i]->queue_.push(this)被执行后,会调用block_queue.cpp文件中的push函数激活GPU0的子GPU,即GPU1,同时把GPU1压入队列,此时队列中只有GPU1。

  1. void BlockingQueue<T>::push(const T& t) {
  2. boost::mutex::scoped_lock lock(sync_->mutex_);
  3. queue_.push(t);
  4. lock.unlock();
  5. sync_->condition_.notify_one();
  6. }

此时,多个GPU的参数分发过程已经完成,接下来GPU0和GPU1并行执行Step()函数的下一步,即:ForwardBackward(),该函数的代码如下:

  1. Dtype ForwardBackward(const vector<Blob<Dtype>* > & bottom) {
  2. Dtype loss;
  3. Forward(bottom, &loss);
  4. Backward();
  5. return loss;
  6. }

该函数的主要作用就是就是计算出loss和梯度diff,然后再接着执行Step()函数中的下一步,即:on_gradients_ready()函数,该函数分为两个部分,第一部分是多个GPU的梯度加和,第二部分是将计算后的梯度传给根GPU(GPU0)。第一部分的代码如下:

  1. void P2PSync<Dtype>::on_gradients_ready() {. . . . . . . .
  2. // Sum children gradients as they appear in the queue
  3. for (int i = ; i < children_.size(); ++i) {
  4. P2PSync<Dtype> *child = queue_.pop();
  5. Dtype* src = child->parent_grads_;
  6. Dtype* dst = diff_;
  7. #ifdef DEBUG
  8. cudaPointerAttributes attributes;
  9. CUDA_CHECK(cudaPointerGetAttributes(&attributes, src));
  10. CHECK(attributes.device == device);
  11. CUDA_CHECK(cudaPointerGetAttributes(&attributes, dst));
  12. CHECK(attributes.device == device);
  13. #endif
  14. caffe_gpu_add(size_, src, dst, dst);
  15. }

第一部分是多个GPU的梯度加和,因为GPU0和GPU1是并行计算的,如果GPU0执行到这里时,会使队列中仅有的GPU1出队列,然后通过调用caffe_gpu_add()函数,将一个GPU的梯度diff直接传给另一个GPU,不需要经过CPU通信,即GPU1把其计算的diff传给GPU0。如果是GPU1执行到这里时,因为GPU1没有子GPU,所以会直接跳过这一部分。第二部分的代码如下:

  1. if (parent_) {
  2. Dtype* src = diff_;
  3. Dtype* dst = parent_grads_;
  4. #ifdef DEBUG
  5. #endif
  6. CUDA_CHECK(cudaMemcpyAsync(dst, src, size_ * sizeof(Dtype), //
  7. cudaMemcpyDeviceToDevice, cudaStreamDefault));
  8. CUDA_CHECK(cudaStreamSynchronize(cudaStreamDefault));
  9. parent_->queue_.push(this);
  10. } else {
  11. // Loss functions divide gradients by the batch size, so to compensate
  12. // for split batch, the root solver divides by number of solvers.
  13. caffe_gpu_scal(size_, Dtype(1.0 / Caffe::solver_count()), diff_);
  14. }

如果是GPU0的话,会执行else,即caffe_gpu_scal(),该函数把得到的之前计算的梯度diff_和除以GPU的个数,来更新梯度。如果是GPU1的话,会执行if的语句,此时和on_start()函数分析类似,经过cudaMemcpyAsync()和cudaStreamSynchronize()函数操作之后,将GPU1中的梯度传送给GPU0,第二部分完成。

  接下来根GPU(GPU0)会得到所有的参数信息,会执行Step()函数的下一步,即执行ApplyUpdate()函数,该函数中有一个程序:CHECK(Caffe::root_solver()),会在根GPU中利用梯度下降法更新权重,计算参数,到此为止一次迭代完成,再进入下一次迭代时,根GPU已经保存了所有的网络参数,再继续迭代循环,直至结束。

Caffe参数交换源码分析的更多相关文章

  1. 认识 Redis client-output-buffer-limit 参数与源码分析

    概述 Redis 的 client-output-buffer-limit 可以用来强制断开无法足够快从 redis 服务器端读取数据的客户端.保护机制规则如下: [hard limit] 大小限制, ...

  2. caffe web demo运行+源码分析

    caffe web demo学习 1.运行 安装好caffe后,进入/opt/caffe/examples/web_demo/的caffe web demo项目目录,查看一下app.py文件,这是一个 ...

  3. 详解SpringMVC中Controller的方法中参数的工作原理[附带源码分析]

    目录 前言 现象 源码分析 HandlerMethodArgumentResolver与HandlerMethodReturnValueHandler接口介绍 HandlerMethodArgumen ...

  4. 性能测试分享: Jmeter的源码分析main函数参数

    性能测试分享: Jmeter的源码分析main函数参数   poptest是国内唯一一家培养测试开发工程师的培训机构,以学员能胜任自动化测试,性能测试,测试工具开发等工作为目标.如果对课程感兴趣,请大 ...

  5. springMVC源码分析--RequestParamMethodArgumentResolver参数解析器(三)

    之前两篇博客springMVC源码分析--HandlerMethodArgumentResolver参数解析器(一)和springMVC源码解析--HandlerMethodArgumentResol ...

  6. 【Spark篇】---Spark中资源和任务调度源码分析与资源配置参数应用

    一.前述 Spark中资源调度是一个非常核心的模块,尤其对于我们提交参数来说,需要具体到某些配置,所以提交配置的参数于源码一一对应,掌握此节对于Spark在任务执行过程中的资源分配会更上一层楼.由于源 ...

  7. 开源网站流量统计系统Piwik源码分析——参数统计(一)

    Piwik现已改名为Matomo,这是一套国外著名的开源网站统计系统,类似于百度统计.Google Analytics等系统.最大的区别就是可以看到其中的源码,这正合我意.因为我一直对统计的系统很好奇 ...

  8. springMVC源码分析--HttpMessageConverter参数read操作(二)

    上一篇博客springMVC源码分析--HttpMessageConverter数据转化(一)中我们简单介绍了一下HttpMessageConverter接口提供的几个方法,主要有以下几个方法: (1 ...

  9. 【MVC - 参数原理】详解SpringMVC中Controller的方法中参数的工作原理[附带源码分析]

    前言 SpringMVC是目前主流的Web MVC框架之一. 如果有同学对它不熟悉,那么请参考它的入门blog:http://www.cnblogs.com/fangjian0423/p/spring ...

随机推荐

  1. element ui中的一些小技巧

    最近写公司的项目,这项目是vue和element ui搭建的, 做的是一套电力系统的管理平台.  遇到一个小麻烦,用过element ui 的都知道,使用element ui 弹框,点击空白处,默认是 ...

  2. Windows系统下安装MySQL详细教程(命令安装法)

    1.安装包下载. 下载地址:https://dev.mysql.com/downloads/mysql/ 点击下载之后,可以选择注册Oracle账号,也可以跳过直接下载. 下载完成后,选择一个磁盘内放 ...

  3. AVCaptureSession拍照,摄像,载图总结

    AVCaptureSession [IOS开发]拍照,摄像,载图总结 1 建立Session  2 添加 input  3 添加output  4 开始捕捉 5 为用户显示当前录制状态 6 捕捉 7 ...

  4. 编程的智慧(王垠)(http://www.cocoachina.com/programmer/20151125/14410.html)

    编程是一件创造性的工作,是一门艺术.精通任何一门艺术,都需要很多的练习和领悟,所以这里提出的“智慧”,并不是号称三天瘦二十斤的减肥药,它并不能代替你自己的勤奋.然而我希望它能给迷惑中的人们指出一些正确 ...

  5. Educational Codeforces Round 68 (Rated for Div. 2) C. From S To T (字符串处理)

    C. From S To T time limit per test1 second memory limit per test256 megabytes inputstandard input ou ...

  6. L3-015. 球队“食物链”

    某国的足球联赛中有N支参赛球队,编号从1至N.联赛采用主客场双循环赛制,参赛球队两两之间在双方主场各赛一场. 联赛战罢,结果已经尘埃落定.此时,联赛主席突发奇想,希望从中找出一条包含所有球队的“食物链 ...

  7. 如何获得带转义的json内容

    stringify两次 JSON.stringify(JSON.stringify(obj))

  8. java编写算法题格式(链表和二叉树)

    (1)链表 /** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; ...

  9. 【Luogu4221】[WC2018] 州区划分

    题目链接 题目描述 略 Sol 一个州合法就是州内点形成的子图中 不存在欧拉回路(一个点也算欧拉回路). 这个东西显然就状压 dp 一下: 设 \(f[S]\) 表示当前考虑了 \(S\) 这个集合内 ...

  10. C#知识点:委托、事件、正则表达式、SVN、找按段等差递增至不变序列的规律

    using System; using System.Collections.Generic; using System.Text; namespace Delegate { //定义委托,它定义了可 ...