ядро opencl, реализующее простую математическую формулу

Какие наилучшие методы следует учитывать при реализации функции ошибки, определенной как

формула ошибки

с помощью ядра OpenCL?

A, B и C — трехмерные массивы с плавающей запятой, а \delta — дельта Кронекера.

Типичные значения для (N, M) = (2, 7) или (N, M) = (3, 23).

Наивная реализация (приведенная ниже) на несколько порядков медленнее процессорной версии.

Спасибо,

T.

__kernel void cl_bilinear_alg(
                            __global float * A,
                            __global float * B,
                            __global float * C,
                            __global const int M,
                            __global const int N,
                            __global float * R)
{
    int index = get_global_id(0);
    int N2 = N * N;
    int mat_offset = index * N2 * M;
    float s1, s2, err = 0.0f;

    for (int i = 0; i < N; ++i)
    {
        for (int j = 0; j < N; ++j)
        {
            for (int k = 0; k < N; ++k)
            {
                for (int l = 0; l < N; ++l)
                {
                    for (int m = 0; m < N; ++m)
                    {
                        for (int n = 0; n < N; ++n)
                        {
                            s1 = (n == i) * (j == k) * (l == m);
                            s2 = 0;

                            for (int r = 0; r < M; ++r)
                            {
                                s2 += A[mat_offset + r * N2 + i * N + j] *
                                      B[mat_offset + r * N2 + k * N + l] *
                                      C[mat_offset + r * N2 + m * N + n];
                            }
                            err += (s2 - s1) * (s2 - s1);
                        }
                    }
                }
            }
        }
    }
    R[index] = err;
}

ОБНОВЛЕНИЕ

Основной целью является Geforce GTX 570, хотя это может измениться в будущем.

ОБНОВЛЕНИЕ 2

После векторизации кода, перемещения битов в локальную память, развертывания некоторых циклов и явной передачи предварительно вычисленных произведений Кронекера ядру код выглядит следующим образом:

__kernel void cl_bilinear_alg(__global const float * A,
                              __global const float * B,
                              __global const float * C,
                              __global const int N,
                              __global const int M,
                              __global const float * kron,
                              __global float * R) 
{
    __private int index = get_global_id(0);
    __private int cM = ceil(M / 4.0f);
    __private int N2 = N*N;
    __private int N4 = N2*N2;
    __private int mat_offset = index * N2 * M;
    __private float s1, s2, err = 0;
    __private float4 vzero = (float4) (0.0f, 0.0f, 0.0f, 0.0f);
    __local float4 va[54], vb[54], vc[54];

for (int ij = 0, k = 0; ij < N2; ++ij)
{
    int r = 0;
    for (; r < M / 4; r += 4, ++k)
    {
        int idx0 = mat_offset + N2 * r + ij;
        int idx1 = mat_offset + N2 * (r + 1) + ij;
        int idx2 = mat_offset + N2 * (r + 2) + ij;
        int idx3 = mat_offset + N2 * (r + 3) + ij;
        va[k] = (float4) (A[idx0], A[idx1], A[idx2], A[idx3]);
        vb[k] = (float4) (B[idx0], B[idx1], B[idx2], B[idx3]);
        vc[k] = (float4) (C[idx0], C[idx1], C[idx2], C[idx3]);
    }

    if (M % 4)
    {
        float buffa[4] = {0}, buffb[4] = {0}, buffc[4] = {0};
        for (; r < M; ++r)
        {
            int idx = mat_offset + N2 * r + ij;
            buffa[r % 4] = A[idx];
            buffb[r % 4] = B[idx];
            buffc[r % 4] = C[idx];
        }
        va[k] = vload4(0, buffa);
        vb[k] = vload4(0, buffb);
        vc[k++] = vload4(0, buffc);
    }
}    

for (int ij = 0; ij < N2; ++ij)
{
    for (int kl = 0; kl < N2; ++kl)
    {
        for (int mn = 0; mn < N2; ++mn)
        {
            s1 = kron[ij * N4 + kl * N2 + mn];
            s2 = 0;
            for (int r = 0; r < cM; ++r)
                s2 += dot(va[cM * ij + r], mad(vb[cM * kl + r], vc[cM * mn + r], vzero));

            //the most expensive line
            err += (s2 - s1) * (s2 - s1);
        }
    }
}

R[index] = err;
}

При применении этих изменений наблюдалось увеличение скорости в 4 раза по сравнению с простой реализацией. Кроме того, выяснилось, что самая дорогая строка — это обновление ошибок, т.е.

