从零开始山寨Caffe·拾贰:IO系统(四)

消费者

回忆:生产者提供产品的接口

在第捌章,IO系统(二)中,生产者DataReader提供了外部消费接口:

class DataReader
{
public:
    .........
    BlockingQueue<Datum*>& free() const  { return ptr_pair->free; }
    BlockingQueue<Datum*>& full() const  { return ptr_pair->full; }
    .........
};

生产者DataReader本身继承了线程DragonThread,在其异步的线程工作函数中interfaceKernel()中,

不断地从pair的Free阻塞队列取出空Datum,在read_one()用KV数据库内容填充,再塞到Full队列中,如图:

作为消费者(DataLayer),在从Datum获得数据后,立即做一份Copy,再把Datum塞回到Free队列中,继续生产。

整个过程就好像是一个工厂生产的循环链,Datum就好比一个包装盒。

生产者将产品放置其中,传递包装盒给消费者。消费者从中取出产品,让生产者回收包装盒。

回忆: 变形者加工产品接口

在第拾章,IO系统(三)中,变形者DataTransformer提供了数据变形的基接口:

void DataTransformer<Dtype>::transform(const Datum& datum, Dtype* shadow_data)

仔细观察一下transform的两个参数,你会发现整个transform过程,就是将Datum数据Copy到shadow_data的数组里。

这就是上节提到的“Copy”过程——从包装盒中取出产品,再变形加工。

加工放置的数组,之所以叫shadow_data,是因为它映射的是一个Blob的局部内存。

回忆一下Blob的shape,[batch_size,channels,height,width],便可知,一个Datum仅仅是一个Blob的1/batch_size。

让Transformer对映射的内存处理,避免了直接对Datum变形的不便。映射的内存空间,就是最终成品的实际空间,如图:

二级封装:从Datum到Blob

在上图中,Transformer提供了一个由Datum堆砌成Blob的途径。

我们只需要给Transformer提供Datum元素,以及一段内存空间(数组首指针)即可。

为了保证内存空间提供的正确性,有两点需要保障:

①每个Datum在Blob的偏移位置必须计算出来,第玖章BlobFlow给了一点偏移的思路,

只要偏移offset=Blob.offset(i)即可,i 为一个Batch内的样本数据下标。

②内存空间,也就是Blob具体的shape必须提前计算出来,而且必须启动SyncedMemory自动机,分配实际内存。

 

考虑一个Blob的shape,[batch_size,channels,height,width],后三个shape都可以由Datum推断出来。

至于batch_size,是一个由使用者提供的超参,可以根据网络定义直接获取。

由Datum推理channe/height/width,由DataTransformer的inferBlobShape完成,在第拾章IO系统(三)已经给出。

二级生产者

第捌章IO系统(二)介绍了LayerParameter中的prefetch概念。

在构造一个DataReader时,指定了默认Pair的缓冲区大小:

DataReader::DataReader(const LayerParameter& param){
    ptr_pair.reset(new QueuePair(
        param.data_param().prefech()*param.data_param().batch_size()));
    ........
}

total_size=prefetch*batch_size

这个大小表明了DataReader需要预缓冲prefetch个Batch,每个Batch有batch_size个Datum单元。

在 单生产者单缓冲区 一节的最后,讨论了多GPU下,如何使用单Pair的补救措施:

这可能是Caffe源码的本意。在这种方案中,DataReader和DataLayer是无须改动代码的。

只要我们加大DataParameter里的prefech数值,让CPU多缓冲几个Batch,为多个GPU准备就好了。

prefetch的数量由用户指定,而且也是一个上界,而且显然不能全部将整个KV数据库prefetch完。

于是,以Batch为单位的二级封装,需要一个二级生产者和消费者,而且同样是异步的,如IO系统(二)的图:

二级生产者,在Caffe里就是DataLayer衍生的线程。二级消费者,恰恰就是DataLayer本身。

DataLayer集二级生产者与消费者于一体,这归功于面向对象技术的多重继承。

类继承体系

最终使用的是DataLayer,被拆解成3个类BaseDataLayer、BasePrefetchingDataLayer、DataLayer。

三个类负责不同的任务,你也可以整合在一起写。

构造函数执行顺序与二级生产者预缓冲流程

二级C++最喜欢考继承类的执行顺序,当然,这里搞清楚这点至关重要。

除了基本的类构造函数外,我们还需要考虑Layer类setup的具体函数layerSetup。

几个DataLayer的layerSetup相当混乱,几乎每个都各司其职,①②③④顺序不能颠倒。

完成全部setup之后,才能让二级生产者工作。

生产单位以一个Batch为单元,每个Batch包含DataBlob和LabelBlob(可选)。

二级生产缓冲区

二级缓冲区构建于BasePrefetchingDataLayer类中。

template<typename Dtype>
class BasePrefetchingDataLayer :public BaseDataLayer<Dtype>,public DragonThread {
public:
    .......
    const int PREFETCH_COUNT;
protected:
    .......
    Batch<Dtype>* prefetch;
    BlockingQueue<Batch<Dtype>*> free;
    BlockingQueue<Batch<Dtype>*> full;
};

产能上界由常数PREFETCH_COUNT指定,来源于proto参数DataParamter里prefetch大小。

在BasePrefetchingDataLayer构造函数中,用new申请等量的堆内存prefetch。

注意这里不要使用shared_ptr,比较麻烦,而且Batch有可能会被智能指针提前释放,应当手动析构。

可以看到,默认提供了和DataReader类似的消费者接口free/full,不过这消费的是Batch,而不是Datum。

