RTFSC | Caffe源码阅读 (其二)

这一回我们来深入im2col。我们会顺便介绍一下Cuda的c接口以及一些基本的cuda编程的概念。

Cuda Introduction

http://www.nvidia.com/content/GTC-2010/pdfs/2131_GTC2010.pdf
这是一个很好的入门指南。为了方便起见,我们略去原文中和我们这次需要使用的接口无关的篇幅简要复述。

概述

Cuda是Nvidia为了不了解显卡以及并行操作的人群提供的一个方便的接口。Cuda提供了自己的类C编译器nvcc,在C的基础上添加了关于N卡操作的关键字/接口。你可以直接使用nvcc编译你的程序,也可以把给CPU跑的程序和给GPU跑的代码分别写在.cu,.cpp,把他们分辨编译成.cuo,.o,最后再通过链接器组成一个完整的程序。nvcc提供了比较方便的编程方式,把多流处理器并行处理程序的概念抽象成类似于进程/线程的结构方便使用。同时还提供了cublas这种官方优化过的blas库。

内存管理

Cuda的内存和CPU的内存是分开管理的,Cuda提供了互相映射的办法。Cuda中把GPU内存称为Device,把CPU内存称为Host

int main( void ) {
    int a, b, c; // host copies of a, b, c
    int *dev_a, *dev_b, *dev_c; // device copies of a, b, c
    int size = sizeof( int ); // we need space for an integer

    // allocate device copies of a, b, c
    cudaMalloc( (void**)&dev_a, size );
    cudaMalloc( (void**)&dev_b, size );
    cudaMalloc( (void**)&dev_c, size );
    a = 2;
    b = 7;

    // copy inputs to device
    cudaMemcpy( dev_a, &a, size, cudaMemcpyHostToDevice );
    cudaMemcpy( dev_b, &b, size, cudaMemcpyHostToDevice );

    // launch add() kernel on GPU, passing parameters
    add<<< 1, 1 >>>( dev_a, dev_b, dev_c );

    // copy device result back to host copy of c
    cudaMemcpy( &c, dev_c, size, cudaMemcpyDeviceToHost );
    cudaFree( dev_a );
    cudaFree( dev_b );
    cudaFree( dev_c );

    return 0;
}

我们简单地看一下这一份代码,略去

add<<< 1, 1 >>>(dev_a, dev_b, dev_c);

