caffe源码深入学习3:更底层的数据信息存取与交换代码:syncedmem.hpp和syncedmem.cpp

  还记得在上一期博客中,当我们解析Blob类相关时,遇到一些成员函数如Update(),cpu_data(),mutable_gpu_data()等等,这些函数在完成对应功能的同时,调用了更多底层的函数,这些函数与Blob中的函数同名,如cpu_data(),gpu_data(),mutable_cpu_data(),mutable_gpu_data()等,在上一篇博客中的代码注释部分我们提到,这些函数被封装在了SyncedMemory类中,那么,在这一篇博客中,笔者打算解析一下SyncedMemory类代码,此类代码被封装在syncedmem.hpp与syncedmem.cpp中。

  依照惯例,我们还是先放注释的代码片,首先是syncedmem.hpp的代码:

#ifndef CAFFE_SYNCEDMEM_HPP_
#define CAFFE_SYNCEDMEM_HPP_

#include <cstdlib>

#include "caffe/common.hpp"

namespace caffe {

// If CUDA is available and in GPU mode, host memory will be allocated pinned,
// using cudaMallocHost. It avoids dynamic pinning for transfers (DMA).
// The improvement in performance seems negligible in the single GPU case,
// but might be more significant for parallel training. Most importantly,
// it improved stability for large models on many GPUs.
inline void CaffeMallocHost(void** ptr, size_t size, bool* use_cuda) {
#ifndef CPU_ONLY
  if (Caffe::mode() == Caffe::GPU) {
    CUDA_CHECK(cudaMallocHost(ptr, size));//如果占用了gpu的空间,那么将使用cudaMallocHost函数分配空间
    *use_cuda = true;
    return;
  }
#endif
  *ptr = malloc(size);//如果只是使用cpu的空间,那么就使用malloc函数
  *use_cuda = false;
  CHECK(*ptr) << "host allocation of size " << size << " failed";
}

inline void CaffeFreeHost(void* ptr, bool use_cuda) {
#ifndef CPU_ONLY
  if (use_cuda) {
    CUDA_CHECK(cudaFreeHost(ptr));//如果占用了gpu的空间,那么将使用cudaFreeHost函数释放空间
    return;
  }
#endif
  free(ptr);//否则直接使用free函数释放空间
}


/**
 * @brief Manages memory allocation and synchronization between the host (CPU)
 *        and device (GPU).
 *
 * TODO(dox): more thorough description.
 */
class SyncedMemory {
 public:
  SyncedMemory()//构造函数,进行初始化
      : cpu_ptr_(NULL), gpu_ptr_(NULL), size_(0), head_(UNINITIALIZED),
        own_cpu_data_(false), cpu_malloc_use_cuda_(false), own_gpu_data_(false),
        gpu_device_(-1) {}
  explicit SyncedMemory(size_t size)//与上一个构造函数不同的地方是,在这里设置了size_变量
      : cpu_ptr_(NULL), gpu_ptr_(NULL), size_(size), head_(UNINITIALIZED),
        own_cpu_data_(false), cpu_malloc_use_cuda_(false), own_gpu_data_(false),
        gpu_device_(-1) {}
  ~SyncedMemory();
  const void* cpu_data();//只读方式获取cpu上面的数据
  void set_cpu_data(void* data);//用void* data设置cpu上面的数据
  const void* gpu_data();//只读方式获取gpu上面的数据
  void set_gpu_data(void* data);//用void* data设置gpu上面的数据
  void* mutable_cpu_data();//获取cpu上面的数据,并且支持改变cpu上面的数据
  void* mutable_gpu_data();//获取gpu上面的数据,并且支持改变gpu上面的数据
  enum SyncedHead { UNINITIALIZED, HEAD_AT_CPU, HEAD_AT_GPU, SYNCED };//一些指向数据的指针位置的枚举值
  SyncedHead head() { return head_; }//返回指向最新数据的指针位置的枚举值
  size_t size() { return size_; }//返回数据的大小

#ifndef CPU_ONLY
  void async_gpu_push(const cudaStream_t& stream);//将cpu上面的数据往gpu上面同步
#endif

 private:
  void to_cpu();//表示数据复制到cpu
  void to_gpu();//表示数据复制到gpu
  void* cpu_ptr_;//指向cpu的指针,通过此指针访问cpu的数据
  void* gpu_ptr_;//指向gpu的指针,通过此指针访问gpu的数据
  size_t size_;//表示数据大小
  SyncedHead head_;//指向目前最新数据块的位置
  bool own_cpu_data_;//是否有cpu数据
  bool cpu_malloc_use_cuda_;//检测cpu数据是否占用了gpu空间
  bool own_gpu_data_;//是否有gpu数据
  int gpu_device_;//查询设备编号

