改进Perceptron神经网络的OpenCL内核

时间:2014-02-08 20:56:08

标签: performance opencl

之前我一直在做很多OpenGL和着色器,现在,我决定尝试一下OpenCL。我看了一些在线教程,并开始阅读有关该主题的书籍。为了更好地理解,并且因为我认为最好的学习方法是通过智能地尝试和学习这样做时出现的问题,我决定开始为完全连接的感知器实现内核。

对于那些不知道那是什么的人,我会解释一下这个基本想法。它是一个神经网络,其中一个层的每个神经元连接到下一层的每个神经元。每个神经元只有一个动作要执行:执行前一层所有神经元的总和,并按每个神经元的不同值加权。

这看起来很简单,在阅读了“使用OpenCL进行并行神经网络训练”之后,我按照以下方式实现了它

  • 每一层都依赖于前一层,它们由主机按顺序运行

  • 为了计算一个图层,我运行我的内核,其全局工作大小为图层内的神经元数量(例如,可能非常大,数万个)。这使得所有神经元都能彼此独立地执行其总和。

  • 每个神经元(由其global_work_id标识)对前一层的所有神经元执行加权和。

这是我的全功能opencl内核:

/**
* @brief Computes one layer of the perceptron given the previous one and the
* weights
* The kernel is run once for each layer.
* The work items are each tasked with computing the output of a single neuron
* of the out layer.
*
* @param out_layer_size
*   Size of the output layer (number of elements in the output array that will
*   contain the result for each neuron).
* @param in_layer_size
*   Number of elements of the input layer
* @param in_value
*   Values of the neuron in the previous layer
* @param in_weights
*   Array containing the weights for each input neuron. It is organised as a
*   two dimensional matrix, written by concatenating each line in the array
*   [ w11, w12, w13, ...
*     w21, w22, w23, ...
*     ..., ..., ..., ...
*   ]
*   Where wij is the weight linking the neuron i of the input layer to the
*   neuron j of the output layer
* @param out_values
*   Computed values for the current layer
*/
void kernel perceptron(global const int* in_layer_size, global const int* out_layer_size, global const float *in_value, global const float* in_weights, global float* out_values)
{
    private const int global_id = get_global_id(0);
    private const int out_layer_s = *out_layer_size;
    private const int in_layer_s = *in_layer_size;
    private const int offset = out_layer_s * global_id;

    private float sum = 0.;
    for(int i=0; i < in_layer_s; i++) {
        sum += in_weights[i*out_layer_s+global_id] * in_value[i];
    }
    //out_values[global_id] = sigma(sum);
    out_values[global_id] = sum;
}

以下是我如何调用它:

queue.enqueueNDRangeKernel(kernel, cl::NullRange,cl::NDRange(number of neurons within layer),cl::NullRange);

我意识到这个内核的瓶颈是加权和的实现。如果有人可以解释我如何改进这一点以使其更快,那将是非常有用的。

我可能没有正确使用不同的内存区域,我主要考虑的是我甚至不使用的本地内存。

为了让您了解性能(在Nvidia GTX 660M上),我将向您展示我实现的一些时间。每个值是每层神经元的数量:

  • 2500,10000,2500:0.018s~60FPS。它比我的处理器(运行在2.40GHz的Intel Core i7)快4到5倍

  • 100 000,10万,500:140s - &gt;因为第二层中的每个神经元必须执行100 000个元素的加权和,所以我认为这并不令人惊讶。在我的处理器上运行它会产生大致相同的结果。

3 个答案:

答案 0 :(得分:1)

正如你所说,瓶颈是加权汇总。这并不难,因为在每一层,每个WI(工作项)与算术运算的数量相比,正在进行大量的IO操作。我没有神经网络的经验,但对我来说问题看起来像GPU上的内存访问模式不佳。