err += (s2 - s1) * (s2 - s1);

Какие-либо предложения?


person user92382    schedule 01.06.2012    source источник
comment
какая архитектура устройства? если это Intel с векторными регистрами, вы можете использовать регистры SIMD, чтобы увеличить скорость, до 5 раз быстрее   -  person ardiyu07    schedule 02.06.2012
comment
Наивная версия запускается как отдельный рабочий элемент? Если это так, вы тратите около 99,8% общей арифметической пиковой мощности вашего графического процессора, поэтому не стоит слишком удивляться, что он медленный.....   -  person talonmies    schedule 02.06.2012
comment
@talonmies: нет, каждый рабочий элемент работает над отдельным экземпляром проблемы.   -  person user92382    schedule 02.06.2012
comment
@user92382 user92382 сколько есть рабочих элементов? (т. е. каков диапазон для индекса?) Если он не исчисляется сотнями или тысячами, рассмотрите возможность распределения некоторых циклов, особенно циклов с коротким матричным шагом, по рабочим элементам 2D или 3D.   -  person pmdj    schedule 02.06.2012
comment
@pmjordan: индекс находится в диапазоне от 20 000 до 500 000   -  person user92382    schedule 02.06.2012
comment
@user92382 user92382, если A, B, C постоянны, вы пытались сообщить об этом OpenCL? Другое дело, что N мало, поэтому раскрутка петель очень поможет. Я не уверен, что текущие реализации OpenCL будут развернуты на основе константного параметра ядра. Вы пытались явно сделать N фиксированной константой 2 или 3, а не передавать ее? Другое дело, что все ваши рабочие элементы работают с отдельными областями памяти и выполняют загрузку одновременно. Я бы попытался распараллелить работу так, чтобы рабочие элементы выдавали смежные чтения.   -  person pmdj    schedule 02.06.2012
comment
@ user92382: указанная вами линия не самая дорогая. Это строка кода, удаление которой позволяет компилятору оптимизировать большую часть остальной части ядра. Обращения к глобальной памяти, несомненно, являются малоэффективной частью ядра, и они медленны из-за доступа к шагам. Посмотрите обсуждение объединения памяти в документации OpenCL и CUDA для получения дополнительной информации. См. этот ответ для получения дополнительной информации.   -  person talonmies    schedule 03.06.2012
comment
Это типичная проблема простых операций, большого количества обращений к памяти. Соблюдайте осторожность при чтении/записи памяти как можно меньше и используйте объединенный доступ. Как сказал @talonmies, удаление этой строки ускоряет работу ядра только потому, что компилятор оптимизирует все остальное. Однако на первый взгляд я не вижу ничего неоптимизированного в вашем коде... единственное, что может быть, это скремблированный доступ к kron   -  person DarkZeros    schedule 24.06.2013


Ответы (1)


Как правило, вы хотите разбить некоторые из этих циклов... много... - внешние циклы разбиваются на несколько workgroup, которые работают на своем собственном вычислительном блоке (на GPU приходится около 16 вычислительных блоков, не много) - следующие несколько циклов будут разделены на разные потоки в каждой рабочей группе

Если вы попытаетесь запустить все расчеты одновременно, все они будут одновременно пытаться загрузить данные в память, и это будет просто ужасно тормозить. Графические процессоры имеют очень ограниченную память. Конечно, глобальная память кажется достаточно большой, несколько гигабайт, но глобальная память GPU медленная. Вы хотите получить данные в локальную память, которая находится в расчете на вычислительную единицу и имеет порядок 32-64 КБ, не намного больше.

Обычно вы хотите каким-то образом разделить свою задачу на очень маленькие задачи и сделать следующее для каждой рабочей группы:

  • load a chunk of memory from global memory into local memory
    • the whole workgroup warp of threads can participate in doing the copy, using coallesced access
  • поработайте над этой памятью, например, сложите какие-нибудь суммы и т. д.
  • записать результаты обратно в глобальную память
  • затем можно либо немного повторить, либо просто выйти и оставить другие рабочие группы для выполнения других частей работы.

На ЦП математические операции, как правило, являются основным узким местом, но на графическом процессоре, как правило, ядра в основном бесполезно вращаются, ожидая, пока данные постепенно поступят к ним из глобальной памяти. Все, что вы можете сделать для оптимизации этого процесса, предотвращения противоречивых требований и т. д., значительно ускорит работу ядра.

person Hugh Perkins    schedule 19.02.2015