这一行代码不看的话。我们可以发现这好像和C的malloc, memcpy, free差不多。我们来回顾一下c的分配内存,复制内存,释放内存的办法。(以下摘自http://cplusplus.com/

RTFSC | Caffe源码阅读 (其二)_第1张图片
RTFSC | Caffe源码阅读 (其二)_第2张图片
RTFSC | Caffe源码阅读 (其二)_第3张图片

可以发现,除了cudaMemcpy需要注明一下从Host到Device或者是从Device到Host以外,cudaMemcpy,cudaFree和c的memcpy,free区别不大。比较奇怪的cudaMalloc,为什么传入的参数是指针的指针?其实也很好理解,我们实际上要改变的是指针的值(也就是指针的地址),如果我们把指针传进去,只能够改变原来指向的内存的部分,不能改变指针上的值,所以我们把它的指针穿进去就可以改变它指向的值了(实际上C++的引用传参就是这个原理)。

global 关键字

cuda巧妙地把多核并行运行的操作抽象成了我们熟悉的进程/线程的概念。举个大家熟悉的例子,python使用多进程的例子。原blog地址http://blog.sina.com.cn/s/blog_951bb8a101017icd.html

from multiprocessing import Process

def f(name):
    print 'hello', name

if __name__ == '__main__':
    p = Process(target=f, args=('bob',))
    p.start()
    p.join()

大多数多进程/线程库的设计都是,把运行器(Process)当做一个工厂,把你定义需要并行的多进程库当做一个工作,把工作分配给工厂这样一个设计模式。Cuda提供的接口和这个很像。Cuda定义了两种概念分别对应进程和线程: block(对应进程), thread(对应进程)。如果我们要定义一个并行工作,那么我们要给这个函数顶一个__global__关键字(这个关键字是nvcc提供)。我们考虑一个n维向量加法的任务。

我们把上面的代码重新写一次

__global__ void add( int *a, int *b, int *c ) {
    int index = threadIdx.x + blockIdx.x * blockDim.x;
    c[index] = a[index] + b[index];
}

#define N (2048*2048)
#define THREADS_PER_BLOCK 512

int main( void ) {
    int *a, *b, *c; // host copies of a, b, c
    int *dev_a, *dev_b, *dev_c; // device copies of a, b, c
    int size = N * sizeof( int ); // we need space for N integers

    // allocate device copies of a, b, c
    cudaMalloc( (void**)&dev_a, size );
    cudaMalloc( (void**)&dev_b, size );
    cudaMalloc( (void**)&dev_c, size );

    a = (int*)malloc( size );
    b = (int*)malloc( size );
    c = (int*)malloc( size );
    
    random_ints( a, N );
    random_ints( b, N );

    // copy inputs to device
    cudaMemcpy( dev_a, a, size, cudaMemcpyHostToDevice );
    cudaMemcpy( dev_b, b, size, cudaMemcpyHostToDevice );

    // launch add() kernel with blocks and threads
    add<<< N/THREADS_PER_BLOCK, THREADS_PER_BLOCK >>>( dev_a, dev_b, dev_c );

    // copy device result back to host copy of c
    cudaMemcpy( c, dev_c, size, cudaMemcpyDeviceToHost );

    free( a ); free( b ); free( c );

    cudaFree( dev_a );
    cudaFree( dev_b );
    cudaFree( dev_c );

    return 0;
}

代码中threadIdx.x, blockIdx.x是nvcc在__global__关键字定义的函数中直接定义出的变量。意义是当前所属的block和thread。调用__global__申明的函数的不仅需要调用普通函数需要传入参数,还需要指定运行这个函数你所分配的block和thread。(注意写在三对尖括号中,用逗号隔开)

GLOBAL_FUNCTION<<< block_num, thread_num >>> (params...)

特别需要指出的是,block之间不能共享内存。如果在thread之间如果要声明共享内存的话,使用__shared__关键字。这个例子我们会在下面的同步问题中一起给出代码。

同步问题

说到并行就不得不提锁和同步的问题。这也是软件开发中的日经问题了。我们来看看Cuda中对于同步提供的接口。同样地,我们也通过一个例子来说一下。这个例子我们就举一下内积问题。内积分成两个部分,第一部分把对应的元素相乘,第二步加和。同步问题就会出现了:如果我们把内积分成两部分来做,那么加和的操作必须要等到所有的乘积运算结束之后才能运行;如果我们在乘积结束之后直接把当前的乘积值加入到累加器中,那么我们又面临着加法原子性的问题。我们通过例子分别解释这两种方法如何实现。

__global__ void dot( int *a, int *b, int *c ) {
    // All threads in the same block
    // share `temp` array
    __shared__ int temp[N];
    temp[threadIdx.x] = a[threadIdx.x] * b[threadIdx.x];

    // wait for all thread
    __syncthreads();

    if( 0 == threadIdx.x ) {
        int sum = 0;
        for( int i = 0; i < N; i++ )
        sum += temp[i];
        *c = sum;
    }
}

int main() {
    ...
    dot<<<1, N>>>(a, b, c);
    ...
}

上面这段代码中我们介绍了如何声明同一block下同步内存的__shared__的使用方法,如何等待其他thread同步到同一点的__synchthreads()函数,以及强调了同步问题只能在thread中使用(调用是只分配了一个block)。

我们再介绍另外一种方法。

__global__ void dot( int *a, int *b, int *c ) {
    // All threads in the same block
    // share `temp` array
    __shared__ int temp[N];
    temp[threadIdx.x] = a[threadIdx.x] * b[threadIdx.x];

    // assume that c[0] == 0
    atomicAdd(c , temp[threadIdx.x);
}

以及multiblock的办法

#define N (2048*2048)
#define THREADS_PER_BLOCK 512
__global__ void dot( int *a, int *b, int *c ) {
    __shared__ int temp[THREADS_PER_BLOCK];
    int index = threadIdx.x + blockIdx.x * blockDim.x;
    temp[threadIdx.x] = a[index] * b[index];

     __syncthreads();

    if( 0 == threadIdx.x ) {
        int sum = 0;
        for( int i = 0; i < THREADS_PER_BLOCK; i++ )
            sum += temp[i];
    }
}

Caffe im2col

RTFSC | Caffe源码阅读 (其二)_第4张图片

上面代码中的height_colwidth_col分别是卷积之后的长宽(下一层的长宽)

我们久违地见到了这个函数

im2col_gpu_kernel
<<>>( 
    num_kernels, data_im, height, width, kernel_h, kernel_w, 
    pad_h, pad_w, stride_h, stride_w, 
    dilation_h, dilation_w, height_col, width_col, data_col);

可以知道这是一个template,同时带一个__global__关键字的。我们继续回溯看看im2col_gpu_kernel的实现。

RTFSC | Caffe源码阅读 (其二)_第5张图片

整个过程都比较繁琐,代码需要自己去慢慢理解。为了方便大家理解我在这里告诉大家。我们原来的图片是一个channel X height x width一个三维数组转换成一个(kernel_height * kerner_width * channel) x (height_col * width_col)一个矩阵。

你可能感兴趣的:(RTFSC | Caffe源码阅读 (其二))