Caffe源码理解2:SyncedMemory CPU和GPU间的数据同步
博客:blog.shinelee.me | 博客园 | CSDN
写在前面
在Caffe源码理解1中介绍了Blob类,其中的数据成员有
shared_ptr<SyncedMemory> data_;
shared_ptr<SyncedMemory> diff_;
std::shared_ptr 是共享对象所有权的智能指针,当最后一个占有对象的shared_ptr被销毁或再赋值时,对象会被自动销毁并释放内存,见cppreference.com。而shared_ptr所指向的SyncedMemory即是本文要讲述的重点。
在Caffe中,SyncedMemory有如下两个特点:
- 屏蔽了CPU和GPU上的内存管理以及数据同步细节
- 通过惰性内存分配与同步,提高效率以及节省内存
背后是怎么实现的?希望通过这篇文章可以将以上两点讲清楚。
成员变量的含义及作用
SyncedMemory的数据成员如下:
enum SyncedHead { UNINITIALIZED, HEAD_AT_CPU, HEAD_AT_GPU, SYNCED };
void* cpu_ptr_; // CPU侧数据指针
void* gpu_ptr_; // GPU侧数据指针
size_t size_; // 数据所占用的内存大小
SyncedHead head_; // 指示再近一次数据更新发生在哪一侧,在调用另一侧数据时需要将该侧数据同步过去
bool own_cpu_data_; // 指示cpu_ptr_是否为对象内部调用CaffeMallocHost分配的CPU内存
bool cpu_malloc_use_cuda_; // 指示是否使用cudaMallocHost分配页锁定内存,系统malloc分配的是可分页内存,前者更快
bool own_gpu_data_; // 指示gpu_ptr_是否为对象内部调用cudaMalloc分配的GPU内存
int device_; // GPU设备号
cpu_ptr_和gpu_ptr_所指向的数据空间有两种来源,一种是对象内部自己分配的,一种是外部指定的,为了区分这两种情况,于是有了own_cpu_data_和own_gpu_data_,当为true时表示是对象内部自己分配的,因此需要对象自己负责释放(析构函数),如果是外部指定的,则由外部负责释放,即谁分配谁负责释放。
外部指定数据时需调用set_cpu_data和set_gpu_data,代码如下:
void SyncedMemory::set_cpu_data(void* data) {
check_device();
CHECK(data);
if (own_cpu_data_) { // 如果自己分配过内存,先释放,换外部指定数据
CaffeFreeHost(cpu_ptr_, cpu_malloc_use_cuda_);
}
cpu_ptr_ = data; // 直接指向外部数据
head_ = HEAD_AT_CPU; // 指示CPU侧更新了数据
own_cpu_data_ = false; // 指示数据来源于外部
}
void SyncedMemory::set_gpu_data(void* data) {
check_device();
#ifndef CPU_ONLY
CHECK(data);
if (own_gpu_data_) { // 如果自己分配过内存,先释放,换外部指定数据
CUDA_CHECK(cudaFree(gpu_ptr_));
}
gpu_ptr_ = data; // 直接指向外部数据
head_ = HEAD_AT_GPU; // 指示GPU侧更新了数据
own_gpu_data_ = false; // 指示数据来源于外部
#else
NO_GPU;
#endif
}
构造与析构
在SyncedMemory构造函数中,获取GPU设备(如果使用了GPU的话),注意构造时head_ = UNINITIALIZED,初始化成员变量,但并没有真正的分配内存。
// 构造
SyncedMemory::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) {
#ifndef CPU_ONLY
#ifdef DEBUG
CUDA_CHECK(cudaGetDevice(&device_));
#endif
#endif
}
// 析构
SyncedMemory::~SyncedMemory() {
check_device(); // 校验当前GPU设备以及gpu_ptr_所指向的设备是不是构造时获取的GPU设备
if (cpu_ptr_ && own_cpu_data_) { // 自己分配的空间自己负责释放
CaffeFreeHost(cpu_ptr_, cpu_malloc_use_cuda_);
}
#ifndef CPU_ONLY
if (gpu_ptr_ && own_gpu_data_) { // 自己分配的空间自己负责释放
CUDA_CHECK(cudaFree(gpu_ptr_));
}
#endif // CPU_ONLY
}
// 释放CPU内存
inline void CaffeFreeHost(void* ptr, bool use_cuda) {
#ifndef CPU_ONLY
if (use_cuda) {
CUDA_CHECK(cudaFreeHost(ptr));
return;
}
#endif
#ifdef USE_MKL
mkl_free(ptr);
#else
free(ptr);
#endif
}
但是,在析构函数中,却释放了CPU和GPU的数据指针,那么是什么时候分配的内存呢?这就要提到,Caffe官网中说的“在需要时分配内存” ,以及“在需要时同步CPU和GPU”,这样做是为了提高效率、节省内存。
Blobs conceal the computational and mental overhead of mixed CPU/GPU operation by synchronizing from the CPU host to the GPU device as needed. Memory on the host and device is allocated on demand (lazily) for efficient memory usage.
具体怎么实现的?我们接着往下看。
内存同步管理
SyncedMemory成员函数如下:
const void* cpu_data(); // to_cpu(); return (const void*)cpu_ptr_; 返回CPU const指针
void set_cpu_data(void* data);
const void* gpu_data(); // to_gpu(); return (const void*)gpu_ptr_; 返回GPU const指针
void set_gpu_data(void* data);
void* mutable_cpu_data(); // to_cpu(); head_ = HEAD_AT_CPU; return cpu_ptr_;
void* mutable_gpu_data(); // to_gpu(); head_ = HEAD_AT_GPU; return gpu_ptr_;
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);
#endif
其中,cpu_data()和gpu_data()返回const指针只读不写,mutable_cpu_data()和mutable_gpu_data()返回可写指针,它们4个在获取数据指针时均调用了to_cpu()或to_gpu(),两者内部逻辑一样,内存分配发生在第一次访问某一侧数据时分配该侧内存,如果不曾访问过则不分配内存,以此按需分配来节省内存。同时,用head_来指示最近一次数据更新发生在哪一侧,仅在调用另一侧数据时才将该侧数据同步过去,如果访问的仍是该侧,则不会发生同步,当两侧已同步都是最新时,即head_=SYNCED,访问任何一侧都不会发生数据同步。下面以to_cpu()为例,
inline void SyncedMemory::to_cpu() {
check_device();
switch (head_) {
case UNINITIALIZED: // 如果未分配过内存(构造函数后就是这个状态)
CaffeMallocHost(&cpu_ptr_, size_, &cpu_malloc_use_cuda_); // to_CPU时为CPU分配内存
caffe_memset(size_, 0, cpu_ptr_); // 数据清零
head_ = HEAD_AT_CPU; // 指示CPU更新了数据
own_cpu_data_ = true;
break;
case HEAD_AT_GPU: // 如果GPU侧更新过数据,则同步到CPU
#ifndef CPU_ONLY
if (cpu_ptr_ == NULL) { // 如果CPU侧没分配过内存,分配内存
CaffeMallocHost(&cpu_ptr_, size_, &cpu_malloc_use_cuda_);
own_cpu_data_ = true;
}
caffe_gpu_memcpy(size_, gpu_ptr_, cpu_ptr_); // 数据同步
head_ = SYNCED; // 指示CPU和GPU数据已同步一致
#else
NO_GPU;
#endif
break;
case HEAD_AT_CPU: // 如果CPU数据是最新的,不操作
case SYNCED: // 如果CPU和GPU数据都是最新的,不操作
break;
}
}
// 分配CPU内存
inline void CaffeMallocHost(void** ptr, size_t size, bool* use_cuda) {
#ifndef CPU_ONLY
if (Caffe::mode() == Caffe::GPU) {
CUDA_CHECK(cudaMallocHost(ptr, size)); // cuda malloc
*use_cuda = true;
return;
}
#endif
#ifdef USE_MKL
*ptr = mkl_malloc(size ? size:1, 64);
#else
*ptr = malloc(size);
#endif
*use_cuda = false;
CHECK(*ptr) << "host allocation of size " << size << " failed";
}
下面看一下head_状态是如何转换的,如下图所示:

若以X指代CPU或GPU,Y指代GPU或CPU,需要注意的是,如果HEAD_AT_X表明X侧为最新数据,调用mutable_Y_data()时,在to_Y()内部会将X侧数据同步至Y,会暂时将状态置为SYNCED,但退出to_Y()后最终仍会将状态置为HEAD_AT_Y,如mutable_cpu_data()代码所示,
void* SyncedMemory::mutable_cpu_data() {
check_device();
to_cpu();
head_ = HEAD_AT_CPU;
return cpu_ptr_;
}
不管之前是何种状态,只要调用了mutable_Y_data(),则head_就为HEAD_AT_Y。背后的思想是,无论当前是HEAD_AT_X还是SYNCED,只要调用了mutable_Y_data(),就意味着调用者可能会修改Y侧数据,所以认为接下来Y侧数据是最新的,因此将其置为HEAD_AT_Y。
至此,就可以理解Caffe官网上提供的何时发生内存同步的例子,以及为什么建议不修改数据时要调用const函数,不要调用mutable函数了。
// Assuming that data are on the CPU initially, and we have a blob.
const Dtype* foo;
Dtype* bar;
foo = blob.gpu_data(); // data copied cpu->gpu.
foo = blob.cpu_data(); // no data copied since both have up-to-date contents.
bar = blob.mutable_gpu_data(); // no data copied.
// ... some operations ...
bar = blob.mutable_gpu_data(); // no data copied when we are still on GPU.
foo = blob.cpu_data(); // data copied gpu->cpu, since the gpu side has modified the data
foo = blob.gpu_data(); // no data copied since both have up-to-date contents
bar = blob.mutable_cpu_data(); // still no data copied.
bar = blob.mutable_gpu_data(); // data copied cpu->gpu.
bar = blob.mutable_cpu_data(); // data copied gpu->cpu.
A rule of thumb is, always use the const call if you do not want to change the values, and never store the pointers in your own object. Every time you work on a blob, call the functions to get the pointers, as the SyncedMem will need this to figure out when to copy data.
以上。
参考
Caffe源码理解2:SyncedMemory CPU和GPU间的数据同步的更多相关文章
- Caffe源码理解1:Blob存储结构与设计
博客:blog.shinelee.me | 博客园 | CSDN Blob作用 据Caffe官方描述: A Blob is a wrapper over the actual data being p ...
- Caffe源码理解3:Layer基类与template method设计模式
目录 写在前面 template method设计模式 Layer 基类 Layer成员变量 构造与析构 SetUp成员函数 前向传播与反向传播 其他成员函数 参考 博客:blog.shinelee. ...
- caffe源码 理解链式法则
网络结构 首先我们抽象理解下一个网络结构是怎样的,如下图所示 F1,F2,F3为某种函数 input为输入数据,output为输出数据 X1,X2为为中间的层的输入输出数据 总体来说有以下关系 X1 ...
- Caffe源码-SyncedMemory类
SyncedMemory类简介 最近在阅读caffe源码,代码来自BVLC/caffe,基本是参照网络上比较推荐的 Blob-->Layer-->Net-->Solver 的顺序来分 ...
- caffe源码阅读(1)-数据流Blob
Blob是Caffe中层之间数据流通的单位,各个layer之间的数据通过Blob传递.在看Blob源码之前,先看一下CPU和GPU内存之间的数据同步类SyncedMemory:使用GPU运算时,数据要 ...
- Caffe源码-Blob类
Blob类简介 Blob是caffe中的数据传递的一个基本类,网络各层的输入输出数据以及网络层中的可学习参数(learnable parameters,如卷积层的权重和偏置参数)都是Blob类型.Bl ...
- caffe源码阅读
参考网址:https://www.cnblogs.com/louyihang-loves-baiyan/p/5149628.html 1.caffe代码层次熟悉blob,layer,net,solve ...
- Caffe源码中syncedmem文件分析
Caffe源码(caffe version:09868ac , date: 2015.08.15)中有一些重要文件,这里介绍下syncedmem文件. 1. include文件: (1).& ...
- Caffe源码中common文件分析
Caffe源码(caffe version:09868ac , date: 2015.08.15)中的一些重要头文件如caffe.hpp.blob.hpp等或者外部调用Caffe库使用时,一般都会in ...
随机推荐
- 0510JS流程语句
|--跳转语句|----break; 终止整个循环,不再进行判断|----continue; 终止本次循环,接着去判断是否执行下次循环 |-选择(判断)结构|--if 如果|----if(条件1){ ...
- 架构之微服务设计(Nginx + Upsync)
Upsync,微博开源基于Nginx容器动态流量管理方案 . Nginx 以其超高的性能与稳定性,在业界获得了广泛的使用,微博的七层就大量使用了 Nginx .结合 Nginx 的健康检查模块,以及动 ...
- JavaBean转JSON方式
- LR测试
LoadRunner种预测系统行性能负载测试工具通模拟千万用户实施并发负载及实性能监测式确认查找问题LoadRunner能够整企业架构进行测试通使用 LoadRunner企业能限度缩短测试间优化性能加 ...
- 基于Mybatis的Dao层的开发
基于Mybatis的Dao层开发 SqlSessionFactoryBuilder用于创建SqlSessionFacoty,SqlSessionFacoty一旦创建完成就不需要SqlSessionFa ...
- subline常用快捷键
一次创建5个class为main的div : div.main*5 +TAB 快速生成HTML结构: ! + TAB 使盒子内的文本水平垂直方向对齐: height:value; line-h ...
- redis与python交互
import redis #连接 r=redis.StrictRedis(host="localhost",port=6379,password="sunck" ...
- JavaScript 设计模式之----单体(单例)模式
设计模式之--单体(单例)模式 1.介绍 从本章开始,我们会逐步介绍在JavaScript里使用的各种设计模式实现,在这里我不会过多地介绍模式本身的理论,而只会关注实现.OK,正式开始. 在传统开发工 ...
- win7 telnet命令无法开启的解决方案(不是内部命令或外部命令)
如果你想在win 7上直接使用 telnet命令,却不能开启那怎么办呢?记得在Wingdows XP上telnet都是已经安装好的,直接就可用,但是Win7是没有这个功能的,都需要后来自己安装的,下面 ...
- Android 打造编译时注解解析框架 这只是一个开始
转载请标明出处:http://blog.csdn.net/lmj623565791/article/details/43452969 ,本文出自:[张鸿洋的博客] 1.概述 记得很久以前,写过几篇博客 ...