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 ...
随机推荐
- 让 Homebrew 走代理更新 + brew 管理 node 版本
0.前言 环境:MacOS 背景:整理下今天所做的配置. 1. 让 Homebrew 走代理更新 brew update 就卡住了,即使开了 shadowsocks 也不行.因为 shadowsock ...
- java 通过HttpURLConnection与servlet通信
研究了一天才搞清楚,其实挺简单的,在这里记录下,以便以后参考. 一.创建一个servlet项目 主要包括(WEB-INF)里面有classes文件夹.lib文件夹.web.xml文件. 将写好的ser ...
- bootstrap-table+x-editable入门
Bootstrap-table 快速入门bootstrap-table----我的表单不可能这么帅. Table of contents Quick start Why use it What's i ...
- linux 关于Apache默认编码错误 导致网站乱码的解决方案
Apache默认编码UTF-8在解析A网站的时候没有任何问题,当运行B网站时出现的"蝌蚪文"乱码问题 最近经常有同学在使用LAMP/WAMP时,遇到这样的编码错误问题: A网站 ...
- debain 安装nodejs
apt-get update -yapt-get install -y build-essential curl curl -sL https://deb.nodesource.com/setup_8 ...
- C++相关:动态内存和智能指针
前言 在C++中,动态内存的管理是通过运算符new和delete来完成的.但使用动态内存很容易出现问题,因为确保在正确的时间释放内存是及其困难的.有时候我们会忘记内存的的释放,这种情况下就会产生内存泄 ...
- DoxygenToolKit.vim 插件配置
如何才能既享受 Doxygen 的强大功能,同时又避免大量的重复性的注释内容? 解决思路: 让编辑器来替我们写那些格式和内容固定的部分,我们只负责写真正的有效内容. 所以,答案就是:Vim + Dox ...
- Notify和NotifyAll的区别?
Notify和NotifyAll都是用来对对象进行状态改变的方式,只是他们的作用域不太一样,从字面上就能看的出来,当对象被上锁之后,当其他的方法要去访问该对象中的数据,就需要该对象对其进行解锁,当然, ...
- Mongo 专题
什么是MongoDB ? MongoDB 是由C++语言编写的,是一个基于分布式文件存储的开源数据库系统. 在高负载的情况下,添加更多的节点,可以保证服务器性能. MongoDB 旨在为WEB应用提供 ...
- JS题目合集---新技术层出不穷,打好基础才是上策~
在IT界中公司对JavaScript开发者的要求还是比较高的,但是如果JavaScript开发者的技能和经验都达到了一定的级别,那他们还是很容易跳到优秀的公司的,当然薪水就更不是问题了.但是在面试之前 ...