  DISABLE_COPY_AND_ASSIGN(SyncedMemory);
};  // class SyncedMemory

}  // namespace caffe

#endif  // CAFFE_SYNCEDMEM_HPP_

然后是syncedmem.cpp的代码:

#include "caffe/common.hpp"
#include "caffe/syncedmem.hpp"
#include "caffe/util/math_functions.hpp"

namespace caffe {

/*析构函数的实现*/
/*析构函数中,先检验cpu_ptr_和own_cpu_data_的有效性,然后调用CaffeFreeHost函数,
在调用CaffeFreeHost函数的时候使用cpu_malloc_use_cuda_这个布尔量检验cpu是否占用了
gpu的存储空间,若有则释放之*/
SyncedMemory::~SyncedMemory() {
  if (cpu_ptr_ && own_cpu_data_) {
    CaffeFreeHost(cpu_ptr_, cpu_malloc_use_cuda_);
  }

#ifndef CPU_ONLY
  if (gpu_ptr_ && own_gpu_data_) {
    int initial_device;
	//获取目前可用的设备
    cudaGetDevice(&initial_device);
    if (gpu_device_ != -1) {
      CUDA_CHECK(cudaSetDevice(gpu_device_));
    }
	//将gpu上面的数据释放
    CUDA_CHECK(cudaFree(gpu_ptr_));
    cudaSetDevice(initial_device);
  }
#endif  // CPU_ONLY
}

/*to_cpu()函数描述了将最新数据复制到cpu上面的处理*/
inline void SyncedMemory::to_cpu() {
  switch (head_) {//首先查看一下数据的位置
  case UNINITIALIZED://如果数据没有初始化,则将数据初始化
    CaffeMallocHost(&cpu_ptr_, size_, &cpu_malloc_use_cuda_);
    caffe_memset(size_, 0, cpu_ptr_);
    head_ = HEAD_AT_CPU;//将最新数据的位置放置在cpu
    own_cpu_data_ = true;//置cpu有数据
    break;
  case HEAD_AT_GPU://如果数据在gpu上面
#ifndef CPU_ONLY
    if (cpu_ptr_ == NULL) {
      CaffeMallocHost(&cpu_ptr_, size_, &cpu_malloc_use_cuda_);
      own_cpu_data_ = true;//首先在cpu上面创建空间
    }
    caffe_gpu_memcpy(size_, gpu_ptr_, cpu_ptr_);//然后将数据从gpu复制到cpu
    head_ = SYNCED;//最后置最新数据的标志为共享
#else
    NO_GPU;
#endif
    break;
  case HEAD_AT_CPU://如果最新数据被来就在cpu上面,则什么也不做
  case SYNCED://若cpu上面和gpu上面的最新数据都是一样的(共享的),也什么都不做
    break;
  }
}

/*to_cpu()函数描述了将最新数据复制到gpu上面的处理*/
inline void SyncedMemory::to_gpu() {
#ifndef CPU_ONLY
  switch (head_) {//首先查看一下数据的位置
  case UNINITIALIZED://如果数据没有初始化,则将数据初始化
    CUDA_CHECK(cudaGetDevice(&gpu_device_));//获取gpu设备信息
    CUDA_CHECK(cudaMalloc(&gpu_ptr_, size_));//在gpu上面开辟空间
    caffe_gpu_memset(size_, 0, gpu_ptr_);
    head_ = HEAD_AT_GPU;//将最新数据的位置放置在gpu
    own_gpu_data_ = true;//置gpu有数据
    break;
  case HEAD_AT_CPU://如果数据在cpu上面
    if (gpu_ptr_ == NULL) {
      CUDA_CHECK(cudaGetDevice(&gpu_device_));//获取gpu设备信息
      CUDA_CHECK(cudaMalloc(&gpu_ptr_, size_));//在gpu上面开辟空间
      own_gpu_data_ = true;//置gpu有数据
    }
    caffe_gpu_memcpy(size_, cpu_ptr_, gpu_ptr_);//然后将数据从cpu复制到gpu
    head_ = SYNCED;//置数据为共享的
    break;
  case HEAD_AT_GPU://如果最新数据本来就在gpu上面,则啥也不做
  case SYNCED:若cpu上面和gpu上面的最新数据都是一样的(共享的),也什么都不做
    break;
  }
#else
  NO_GPU;
#endif
}

const void* SyncedMemory::cpu_data() {//访问cpu上面的的数据,返回const void*类型的指针
  to_cpu();//在访问的时候调用上文的to_cpu()函数,需要同步最新数据
  return (const void*)cpu_ptr_;
}

void SyncedMemory::set_cpu_data(void* data) {//利用data指针设置cpu上面的数据
  CHECK(data);
  if (own_cpu_data_) {//如果目前cpu上面有数据,则先释放
    CaffeFreeHost(cpu_ptr_, cpu_malloc_use_cuda_);
  }
  cpu_ptr_ = data;//将指向cpu上数据的指针指向data
  head_ = HEAD_AT_CPU;//将最新数据的标志置为在cpu上
  own_cpu_data_ = false;
}

const void* SyncedMemory::gpu_data() {//返回gpu上面的数据,返回const void*类型的指针
#ifndef CPU_ONLY
  to_gpu();//在访问的时候调用上文的to_gpu()函数,需要同步最新数据
  return (const void*)gpu_ptr_;
#else
  NO_GPU;
  return NULL;
#endif
}

void SyncedMemory::set_gpu_data(void* data) {//利用data指针设置gpu上面的数据
#ifndef CPU_ONLY
  CHECK(data);
  if (own_gpu_data_) {//如果目前gpu上面有数据,则先释放
    int initial_device;
    cudaGetDevice(&initial_device);//查询gpu设备信息
    if (gpu_device_ != -1) {
      CUDA_CHECK(cudaSetDevice(gpu_device_));//设置当前所使用的设备
    }
    CUDA_CHECK(cudaFree(gpu_ptr_));//释放gpu上面的数据
    cudaSetDevice(initial_device);//再次设置当前所使用的设备
  }
  gpu_ptr_ = data;//将指向gpu上数据的指针指向data
  head_ = HEAD_AT_GPU;//将最新数据的标志置为在gpu上
  own_gpu_data_ = false;
#else
  NO_GPU;
#endif
}

void* SyncedMemory::mutable_cpu_data() {//支持可改变数据的方式访问cpu上面的数据
  to_cpu();//在访问的时候调用上文的to_cpu()函数,需要同步最新数据
  /*在访问完毕cpu数据之后,将最新数据的指针置在了cpu,意味着若
  要在gpu上面改变数据的时候,需要先同步一下cpu的数据*/
  head_ = HEAD_AT_CPU;
  return cpu_ptr_;
}

void* SyncedMemory::mutable_gpu_data() {//支持可改变数据的方式访问gpu上面的数据
#ifndef CPU_ONLY
  to_gpu();//在访问的时候调用上文的to_gpu()函数,需要同步最新数据
  /*在访问完毕gpu数据之后,将最新数据的指针置在了gpu,意味着若
  要在cpu上面改变数据的时候,需要先同步一下gpu的数据*/
  head_ = HEAD_AT_GPU;
  return gpu_ptr_;
#else
  NO_GPU;
  return NULL;
#endif
}

#ifndef CPU_ONLY
//以流的方式将数据同步到gpu
void SyncedMemory::async_gpu_push(const cudaStream_t& stream) {
  CHECK(head_ == HEAD_AT_CPU);//检验最新数据位于cpu上面
  if (gpu_ptr_ == NULL) {//在gpu上面分配数据的存储空间
    CUDA_CHECK(cudaGetDevice(&gpu_device_));
    CUDA_CHECK(cudaMalloc(&gpu_ptr_, size_));
    own_gpu_data_ = true;
  }
  const cudaMemcpyKind put = cudaMemcpyHostToDevice;
  CUDA_CHECK(cudaMemcpyAsync(gpu_ptr_, cpu_ptr_, size_, put, stream));//将cpu上面的数据同步至gpu
  // Assume caller will synchronize on the stream before use
  head_ = SYNCED;//将最新数据的指针置为共享
}
#endif

}  // namespace caffe


  以上是syncedmem.hpp与syncedmem.cpp的注释代码,通读完SyncedMemory类的代码,第一感觉是caffe对于cpu和gpu的数据同步机制是非常看重的,并且对于数据的更新操作相对谨慎,同时结构清晰简单,有效地管理了cpu与gpu上的数据。

  下面先来解析一下SyncedMemory类的数据成员:

  首先笔者认为最重要的是head_参数

