一、Blob
在之前的介绍中提到
Blob是:
- 对处理数据的一层封装,用于在Caffe中通信传递。
- 也为CPU和GPU间提供同步能力
- 数学上,是一个N维的C风格的存储数组
- 总的来说,Caffe使用Blob来交流数据,其是Caffe中标准的数组与统一的内存接口,它是多功能的,在不同的应用场景具有不同的含义,如可以是:batches of images(图像), model parameters(模型参数), and derivatives for optimization(最优化的导数)等。
Blob核心代码
/**
* @brief A wrapper around SyncedMemory holders serving as the basic
* computational unit through which Layer%s, Net%s, and Solver%s
* interact.
*
* TODO(dox): more thorough description.
*/
template <typename Dtype>
class Blob {
public:
Blob()
: data_(), diff_(), count_(0), capacity_(0) {}
/// @brief Deprecated; use <code>Blob(const vector<int>& shape)</code>.
explicit Blob(const int num, const int channels, const int height,
const int width);
explicit Blob(const vector<int>& shape);
.....
protected:
shared_ptr<SyncedMemory> data_;
shared_ptr<SyncedMemory> diff_;
shared_ptr<SyncedMemory> shape_data_;
vector<int> shape_;
int count_;
int capacity_;
DISABLE_COPY_AND_ASSIGN(Blob);
}; // class Blob
注:此处只保留了构造函数与成员变量。
说明:
- Blob在实现上是对SyncedMemory进行了一层封装。
- shape_为blob维度。
- data_为原始数据。
- diff_为梯度信息。
- count为该blob的总容量(即数据的size),函数count(x,y)(或count(x))返回某个切片[x,y]([x,end])内容量,本质上就是shape[x]shape[x+1]….*shape[y]的值.
count的代码如下
inline int count(int start_axis, int end_axis) const {
int count = 1;
for (int i = start_axis; i < end_axis; ++i) {
count *= shape(i);
}
return count;
}
Blob使用SyncedMemory类进行数据存储,数据成员 data_指向实际存储数据的内存或显存块,shape_存储了当前blob的维度信息,diff_这个保存了反向传递时候的梯度信息。在Blob中其实不是只有num,channel,height,width这种四维形式,它是一个不定维度的数据结构,将数据展开存储,而维度单独存在一个vector 类型的shape_变量中,这样每个维度都可以任意变化。
Blob中的关键函数
inline Dtype data_at(const int n, const int c, const int h,
const int w) const {
return cpu_data()[offset(n, c, h, w)];
}
inline Dtype diff_at(const int n, const int c, const int h,
const int w) const {
return cpu_diff()[offset(n, c, h, w)];
}
inline const shared_ptr<SyncedMemory>& data() const {
CHECK(data_);
return data_;
}
inline const shared_ptr<SyncedMemory>& diff() const {
CHECK(diff_);
return diff_;
}
其中cpu_data()函数会返回数据data_的指针,而offset()函数如下,返回数组的偏移位置。
inline int offset(const int n, const int c = 0, const int h = 0,
const int w = 0) const {
return ((n * channels() + c) * height() + h) * width() + w;
}
data_at这个函数可以读取的存储在此类中的数据,diff_at可以用来读取反向传回来的误差。顺便给个提示,尽量使用data_at(const vector& index)来查找数据。Reshape函数可以修改blob的存储大小,count用来返回存储数据的数量。BlobProto类负责了将Blob数据进行打包序列化到Caffe的模型中。
Blob的shape
由源代码中可以注意到Blob有个成员变量:vector<ini> shape_
其作用:
- 对于图像数据,shape可以定义为4维的数组(Num, Channels, Height, Width)或(n, k, h, w),所以Blob数据维度为nkhw,Blob是row-major保存的,因此在(n, k, h, w)位置的值物理位置为((n K + k) H + h) W + w。其中Number是数据的batch size,对于256张图片为一个training batch的ImageNet来说n = 256;Channel是特征维度,如RGB图像k = 3
- 对于全连接网络,使用2D blobs (shape (N, D)),然后调用InnerProductLayer
- 对于参数,维度根据该层的类型和配置来确定。对于有3个输入96个输出的卷积层,Filter核 11 x 11,则blob为96 x 3 x 11 x 11. 对于全连接层,1000个输出,1024个输入,则blob为1000 x 1024.
二、SyncedMemory
从Blob的定义开始看出来,Blob本质是对SyncedMemory的再封装。
核心代码如下
/**
* @brief Manages memory allocation and synchronization between the host (CPU)
* and device (GPU).
*
* TODO(dox): more thorough description.
*/
class SyncedMemory {
public:
...
const void* cpu_data();
const void* gpu_data();
void* mutable_cpu_data();
void* mutable_gpu_data();
...
private:
...
void* cpu_ptr_;
void* gpu_ptr_;
...
}; // class SyncedMemory
Blob同时保存了data_
和diff_
,其类型为SyncedMemory
的指针。
对于data_
(diff_
相同),其实际值要么存储在CPU(cpu_ptr_)
要么存储在GPU(gpu_ptr_)
,有两种方式访问CPU数据(GPU相同):
- 常量方式,
void* cpu_data()
,其不改变cpu_ptr_
指向存储区域的值。 - 可变方式,
void* mutable_cpu_data()
,其可改变cpu_ptr_
指向存储区域的值。
这与二者的实现方式有关。cpu_data()
返回的是const void*的指针,被指向的对象不能修改。
const void* SyncedMemory::cpu_data() {
to_cpu();
return (const void*)cpu_ptr_;
}
void* SyncedMemory::mutable_cpu_data() {
to_cpu();
head_ = HEAD_AT_CPU;
return cpu_ptr_;
}
三、再谈Blob
Blob是 Caffe中处理和传递实际数据的封装包,并且在CPU与GPU之间具有同步处理能力。从数学意义上说, blob是按 C风格连续存储的N维数组。
Blobs可根据 CPU主机到GPU 设备的同步需要,屏蔽CPU/GPU混和运算在计上的开销。主机和设备上的内存按需求分配(lazily),以提高内存的使用效率。
在blob中也提供了类似的函数对数据进行读取,也分为常量方式和变量方式。
const Dtype* cpu_data() const;
const Dtype* gpu_data() const;
const Dtype* cpu_diff() const;
const Dtype* gpu_diff() const;
Dtype* mutable_cpu_data();
Dtype* mutable_gpu_data();
Dtype* mutable_cpu_diff();
Dtype* mutable_gpu_diff();
之所以这么设计是因为blob使用了一个SyncedMem类来同步CPU和GPU上的数值,以隐藏同步的细节和最小化传送数据。一个经验准则是,如果不想改变数值,就一直使用常量调用,而且绝不要在自定义类中存储指针。每次操作blob时,调用相应的函数来获取它的指针,因为SyncedMem需要用这种方式来确定何时需要复制数据。
实际上,使用GPU时,Caffe中CPU代码先从磁盘中加载数据到blob,同时请求分配一个GPU设备核(device kernel)以使用GPU进行计算,再将计算好的blob数据送入下一层,这样既实现了高效运算,又忽略了底层细节。只要所有layers均有GPU实现,这种情况下所有的中间数据和梯度都会保留在GPU上。
这里有一个示例,用以确定blob何时会复制数据:
// 假定数据在CPU上进行初始化,我们有一个blob
const Dtype* foo;
Dtype* bar;
foo = blob.gpu_data(); // 数据从CPU复制到GPU
foo = blob.cpu_data(); // 没有数据复制,两者都有最新的内容
bar = blob.mutable_gpu_data(); // 没有数据复制
// ... 一些操作 ...
bar = blob.mutable_gpu_data(); // 仍在GPU,没有数据复制
foo = blob.cpu_data(); // 由于GPU修改了数值,数据从GPU复制到CPU
foo = blob.gpu_data(); //没有数据复制,两者都有最新的内容
bar = blob.mutable_cpu_data(); // 依旧没有数据复制
bar = blob.mutable_gpu_data(); //数据从CPU复制到GPU
bar = blob.mutable_cpu_data(); //数据从GPU复制到CPU