Раньше я много работал с 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, 10 000, 2500: 0,018 с ~ 60 кадров в секунду. Это примерно в 4-5 раз быстрее, чем на моем процессоре (Intel Core i7 работает на частоте 2,40 ГГц)
100 000, 100 000, 500: 140 с -> что, я думаю, неудивительно, поскольку каждый нейрон во втором слое должен выполнить взвешенную сумму 100 000 элементов. Запуск этого на моем процессоре дает примерно такие же результаты.