SyncedHead head_;
这个参数标志了目前最新的数据的位置,或者说到目前为止最后更新过的数据的位置,这个参数的取值类型集合为下面的枚举类型:

enum SyncedHead { UNINITIALIZED, HEAD_AT_CPU, HEAD_AT_GPU, SYNCED };UNINITIALIZED表示数据未初始化,HEAD_AT_CPU表示最新数据位于cpu上面,HEAD_AT_GPU表示最新数据位于gpu上面,SYNCED表示最新数据是cpu与gpu共享的(没有特殊指明位于cpu还是gpu),这个head_参数在cpu与gpu数据同步时起到了重要作用。

  然后是size_参数

size_t size_;
size_参数表示了初始化时在cpu/gpu上开辟空间的大小。

  再接着是两个指针

void* cpu_ptr_;
void* gpu_ptr_;
这两个指针一个指向cpu上面的数据,一个指向gpu上面的数据。

  最后是一些bool值和设备编号

bool own_cpu_data_;
bool cpu_malloc_use_cuda_;
bool own_gpu_data_;
int gpu_device_;
own_cpu_data和own_gpu_data为真表示cpu上面和gpu上面目前有数据存在,这两个bool值在手动置cpu和gpu值的时候会帮助程序判断是否会先清除目前的数据,cpu_malloc_use_cuda标志cpu是否占用gpu的存储空间,而gpu_device_标志gpu的设备编号。


  下面再来看一看SyncedMemory类的成员函数:

  首先是构造函数与析构函数