可能通过将WI组织到本地工作组(工作组)中可以解决这个问题。因为每个WI都需要处理来自prev的所有数据。层,我想WG中的所有WI都可以将一些数据加载到本地内存中,处理它们而不是下一堆数据。这将使您的算法更加缓存友好。内核的伪代码如下:

void kernel Kernel(
__global const int  in_layer_size, 
__global const int  out_layer_size, 
__global const float    *in_value, 
__global const float    *in_weights, 
__global float      *out_values){

__local float buffer[SOME_SIZE];
__global const float* p_in  = in_value;
__global float* p_out = out_values;

const int 
    global_id   = get_global_id(0),
    local_id    = get_local_id(0),
    num_buffers = in_layer_size / SOME_SIZE,
    offset      = out_layer_size * global_id;

float sum = 0.0f;
for(int i=0; i < num_buffers; i++){
    buffer[local_id] = p_in[local_id];
    barrier(CLK_LOCAL_MEM_FENCE);

    //Process all data inside buffer by every WI in WG
    //...

    p_in += SOME_SIZE;
    out_values += SOME_SIZE;
    }

//...
return;

}

所以,你正在使用固定尺寸的窗户滑动。在&amp;内计算数据然后进入下一个窗口。 Al数据操作是独立完成的,工作项只能同时使用相同的数据。本地组的最佳大小取决于设备和内核。

答案 1 :(得分:1)

你可以通过多种方式实现这一目标。 但是,最通用的方法,不改变内核的行为方式就是重用你的工作组大小(无论你选择什么,或默认),并重用组中的内存访问。

我会建议这样的事情:

注意:我删除了单个值的丑陋指针。 OpenCL支持这一点,而且更容易。无需创建内存区域,只需clSetKernelArg(kernel, arg_index, sizeof(cl_float), &size); cl_float size = the_size;

#define IN_LOCAL_SIZE 4096 //Because 16KB/4B (for each float)

void kernel perceptron(global const int in_layer_size, global const int out_layer_size, global const float *in_value, global const float* in_weights, global float* out_values)
{
    const int global_id = get_global_id(0);
    __local float in_buffer[IN_LOCAL_SIZE];

    float sum = 0.0f;
    event_t ev;
    int j;
    //For each full buffer
    for(j=0; j < (in_layer_size/IN_LOCAL_SIZE)-1; i++) {
        ev = async_work_group_copy(in_buffer, in_value+j*IN_LOCAL_SIZE, IN_LOCAL_SIZE, ev);
        wait_group_events(1,&ev);
        barrier(CLK_LOCAL_MEM_FENCE);
        for(int i=0; i < IN_LOCAL_SIZE; i++) {
            sum += in_weights[(i+j*IN_LOCAL_SIZE)*out_layer_size+global_id] * in_buffer[i];
        }
    }
    //Last one
    ev = async_work_group_copy(in_buffer, in_value+j*IN_LOCAL_SIZE, in_layer_size%IN_LOCAL_SIZE, ev);
    wait_group_events(1,&ev);
    barrier(CLK_LOCAL_MEM_FENCE);
    for(int i=0; i < in_layer_size%IN_LOCAL_SIZE; i++) {
        sum += in_weights[(i+j*IN_LOCAL_SIZE)*out_layer_size+global_id] * in_buffer[i];
    }
    out_values[global_id] = sum;
}

但是,如果输出尺寸很小(100k,250k,500),那么您将只有500个工作项,这不是最佳选择。在这种情况下,您应该重塑算法。

一种可能的方法是每个工作项在内层工作,执行总和,整个工作组从所有工作项中创建一个输出。这很容易,因为您可以轻松控制工作组内的总和。

但也许其他方法更适合你的问题。

答案 2 :(得分:1)

您可以通过在本地内存中缓存in_values来进行大量改进。从全局内存中读取in_values的每个元素的次数越少越好。

我提出了一个缓存最大输入值数量的解决方案,并且每个工作组只从全局内存中读取一个元素。这是通过一次复制一个in_values块,对所有out_value处理它,然后继续下一个块来完成的。还有一个本地浮点数组,用于减少每个块的工作项总和。

