本文是作者原创,如有理解错误,恳请大家指出,如需引用,请注明出处。
Blob内存管理分析
在caffe的分层结构中,Blob充当了内存管理的角色,屏蔽了上层逻辑代码对于数据的申请释放的感知,同时也屏蔽了底层设备对上层逻辑的影响,本文主要分析Blob的管理机制和实际内存申请单元SyncedMemory 的机制。
首先我们看一下Blob和SyncedMemory的关系,类图如下:
实际上整个Blob的实现就是在SyncedMemory上封装了一层,所以首先需要分析一下SyncedMemory的实现机制。
SyncedMemory的实现机制
SyncedMemory的目的是为了屏蔽上层代码对不同硬件设备的内存分配的感知,同时隐藏了CPU和GPU之间的同步过程。同时,SyncedMemory实现时,采用的是 “lazy”的模式,就是内存的实际申请时机是在第一次使用时进行的。有了大体的了解,下面我们来详细分析一下。
下面是SyncedMemory 提供的一组接口,
名称 | 功能 |
---|---|
cpu_data() | 获取CPU数据指针 |
gpu_data() | 获取GPU数据指针 |
实现的代码如下:
const void* SyncedMemory::cpu_data() {
to_cpu();
return (const void*)cpu_ptr_;
}
const void* SyncedMemory::gpu_data() {
#ifdef USE_CUDA
to_gpu();
return (const void*)gpu_ptr_;
#else
NO_GPU;
return NULL;
#endif // USE_CUDA
}
可以看出,每次调用接口时,都会有 to_cpu() 和 to_gpu() 的操作,那么这两个操作是什么作用呢,我们先看下SyncedMemory中的一些关键参数:
名称 | 功能 |
---|---|
cpu_ptr_ | cpu数据指针 |
gpu_ptr_ | gpu数据指针 |
size_ | 当前SyncedMemory需要维护的数据个数 |
head_ | 当前 SyncedMemory处于的状态 |
前三个都比较好理解,最后一个比较特殊,它维护的是 SyncedMemory 当前的状态,分为 UNINITIALIZED,HEAD_AT_GPU,HEAD_AT_CPU ,SYNCED 四中状态。现在介绍一下具体的流程,当第一次调用 to_cpu()时, head_处于UNINITIALIZED状态,那么系统会调用 CPU的申请内存的方式去获得内存区域,之后设置 head_ = HEAD_AT_CPU
,如果中间过程没有GPU设备则不会有状态变动,如果中间有代码调用了 to_gpu() ,则会发现 head_处于 HEAD_AT_CPU 状态,此时会调用同步函数,将数据从CPU同步到GPU, 之后如果又回到CPU上,则同样会发现 head_ 处于HEAD_AT_GPU的状态,那么又会调用相应的同步代码,将数据同步回CPU,通过 head_这样一个状态参数屏蔽了GPU和CPU间的申请和切换的不同。
所以上层业务只需要知道当前自己需要的是CPU还是GPU的数据,然后调用不同的接口,就可以完成数据获取的操作。
Blob的实现分析
了解了SyncedMemory的实现,再来看Blob 就较为简单了,它仅仅做了一些上层的管理逻辑,向外界提供了几个关键的接口:
名称 | 功能 |
---|---|
cpu_data() | 获取CPU数据指针,不能改变数据内容 |
mutable_cpu_data() | 获取CPU数据指针,可以改变数据内容 |
gpu_data() | 获取GPU数据指针,不能改变数据内容 |
mutable_gpu_data() | 获取GPU数据指针,可以改变数据内容 |
Reshape() | 调整数据的维度信息 |
前四个就是对 SyncedMemory 的 cpu_data() 和 gpu_data()的封装,只需要确保每次获取数据前都调用相对的 to_cpu 或者 to_gpu就可以了。对于最后一个Reshape函数,主要是为了调整维度信息,同时可能是出于适配多种数据格式的目的,所以提供3个重载函数,如下:
void Blob::Reshape(const int num, const int channels,const int height, const int width);
void Blob::Reshape(const BlobShape& shape) ;
void Blob::Reshape(const vector<int>& shape);
前两个重载函数仅仅进行了数据格式的转换,然后调用第三个函数,所以 void Blob::Reshape(const vector<int>& shape);
才是实际的执行者,这里需要介绍一下Blob里面较为关键的几个参数:
名称 | 功能 |
---|---|
data_ | 数据的实际存储位置 |
shape_data_ | 数据的维度信息存储位置(NCHW) |
capacity_ | 当前数据块的大小 |
count_ | reshape后的数据块的大小 |
阅读代码不难发现,cout_中所存储的就是所有维度的的乘积,也就是当前要reshape到的数据大小,整个的reshape 过程如下:
结束
以上就是我对Blob的一些理解,希望对大家有帮助。