没有用函数封装,是因为DataLayer自己生产,自己消费。

二级生产

同DataReader的一级生产类似,二级生产需要从free队列pop,填充,再塞入full。

生产过程于BasePrefetchingDataLayer的interfaceKernel函数中。

由于多重继承的关系,interfaceKernel函数来自父类DragonThread

template<typename Dtype>
void BasePrefetchingDataLayer<Dtype>::interfaceKernel(){
    try{
        while (!must_stop()){
            Batch<Dtype> *batch = free.pop(); //batch has already reshape in dataLayerSetup
            loadBatch(batch); // pure abstract function
            full.push(batch); //product
        }
    }
    catch (boost::thread_interrupted&) {}
}

loadBatch函数与DataReader的read_one函数效果类似,负责填充batch。

二级生产与异步流同步

第贰章主存模型末尾介绍了SyncedMemory异步提交显存Memcpy的方法。

第玖章BlobFlow中,且已知SyncedMemory隶属于Blob的成员变量。

当数据缓冲至Blob级别时,就需要考虑提前向显存复制数据了。

第陆章IO系统(一)开头给了这张图:

可以看到,DataLayer处于CPU与GPU的分界点,DataLayer源输入由CPU主控,存于内存。

而DataLayer的下一层是计算层,源输入必须存于显存。

于是,尽管DataLayer的前向传播函数forward(bottom,top)只是复制数据,但是更重要的是转换数据。

在上一节的CPU异步线程工作函数interfaceKernel中,我们可以看到,Batch(Blob)级别已经构成,

而此时整个神经网络Net可能正在初始化,距离Net正式启动前向传播函数Net.forward(),需要显存数据,还有一段时间。

利用这段时间,可以利用CUDA的异步流预先由内存向显存转换数据,据此,完善interfaceKernel函数:

template<typename Dtype>
void BasePrefetchingDataLayer<Dtype>::interfaceKernel(){
    //    create GPU async stream
    //    speed up memcpy between CPU and GPU
    //    because cudaMemcpy will be called frequently 
    //    rather than malloc gpu memory firstly(just call cudaMemcpy)
#ifndef CPU_ONLY
    cudaStream_t stream;
    if (Dragon::get_mode() == Dragon::GPU)
        CUDA_CHECK(cudaStreamCreateWithFlags(&stream, cudaStreamNonBlocking));
#endif
    try{
        while (!must_stop()){
            Batch<Dtype> *batch = free.pop(); //batch has already reshape in dataLayerSetup
            loadBatch(batch); // pure abstract function
#ifndef CPU_ONLY
            if (Dragon::get_mode() == Dragon::GPU){
                batch->data.data()->async_gpu_data(stream);
                // blocking this thread until host->device memcpy finished
                CUDA_CHECK(cudaStreamSynchronize(stream));
            }
#endif
            full.push(batch); //product
        }
    }
    catch (boost::thread_interrupted&) {}
    //    destroy async stream
#ifndef CPU_ONLY
    if (Dragon::get_mode() == Dragon::GPU) CUDA_CHECK(cudaStreamDestroy(stream));
#endif
}

使用异步流,需要用cudaStreamCreateWithFlags申请Flag为cudaStreamNonBlocking的流。

cudaStreamNonBlocking的值为0x1,代表此流非默认Memcpy流(默认流)

与之相对的是Flag为cudaStreamDefault的流,值为0x0,这是主复制流,cudaMemcpy的任务全部提交于此。

使用Blob内提交异步流的函数async_gpu_data(stream)[稍后给出]后,需要立即阻塞(同步)该CPU线程。

使用cudaStreamSynchronize(stream),直到GPU返回复制完毕信号之前,CPU一直同步在本行代码。

最后,需要释放异步流。

二级消费者

即DataLayer的forward函数。

由于大量工作已经在父类中做完,DataLayer的消费函数相对简单。

template <typename Dtype>
void DataLayer<Dtype>::forward_cpu(const vector<Blob<Dtype>*>& bottom, const vector<Blob<Dtype>*>& top){
    // consume
    Batch<Dtype> *batch = full.pop("DataLayer prefectching queue is now empty");
    dragon_copy<Dtype>(batch->data.count(), top[0]->mutable_cpu_data(), batch->data.cpu_data());
    if (has_labels)
        dragon_copy(batch->label.count(), top[1]->mutable_cpu_data(), batch->label.cpu_data());
    free.push(batch);
}

直接访问full队列获取一个可用的Batch,完成消费。

将batch数据(data/label)分别复制到top里,完成Blob的Flow,提供给下一层计算。

template <typename Dtype>
void DataLayer<Dtype>::forward_gpu(const vector<Blob<Dtype>*>& bottom, const vector<Blob<Dtype>*>& top){
    Batch<Dtype> *batch = full.pop("DataLayer prefectching queue is now empty");
    dragon_gpu_copy(batch->data.count(), top[0]->mutable_gpu_data(), batch->data.gpu_data());
    if (has_labels)
        dragon_gpu_copy(batch->label.count(), top[1]->mutable_gpu_data(), batch->label.gpu_data());
    free.push(batch);
}

 GPU版本,直接替换copy函数为GPU版本即可。

(注:Caffe在forward_gpu()最后,对默认流的强制同步是没有必要的。

     Memcpy也本身不是异步执行,不需要额外同步。对默认流同步,也不会影响异步流)

 

posted @ 2016-03-26 13:13  Physcal  阅读(6534)  评论(10编辑  收藏  举报