伪代码:

  output elements assumed to be set to 0 already
  for each block of input values:
    cache the input block
    for each target output value:
      reset local sum to 0
      for each element this work item is responsible for:
        read the weight, multiply, and add to sum
      reduce sums to a single value, ADD value to output element

我还没有机会通过分析器或调试器来运行它,但是当我回到家用PC时,我会尝试一下。 (我的办公室工作站没有opencl工具)。确保对组大小等于GROUP_SIZE常量的内核进行排队。此外,只在您的设备上为每个计算单元创建一个组。

真实代码:

//experiment with GROUP_SIZE to discover the optimal value for your device
//this needs to be equal to local_work_size passed into clEnqueueNDRangeKernel
//use a multiple of CL_KERNEL_PREFERRED_WORK_GROUP_SIZE_MULTIPLE
//max. for most devices is 256
#define GROUP_SIZE = 64;

// IN_VALUE_CACHE_SIZE is the number of floats from in_value to copy to local memory at a time
//assuming GROUP_SIZE can be up to 256, sizeof(float)=4,  and local memory size is 32kb, full saturation can be achieved with the following:
//(32768 - (256 * 4)) /4 = 7936
//try another multiple of 1024 (6144, 4096... )if there is trouble with this value
#define IN_VALUE_CACHE_SIZE = 7936;

void kernel perceptron(global const int* in_layer_size, global const int* out_layer_size, global const float *in_value, global const float* in_weights, global float* out_values)
{
    private const int global_id = get_global_id(0);
    private const int out_layer_s = *out_layer_size;
    private const int in_layer_s = *in_layer_size;
    private const int offset = out_layer_s * global_id;

    private const int item_id = get_local_id(0);    
    private const int group_id = get_group_id(0);   
    private const int group_count = get_num_groups(0);  


    local float result_buffer[GROUP_SIZE];

    local float in_value_cache[IN_VALUE_CACHE_SIZE];
    int i,j,k;

    //init the block to 0, in case there are fewer than IN_VALUE_CACHE_SIZE values in total
    for(i=item_id; i<IN_VALUE_CACHE_SIZE; i+= GROUP_SIZE){
        in_value_cache[i] = 0.0;
    }
    barrier(CL_LOCAL_MEM_FENCE);


    private float sum = 0.0;
    event_t e;
    int copy_total = 0;
    int copy_offset;

    for(i=0; i<in_layer_s; i+=IN_VALUE_CACHE_SIZE){
        //cap the number of values to copy to local memory if loop is near the end of the input data
        copy_total = IN_VALUE_CACHE_SIZE;
        if((copy_total + i*IN_VALUE_CACHE_SIZE) > in_layer_s){
            copy_total = in_layer_s - i*IN_VALUE_CACHE_SIZE;
        }           
        //copy the next block of values
        e = async_work_group_copy(in_value_cache, in_value + i * 4, copy_total, 0);
        wait_group_events(1, &e);

        for(j=group_id; j<out_layer_s; j+=group_count){
            sum = 0.0;

            //need to reset result_buffer[item_id] as well
            //this is in case there are fewer than GROUP_SIZE input values remaining  ie copy_total < GROUP_SIZE
            result_buffer[item_id] = 0.0;

            for(k=item_id; k<copy_total; k+=GROUP_SIZE){
                sum += in_value_cache[k] * in_weights[(k+i) + j * out_layer_s];
            }
            result_buffer[item_id] = sum;

            //simple O(n) reduction can be optimized further
            if(item_id == 0){
                for(k=1;k<GROUP_SIZE;k++){
                    sum += result_buffer[k];
                }
                out_values[j] += sum;
            }
            barrier(CL_LOCAL_MEM_FENCE);
        }

    }
}

这将处理任何大小的输入,因此您可以使用与全局内存一样多的元素来尝试。