对NVCaffe数据读取与处理部分的解读并不像直接解读某个 layer 一样结构清晰,需要拨开几层迷雾才能见到真相。我对数据部分的了解直接从解读 AnnotatedDataLayer这一 层的源代码开始,并以下面这份参数配置作为参考进行推理。
layer {
name: "data"
type: "AnnotatedData"
top: "data"
top: "label"
include {
phase: TRAIN
}
transform_param {
scale: 0.00392157
mirror: true
mean_value: 0
mean_value: 0
mean_value: 0
resize_param {
prob: 1
resize_mode: WARP
height: 160
width: 160
interp_mode: LINEAR
interp_mode: AREA
interp_mode: NEAREST
interp_mode: CUBIC
interp_mode: LANCZOS4
height_scale: 160
width_scale: 160
}
emit_constraint {
emit_type: CENTER
}
distort_param {
brightness_prob: 0.5
brightness_delta: 32
contrast_prob: 0.5
contrast_lower: 0.5
contrast_upper: 1.5
hue_prob: 0.5
hue_delta: 18
saturation_prob: 0.5
saturation_lower: 0.5
saturation_upper: 1.5
random_order_prob: 0
}
expand_param {
prob: 0.8
max_expand_ratio: 4
}
rotate_param {
prob: 0.5
rotate_angle: 5
}
}
data_param {
source: "../data/train.lmdb"
batch_size: 64
backend: LMDB
threads: 4
parser_threads: 4
}
annotated_data_param {
batch_sampler {
max_sample: 1
max_trials: 1
}
batch_sampler {
sampler {
min_scale: 0.3
max_scale: 1
min_aspect_ratio: 0.5
max_aspect_ratio: 2
}
sample_constraint {
min_jaccard_overlap: 0.1
}
max_sample: 1
max_trials: 50
}
batch_sampler {
sampler {
min_scale: 0.3
max_scale: 1
min_aspect_ratio: 0.5
max_aspect_ratio: 2
}
sample_constraint {
min_jaccard_overlap: 0.3
}
max_sample: 1
max_trials: 50
}
/***省略若干****/
label_map_file: "../data/label_map_file.prototxt"
}
}
类组织结构
从构造函数开始
就这两天读代码的经验来看,从构造函数切入会不容易懵逼,因为这玩意里面做了太多的铺垫。c++构造函数的顺序是先基类再父类,看了上面的类结构图我的建议是从 BasePrefetchingDataLayer开始即可,不用再深。
template<typename Ftype, typename Btype>
BasePrefetchingDataLayer<Ftype, Btype>::BasePrefetchingDataLayer(const LayerParameter& param,
size_t solver_rank)
: BaseDataLayer<Ftype, Btype>(param, threads(param)),
InternalThread(Caffe::current_device(), solver_rank, threads(param), false),
auto_mode_(Caffe::mode() == Caffe::GPU && this->phase_ == TRAIN && auto_mode(param)), //设置auto_mode
parsers_num_(parser_threads(param)),
transf_num_(threads(param)), //哟,transf_num_和 threads_num_一样
queues_num_(transf_num_ * parsers_num_), //对列的长度为什么这样设置?
batch_transformer_(make_shared<BatchTransformer<Ftype, Btype>>(Caffe::current_device(),
solver_rank, queues_num_, param.transform_param(), is_gpu_transform())),
iter0_(true) {
CHECK_EQ(transf_num_, threads_num());
batch_size_ = param.data_param().batch_size();
// We begin with minimum required
ResizeQueues();
}
好戏开场,构造函数就两个参数:param 和 solver_rank。param 就是解析 prototxt 时读到的 AnnotatedDataLayer 的相关参数,data_param 、annotated_data_param、transform_param等 ,都具化到了 LayerParameter 这个结构中。solver_rank 就是 solver 的序号,每张卡都有一个对应绑定的 solver,像我平常训练就是4张卡,也就是有4个 solver,solver_rank 依次就是0,1,2,3了。这些预备知识是要有的。 构造函数中参数的初始化顺序并不是按照你在构造函数中写的这个顺序依次初始化的,但是为了方便我们就按这个顺序解读的。首先是基类的构造函数:BaseDataLayer(Ftype,Btype)(param,threads(param))。
template<typename Ftype, typename Btype>
BaseDataLayer<Ftype, Btype>::BaseDataLayer(const LayerParameter& param, size_t transf_num)
: QuantizedLayer<Ftype, Btype>(param), transform_param_(param.transform_param()) {}
没啥可看的,我们还是来捎带看一下 threads(param)这个函数干了什么。
template<typename Ftype, typename Btype>
size_t BasePrefetchingDataLayer<Ftype, Btype>::threads(const LayerParameter& param) {
if (param.type().compare("ImageData") == 0 && param.has_image_data_param()) {
return param.image_data_param().threads();
}
// Check user's override in prototxt file
size_t threads = param.data_param().threads(); //通常会设一个线程数量
if (!auto_mode(param) && threads == 0U) {
threads = 1U; // input error fix
}
// 1 thread for test net
return (auto_mode(param) || param.phase() == TEST || threads == 0U) ? 1U : threads;
}
它是为了确定一个线程数量,至于是什么样的线程,干什么用的,在后面会讲到。这里面有个叫 auto_mode 的东东,顾名思意就是自动化模式,那什么情况下才是自动化模式呢,看一看代码就知道了:
template<typename Ftype, typename Btype>
bool BasePrefetchingDataLayer<Ftype, Btype>::auto_mode(const LayerParameter& param) {
// Both should be set to positive for manual mode
const DataParameter& dparam = param.data_param();
bool auto_mode = !dparam.has_threads() && !dparam.has_parser_threads();
return auto_mode;
}
也就是在 data_param 参数设置中没有指定threads 和 parser_threads 则auto_mode为 true,否则为 false。注意到我在 data_param 中明确给出了 threads:4,parser_threads: 4,所以对于训练而言threads(...)这个函数就是返回的4,而测试阶段返回的就是1。现在回过头来继续看 BasePrefetchingDataLayer 构造函数初始化参数中的第二项:InternalThread(Caffe::current_device(), solver_rank, threads(param), false),这是它其中一个基类InternalThread的构造函数,主要是初始化了一些成员变量。NVCaffe 内部需要用到线程的地方都跟 InternalThread 有关,因为它本身就是对于 boost:thread 的封装。
InternalThread::InternalThread(int target_device, size_t rank, size_t threads, bool delayed)
: target_device_(target_device), //0
rank_(rank), //0,1,2,3...
aux_(nullptr),
threads_(threads), //1
delay_flags_(threads, make_shared<Flag>(!delayed)) {}
然后是给成员变量 auto_mode_初始值,为 true 的条件还是比较苛刻的。然后是给 parse_num_成员变量初始值,按照我目前的设置又是4。transf_num_这个直接由 threads(...)函数给出了。queues_num_等于16(4*4),至于为什么是 transf_num_ * parsers_num_暂且还不清楚。然后是 batch_transformer_的初始化,这是NVCaffe 数据系统中的一个核心组件之一,它绝对是起着承上启下的作用,用下面这图来说明。
template<typename Ftype, typename Btype>
BatchTransformer<Ftype, Btype>::BatchTransformer(int target_device, size_t rank_,
size_t queues_num, const TransformationParameter& transform_param, bool gpu_transform) :
InternalThread(target_device, rank_, 1, false), //这里的线程数量始终是1
queues_num_(queues_num),
next_batch_queue_(0UL),
transform_param_(transform_param),
gpu_transform_(gpu_transform) {
shared_ptr<Batch> processed = make_shared<Batch>(tp<Ftype>(), tp<Ftype>());
processed_free_.push(processed);
resize(false);
StartInternalThread();
}
BatchTransformer 直接派生自 InternalThread,重写了线程函数,在整个训练过程中会启动惟一的一个线程。
template<typename Ftype, typename Btype>
void BatchTransformer<Ftype, Btype>::InternalThreadEntry() {
try {
while (!must_stop(0)) {
//从预取队列里面取数据
//BasePrefetchingDataLayer<Ftype, Btype>::InternalThreadEntryN
shared_ptr<Batch> batch =
prefetches_full_[next_batch_queue_]->pop("Data layer prefetch queue empty");
//从 processed_free_ 队列取一个空的容器
boost::shared_ptr<Batch> top = processed_free_.pop();
if (batch->data_->is_data_on_gpu() && top->data_->shape() == batch->data_->shape() &&
batch->data_packing() == this->transform_param_.forward_packing()) {
top->data_->Swap(*batch->data_);
} else {
if (batch->data_->is_data_on_gpu()) { //数据在 gpu 上面??
//从 gpu 上 copy 数据
top->data_->CopyDataFrom(*batch->data_, true,
batch->data_packing(), transform_param_.forward_packing(), Caffe::GPU_TRANSF_GROUP);
} else {
if (tmp_.shape() != batch->data_->shape()) {
tmp_.Reshape(batch->data_->shape());
}
if (top->data_->shape() != batch->data_->shape()) {
top->data_->Reshape(batch->data_->shape());
}
tmp_.set_cpu_data(batch->data_->template mutable_cpu_data<Btype>());
top->data_->CopyDataFrom(tmp_, false,
batch->data_packing(), transform_param_.forward_packing(), Caffe::GPU_TRANSF_GROUP);
}
}
top->label_->Swap(*batch->label_);
//空容器里面装填好数据后插入 processed_full_队列
processed_full_.push(top);
batch->set_id((size_t) -1L);
prefetches_free_[next_batch_queue_]->push(batch);
next_batch_queue(); //增加next_batch_queue 索引值
}
}catch (boost::thread_interrupted&) {
}
}
对于双阻塞队列我通常采用橱柜+碗来打比喻,有两个橱柜,一个叫 full,一个叫 free,另外还有一个剩菜用的碗。首先是一个空碗,放在 free 橱柜里面。厨师从 free 橱柜里面取出一个空碗,将炒好的菜放入碗中,然后将乘了菜的碗放入到 full 橱柜里面。我就是吃菜的那个人,我从 full 橱柜里面取出乘了菜的碗大口大口的吃。因为整个过程中只有一个碗,厨师假如在我还没吃完的时候就去从free 橱柜里面取碗肯定是取不到的,他得等待。我一旦吃完就会把空碗放回 free 橱柜。另外,如果厨师还没有把菜做好的话, 我去从 full 橱柜里面取碗也是取不到的,也得等。双阻塞队列就是这么个做菜吃饭的道道,它的英文缩写就叫 BBQ,这不就是所谓的"烧烤"么,实在很贴切。关于BatchTransformer,它和上下游打交道的媒介就是这个 BBQ,类内部分别定义了4个成员变量。
typedef BlockingQueue<boost::shared_ptr<Batch>> BBQ;
std::vector<boost::shared_ptr<BBQ>> prefetches_full_;
std::vector<boost::shared_ptr<BBQ>> prefetches_free_;
BBQ processed_full_;
BBQ processed_free_;
注意 prefetches_full_和 prefetchs_free_是一个 BBQ 指针向量,说明可能有很多组 BBQ。