SyncedMemory()
      : cpu_ptr_(NULL), gpu_ptr_(NULL), size_(0), head_(UNINITIALIZED),
        own_cpu_data_(false), cpu_malloc_use_cuda_(false), own_gpu_data_(false),
        gpu_device_(-1) {}
explicit SyncedMemory(size_t size)
      : cpu_ptr_(NULL), gpu_ptr_(NULL), size_(size), head_(UNINITIALIZED),
        own_cpu_data_(false), cpu_malloc_use_cuda_(false), own_gpu_data_(false),
        gpu_device_(-1) {}
~SyncedMemory();
构造函数与析构函数的作用是对数据占用空间以及各参数的初始化,析构函数的作用主要是释放数据空间,在这里不详细展开。

  然后是几个SyncedMemory中最关键的几个控制cpu与gpu之间数据同步与交换的函数

const void* cpu_data();
void set_cpu_data(void* data);
const void* gpu_data();
void set_gpu_data(void* data);
void* mutable_cpu_data();
void* mutable_gpu_data();
void to_cpu();
void to_gpu();
cpu_data()函数是一个提供只读功能的,该函数的核心是to_cpu()函数,首先我们先说一下to_cpu函数,这个函数的作用是检查最新数据的位置,如果数据未初始化则初始化并将最新数据标志置为位于cpu,若最新数据在gpu上面则将数据复制到cpu,并将数据标志置为共享,若最新数据已经位于cpu上面或者是共享的,那么什么都不做。而mutable_cpu_data()与cpu_data()的区别是在做了to_cpu()这个工作之后,强行将最新数据的位置置为位于cpu,这个方法的好处是在调用mutable_gpu_data()函数,gpu_data()函数的时候,需要先进行数据的同步。

gpu_data()函数功能与cpu_data()函数相似,mutable_gpu_data()函数功能与mutable_cpu_data()功能相似。

set_cpu_data(void* data)函数与set_gpu_data(void* data)函数都是重置cpu/gpu上面的数据。

  还有一个以流的形式将数据同步到gpu上面的函数

void SyncedMemory::async_gpu_push(const cudaStream_t& stream)

这个函数在gpu上面分配数据存储的空间,并将cup的数据同步至gpu。

  最后,在类体外面还定义了两个函数

inline void CaffeMallocHost(void** ptr, size_t size, bool* use_cuda)
inline void CaffeFreeHost(void* ptr, bool use_cuda)

CaffeMallocHost函数调用cudaMallocHost函数,cudaMallocHost的作用是在初始化cpu上面的数据时,若允许占用gpu的空间,则将数据放入cpu上面的锁页区,进而将数据传送至gpu,锁页区的数据可以复制到gpu上面,而非锁页区的数据只能同外存的数据进行交换。而CaffeFreeHost数据则调用cudaFreeHost释放上述空间。

  到此为止,syncedmem.hpp和syncedmem.cpp的代码解析完毕。

  最后谈一下笔者对于caffe底层数据相关代码阅读的感受,首先是代码简洁工整,绝不冗杂,用效率最高的代码协调了网络底层的数据规范,同时,代码封装层次清楚严谨,从最上层的Blob类到协调gpu与cpu数据更新交换的SyncedMemory类,到更下层的cuda接口,封装层级清晰,给读者以小说一般的阅读感受。

  总的一句话来说:解析源码是提升代码能力的阶梯。

  在解析完毕caffe的底层数据存储交换代码之后,解析更上等的Layer,Net会更得心应手。

  在文章的末尾放一个师兄的博客链接,他的博客在笔者解析caffe代码时提供了很大的帮助,一亩半分地的博客频道:点击打开链接

  欢迎阅读笔者后续解析caffe源码的博客,各位读者朋友的支持与鼓励是我最大的动力!


written by jiong

追梦其实是最大的冒险

  • 6
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值